< Summary

Information
Class: DotNetApiDiff.Reporting.HtmlFormatterScriban
Assembly: DotNetApiDiff
File(s): /home/runner/work/dotnet-api-diff/dotnet-api-diff/src/DotNetApiDiff/Reporting/HtmlFormatterScriban.cs
Line coverage
83%
Covered lines: 212
Uncovered lines: 41
Coverable lines: 253
Total lines: 344
Line coverage: 83.7%
Branch coverage
55%
Covered branches: 95
Total branches: 172
Branch coverage: 55.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor()100%1166.66%
Format(...)100%11100%
HtmlEscape(...)100%44100%
PrepareConfigData(...)50%150150100%
PrepareResultData(...)100%11100%
PrepareChangeSections(...)80%131067.69%
GroupChanges(...)100%22100%
PrepareBreakingChangesData(...)100%11100%
FormatBooleanValue(...)100%22100%
RenderChangeGroup(...)100%4485.71%
GetCssStyles()100%1150%
GetJavaScriptCode()100%1150%
GetFallbackStyles()100%210%
GetFallbackJavaScript()100%210%

File(s)

/home/runner/work/dotnet-api-diff/dotnet-api-diff/src/DotNetApiDiff/Reporting/HtmlFormatterScriban.cs

#LineLine coverage
 1// Copyright DotNet API Diff Project Contributors - SPDX Identifier: MIT
 2using DotNetApiDiff.Interfaces;
 3using DotNetApiDiff.Models;
 4using DotNetApiDiff.Models.Configuration;
 5using Scriban;
 6using Scriban.Runtime;
 7using System.Linq;
 8
 9namespace DotNetApiDiff.Reporting;
 10
 11/// <summary>
 12/// Formatter for HTML output with rich formatting and interactive features using Scriban templates
 13/// </summary>
 14public class HtmlFormatterScriban : IReportFormatter
 15{
 16    private readonly Template _mainTemplate;
 17
 818    public HtmlFormatterScriban()
 819    {
 20        // Initialize main template from embedded resources
 21        try
 822        {
 823            _mainTemplate = Template.Parse(EmbeddedTemplateLoader.LoadTemplate("main-layout.scriban"));
 824        }
 025        catch (Exception ex)
 026        {
 027            throw new InvalidOperationException("Failed to initialize Scriban HTML formatter templates. Ensure template 
 28        }
 829    }
 30
 31    /// <summary>
 32    /// Formats a comparison result as HTML using Scriban templates
 33    /// </summary>
 34    /// <param name="result">The comparison result to format</param>
 35    /// <returns>Formatted HTML output as a string</returns>
 36    public string Format(ComparisonResult result)
 637    {
 38        // Create the template context with custom functions
 639        var context = new TemplateContext();
 640        var scriptObject = new ScriptObject();
 41
 42        // Add custom functions
 643        scriptObject.Import("format_boolean", new Func<bool, string>(FormatBooleanValue));
 644        scriptObject.Import("render_change_group", new Func<object, string>(RenderChangeGroup));
 45
 46        // Prepare data for the main template
 647        var resultData = PrepareResultData(result);
 648        var changeSections = PrepareChangeSections(result);
 649        var cssStyles = GetCssStyles();
 650        var javascriptCode = GetJavaScriptCode();
 51
 52        // Add template data to script object
 653        scriptObject.SetValue("result", resultData, true);
 654        scriptObject.SetValue("change_sections", changeSections, true);
 655        scriptObject.SetValue("css_styles", cssStyles, true);
 656        scriptObject.SetValue("javascript_code", javascriptCode, true);
 657        scriptObject.SetValue("config", PrepareConfigData(result.Configuration), true);
 58
 659        context.PushGlobal(scriptObject);
 60
 61        // Set up template loader for includes
 662        context.TemplateLoader = new CustomTemplateLoader();
 63
 664        return _mainTemplate.Render(context);
 665    }
 66
 67    private static string HtmlEscape(string? input)
 1068    {
 1069        if (string.IsNullOrEmpty(input))
 770        {
 771            return input ?? string.Empty;
 72        }
 73
 374        return input
 375            .Replace("&", "&amp;")
 376            .Replace("<", "&lt;")
 377            .Replace(">", "&gt;")
 378            .Replace("\"", "&quot;")
 379            .Replace("'", "&#39;");
 1080    }
 81
 82    private object PrepareConfigData(ComparisonConfiguration config)
 1283    {
 1284        var namespaceMappings = config?.Mappings?.NamespaceMappings ?? new Dictionary<string, List<string>>();
 85
 86        // Convert Dictionary to array of objects with key/value properties for Scriban
 1287        var namespaceMappingsArray = namespaceMappings.Select(kvp => new { key = kvp.Key, value = kvp.Value }).ToList();
 1288        var typeMappingsArray = (config?.Mappings?.TypeMappings ?? new Dictionary<string, string>()).Select(kvp => new {
 89
 1290        var mappingsResult = new
 1291        {
 1292            namespace_mappings = namespaceMappingsArray,
 1293            type_mappings = typeMappingsArray,
 1294            auto_map_same_name_types = config?.Mappings?.AutoMapSameNameTypes ?? false,
 1295            ignore_case = config?.Mappings?.IgnoreCase ?? false
 1296        };
 97
 1298        return new
 1299        {
 12100            filters = new
 12101            {
 12102                include_internals = config?.Filters?.IncludeInternals ?? false,
 12103                include_compiler_generated = config?.Filters?.IncludeCompilerGenerated ?? false,
 12104                include_namespaces = config?.Filters?.IncludeNamespaces?.ToList() ?? new List<string>(),
 12105                exclude_namespaces = config?.Filters?.ExcludeNamespaces?.ToList() ?? new List<string>(),
 12106                include_types = config?.Filters?.IncludeTypes?.ToList() ?? new List<string>(),
 12107                exclude_types = config?.Filters?.ExcludeTypes?.ToList() ?? new List<string>()
 12108            },
 12109            mappings = mappingsResult,
 12110            exclusions = new
 12111            {
 12112                excluded_types = config?.Exclusions?.ExcludedTypes?.ToList() ?? new List<string>(),
 12113                excluded_members = config?.Exclusions?.ExcludedMembers?.ToList() ?? new List<string>(),
 12114                excluded_type_patterns = config?.Exclusions?.ExcludedTypePatterns?.ToList() ?? new List<string>(),
 12115                excluded_member_patterns = config?.Exclusions?.ExcludedMemberPatterns?.ToList() ?? new List<string>(),
 12116                exclude_compiler_generated = config?.Exclusions?.ExcludeCompilerGenerated ?? false,
 12117                exclude_obsolete = config?.Exclusions?.ExcludeObsolete ?? false
 12118            },
 12119            breaking_change_rules = new
 12120            {
 12121                treat_type_removal_as_breaking = config?.BreakingChangeRules?.TreatTypeRemovalAsBreaking ?? true,
 12122                treat_member_removal_as_breaking = config?.BreakingChangeRules?.TreatMemberRemovalAsBreaking ?? true,
 12123                treat_signature_change_as_breaking = config?.BreakingChangeRules?.TreatSignatureChangeAsBreaking ?? true
 12124                treat_reduced_accessibility_as_breaking = config?.BreakingChangeRules?.TreatReducedAccessibilityAsBreaki
 12125                treat_added_type_as_breaking = config?.BreakingChangeRules?.TreatAddedTypeAsBreaking ?? false,
 12126                treat_added_member_as_breaking = config?.BreakingChangeRules?.TreatAddedMemberAsBreaking ?? false,
 12127                treat_added_interface_as_breaking = config?.BreakingChangeRules?.TreatAddedInterfaceAsBreaking ?? true,
 12128                treat_removed_interface_as_breaking = config?.BreakingChangeRules?.TreatRemovedInterfaceAsBreaking ?? tr
 12129                treat_parameter_name_change_as_breaking = config?.BreakingChangeRules?.TreatParameterNameChangeAsBreakin
 12130                treat_added_optional_parameter_as_breaking = config?.BreakingChangeRules?.TreatAddedOptionalParameterAsB
 12131            },
 12132            output_format = config?.OutputFormat.ToString() ?? "Console",
 12133            output_path = config?.OutputPath ?? string.Empty,
 12134            fail_on_breaking_changes = config?.FailOnBreakingChanges ?? false
 12135        };
 12136    }
 137
 138    private object PrepareResultData(ComparisonResult result)
 6139    {
 6140        return new
 6141        {
 6142            comparison_timestamp = result.ComparisonTimestamp,
 6143            old_assembly_name = Path.GetFileName(result.OldAssemblyPath),
 6144            old_assembly_path = result.OldAssemblyPath,
 6145            new_assembly_name = Path.GetFileName(result.NewAssemblyPath),
 6146            new_assembly_path = result.NewAssemblyPath,
 6147            total_differences = result.TotalDifferences,
 6148            has_breaking_changes = result.HasBreakingChanges,
 5149            breaking_changes_count = result.Differences.Count(d => d.IsBreakingChange),
 6150            summary = new
 6151            {
 6152                added_count = result.Summary.AddedCount,
 6153                removed_count = result.Summary.RemovedCount,
 6154                modified_count = result.Summary.ModifiedCount,
 6155                breaking_changes_count = result.Summary.BreakingChangesCount
 6156            },
 6157            configuration = PrepareConfigData(result.Configuration),
 5158            breaking_changes = PrepareBreakingChangesData(result.Differences.Where(d => d.IsBreakingChange))
 6159        };
 6160    }
 161
 162    private object[] PrepareChangeSections(ComparisonResult result)
 6163    {
 6164        var sections = new List<object>();
 165
 11166        var addedItems = result.Differences.Where(d => d.ChangeType == ChangeType.Added).ToList();
 6167        if (addedItems.Any())
 2168        {
 2169            sections.Add(new
 2170            {
 2171                icon = "➕",
 2172                title = "Added Items",
 2173                count = addedItems.Count,
 2174                change_type = "added",
 2175                grouped_changes = GroupChanges(addedItems)
 2176            });
 2177        }
 178
 11179        var removedItems = result.Differences.Where(d => d.ChangeType == ChangeType.Removed).ToList();
 6180        if (removedItems.Any())
 2181        {
 2182            sections.Add(new
 2183            {
 2184                icon = "➖",
 2185                title = "Removed Items",
 2186                count = removedItems.Count,
 2187                change_type = "removed",
 2188                grouped_changes = GroupChanges(removedItems)
 2189            });
 2190        }
 191
 11192        var modifiedItems = result.Differences.Where(d => d.ChangeType == ChangeType.Modified).ToList();
 6193        if (modifiedItems.Any())
 1194        {
 1195            sections.Add(new
 1196            {
 1197                icon = "🔄",
 1198                title = "Modified Items",
 1199                count = modifiedItems.Count,
 1200                change_type = "modified",
 1201                grouped_changes = GroupChanges(modifiedItems)
 1202            });
 1203        }
 204
 11205        var movedItems = result.Differences.Where(d => d.ChangeType == ChangeType.Moved).ToList();
 6206        if (movedItems.Any())
 0207        {
 0208            sections.Add(new
 0209            {
 0210                icon = "📦",
 0211                title = "Moved Items",
 0212                count = movedItems.Count,
 0213                change_type = "moved",
 0214                grouped_changes = GroupChanges(movedItems)
 0215            });
 0216        }
 217
 11218        var excludedItems = result.Differences.Where(d => d.ChangeType == ChangeType.Excluded).ToList();
 6219        if (excludedItems.Any())
 0220        {
 0221            sections.Add(new
 0222            {
 0223                icon = "🚫",
 0224                title = "Excluded Items",
 0225                count = excludedItems.Count,
 0226                change_type = "excluded",
 0227                description = "The following items were intentionally excluded from the comparison:",
 0228                grouped_changes = GroupChanges(excludedItems)
 0229            });
 0230        }
 231
 6232        return sections.ToArray();
 6233    }
 234
 235    private object[] GroupChanges(List<ApiDifference> changes)
 5236    {
 10237        return changes.GroupBy(c => c.ElementType)
 5238            .OrderBy(g => g.Key)
 10239            .Select(g => new
 10240            {
 10241                key = g.Key,
 10242                count = g.Count(),
 10243                changes = g.OrderBy(c => c.ElementName).Select(c => new
 5244                {
 5245                    element_name = c.ElementName,
 5246                    description = c.Description,
 5247                    is_breaking_change = c.IsBreakingChange,
 5248                    has_signatures = !string.IsNullOrEmpty(c.OldSignature) || !string.IsNullOrEmpty(c.NewSignature),
 5249                    old_signature = HtmlEscape(c.OldSignature),
 5250                    new_signature = HtmlEscape(c.NewSignature),
 5251                    details_id = $"details-{Guid.NewGuid():N}"
 5252                }).ToArray()
 10253            }).ToArray();
 5254    }
 255
 256    private object[] PrepareBreakingChangesData(IEnumerable<ApiDifference> breakingChanges)
 6257    {
 7258        return breakingChanges.OrderBy(d => d.Severity)
 1259            .ThenBy(d => d.ElementType)
 1260            .ThenBy(d => d.ElementName)
 7261            .Select(change => new
 7262            {
 7263                severity = change.Severity.ToString(),
 7264                severity_class = change.Severity.ToString().ToLower(),
 7265                element_type = change.ElementType,
 7266                element_name = change.ElementName,
 7267                description = change.Description
 7268            }).ToArray();
 6269    }
 270
 271    private string FormatBooleanValue(bool value)
 102272    {
 102273        return value ? "<span class=\"boolean-true\">✓ True</span>" : "<span class=\"boolean-false\">✗ False</span>";
 102274    }
 275
 276    private string RenderChangeGroup(object sectionData)
 5277    {
 278        try
 5279        {
 280            // Load and parse the change-group template
 5281            var templateContent = EmbeddedTemplateLoader.LoadTemplate("change-group.scriban");
 5282            var template = Template.Parse(templateContent);
 283
 284            // Create a new context for the template with the section data as root
 5285            var context = new TemplateContext();
 5286            var scriptObject = new ScriptObject();
 287
 288            // Add the section data properties to the script object
 5289            if (sectionData != null)
 5290            {
 5291                var sectionType = sectionData.GetType();
 65292                foreach (var property in sectionType.GetProperties())
 25293                {
 25294                    var value = property.GetValue(sectionData);
 25295                    scriptObject.SetValue(property.Name.ToLowerInvariant(), value, true);
 25296                }
 5297            }
 298
 5299            context.PushGlobal(scriptObject);
 300
 5301            return template.Render(context);
 302        }
 0303        catch (Exception ex)
 0304        {
 0305            return $"<!-- Error rendering change group: {ex.Message} -->";
 306        }
 5307    }
 308
 309    private string GetCssStyles()
 6310    {
 311        try
 6312        {
 6313            return EmbeddedTemplateLoader.LoadStyles();
 314        }
 0315        catch (Exception ex)
 0316        {
 0317            System.Diagnostics.Debug.WriteLine($"Warning: Could not load CSS styles, using fallback: {ex.Message}");
 0318            return GetFallbackStyles();
 319        }
 6320    }
 321
 322    private string GetJavaScriptCode()
 6323    {
 324        try
 6325        {
 6326            return EmbeddedTemplateLoader.LoadScripts();
 327        }
 0328        catch (Exception ex)
 0329        {
 0330            System.Diagnostics.Debug.WriteLine($"Warning: Could not load JavaScript, using fallback: {ex.Message}");
 0331            return GetFallbackJavaScript();
 332        }
 6333    }
 334
 335    private string GetFallbackStyles()
 0336    {
 0337        return "body { font-family: Arial, sans-serif; margin: 20px; }";
 0338    }
 339
 340    private string GetFallbackJavaScript()
 0341    {
 0342        return "// Fallback JavaScript";
 0343    }
 344}