Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 84 additions & 5 deletions src/DotNetApiDiff/Reporting/HtmlFormatterScriban.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ private object[] PrepareChangeSections(ComparisonResult result)
title = "Added Items",
count = addedItems.Count,
change_type = "added",
grouped_changes = GroupChanges(addedItems)
grouped_changes = GroupChangesByType(addedItems)
});
}

Expand All @@ -185,7 +185,7 @@ private object[] PrepareChangeSections(ComparisonResult result)
title = "Removed Items",
count = removedItems.Count,
change_type = "removed",
grouped_changes = GroupChanges(removedItems)
grouped_changes = GroupChangesByType(removedItems)
});
}

Expand All @@ -198,7 +198,7 @@ private object[] PrepareChangeSections(ComparisonResult result)
title = "Modified Items",
count = modifiedItems.Count,
change_type = "modified",
grouped_changes = GroupChanges(modifiedItems)
grouped_changes = GroupChangesByType(modifiedItems)
});
}

Expand All @@ -211,7 +211,7 @@ private object[] PrepareChangeSections(ComparisonResult result)
title = "Moved Items",
count = movedItems.Count,
change_type = "moved",
grouped_changes = GroupChanges(movedItems)
grouped_changes = GroupChangesByType(movedItems)
});
}

Expand All @@ -225,7 +225,7 @@ private object[] PrepareChangeSections(ComparisonResult result)
count = excludedItems.Count,
change_type = "excluded",
description = "The following items were intentionally excluded from the comparison:",
grouped_changes = GroupChanges(excludedItems)
grouped_changes = GroupChangesByType(excludedItems)
});
}

Expand Down Expand Up @@ -253,6 +253,85 @@ private object[] GroupChanges(List<ApiDifference> changes)
}).ToArray();
}

private object[] GroupChangesByType(List<ApiDifference> changes)
{
// Group changes by the containing type first
return changes
.GroupBy(d => ExtractContainingType(d.ElementName, d.ElementType))
.OrderBy(g => g.Key)
.Select(typeGroup => new
{
key = GetTypeDisplayName(typeGroup.Key),
full_type_name = typeGroup.Key,
count = typeGroup.Count(),
breaking_changes_count = typeGroup.Count(c => c.IsBreakingChange),
has_breaking_changes = typeGroup.Any(c => c.IsBreakingChange),
changes = typeGroup.OrderBy(c => c.ElementName).Select(c => new
{
element_name = GetMemberDisplayName(c.ElementName, c.ElementType),
full_element_name = c.ElementName,
element_type = c.ElementType.ToString(),
description = c.Description,
is_breaking_change = c.IsBreakingChange,
has_signatures = !string.IsNullOrEmpty(c.OldSignature) || !string.IsNullOrEmpty(c.NewSignature),
old_signature = HtmlEscape(c.OldSignature),
new_signature = HtmlEscape(c.NewSignature),
details_id = $"details-{Guid.NewGuid():N}",
severity = c.Severity.ToString().ToLower()
}).ToArray()
}).ToArray();
}

private string ExtractContainingType(string elementName, ApiElementType elementType)
{
// For type-level changes, the element name is the type name
if (elementType == ApiElementType.Type)
{
return elementName;
}

// For member-level changes, extract the type from the full name
// Expected format: TypeName.MemberName or Namespace.TypeName.MemberName
var lastDotIndex = elementName.LastIndexOf('.');
if (lastDotIndex > 0)
{
var typePart = elementName.Substring(0, lastDotIndex);

// Handle nested types (Type+NestedType.Member)
var plusIndex = typePart.LastIndexOf('+');
if (plusIndex > 0)
{
return typePart; // Keep the full nested type name
}

return typePart;
}

// Fallback for malformed names
return elementName;
}

private string GetTypeDisplayName(string typeName)
{
// Extract just the type name without namespace for display
var lastDotIndex = typeName.LastIndexOf('.');
return lastDotIndex > 0 ? typeName.Substring(lastDotIndex + 1) : typeName;
}

private string GetMemberDisplayName(string elementName, ApiElementType elementType)
{
// For type-level changes, return the simple type name
if (elementType == ApiElementType.Type)
{
var lastDotIndex = elementName.LastIndexOf('.');
return lastDotIndex > 0 ? elementName.Substring(lastDotIndex + 1) : elementName;
}

// For member-level changes, return just the member name
var memberDotIndex = elementName.LastIndexOf('.');
return memberDotIndex > 0 ? elementName.Substring(memberDotIndex + 1) : elementName;
}

private object[] PrepareBreakingChangesData(IEnumerable<ApiDifference> breakingChanges)
{
return breakingChanges.OrderBy(d => d.Severity)
Expand Down
74 changes: 44 additions & 30 deletions src/DotNetApiDiff/Reporting/HtmlTemplates/change-group.scriban
Original file line number Diff line number Diff line change
@@ -1,41 +1,55 @@
{{for group in grouped_changes}}
<div class="change-group">
<h3>{{ group.key }} ({{ group.count }})</h3>
<div class="change-items">
{{for change in group.changes}}
<div class="change-item {{ change_type }}">
<div class="change-header">
<div class="change-name">
<code>{{ change.element_name }}</code>
{{if change.is_breaking_change}}
<span class="breaking-badge">BREAKING</span>
{{end}}
<div class="type-group">
<div class="type-header">
<h3 class="type-name">
πŸ“ {{ group.key }}
<span class="type-summary">
({{ group.count }} change{{ if group.count != 1 }}s{{ end }}{{ if group.has_breaking_changes }}, {{ group.breaking_changes_count }} breaking{{ end }})
</span>
</h3>
{{if group.has_breaking_changes}}
<span class="breaking-type-badge">⚠️ Breaking Changes</span>
{{end}}
</div>

<div class="type-changes">
<div class="category-changes">
{{for change in group.changes}}
<div class="change-item {{ change_type }}">
<div class="change-header">
<div class="change-name">
<span class="element-type">{{ change.element_type }}</span>
<code>{{ change.element_name }}</code>
{{if change.is_breaking_change}}
<span class="breaking-badge">BREAKING</span>
{{end}}
</div>
<div class="change-description">{{ change.description }}</div>
</div>
<div class="change-description">{{ change.description }}</div>
</div>
{{if change.has_signatures}}
<div class="signature-toggle">
<button class="toggle-btn" onclick="toggleSignature('{{ change.details_id }}')">
<span class="toggle-icon">β–Ό</span> View Signature Details
</button>
</div>
<div id="{{ change.details_id }}" class="signature-details" style="display: none;">
{{if change.old_signature}}
<div class="signature-section">
<h4>Old Signature:</h4>
<pre><code class="csharp">{{ change.old_signature }}</code></pre>
{{if change.has_signatures}}
<div class="signature-toggle">
<button class="toggle-btn" onclick="toggleSignature('{{ change.details_id }}')">
<span class="toggle-icon">β–Ό</span> View Signature Details
</button>
</div>
{{end}}
{{if change.new_signature}}
<div class="signature-section">
<h4>New Signature:</h4>
<pre><code class="csharp">{{ change.new_signature }}</code></pre>
<div id="{{ change.details_id }}" class="signature-details" style="display: none;">
{{if change.old_signature}}
<div class="signature-section">
<h4>Old Signature:</h4>
<pre><code class="csharp">{{ change.old_signature }}</code></pre>
</div>
{{end}}
{{if change.new_signature}}
<div class="signature-section">
<h4>New Signature:</h4>
<pre><code class="csharp">{{ change.new_signature }}</code></pre>
</div>
{{end}}
</div>
{{end}}
</div>
{{end}}
</div>
{{end}}
</div>
</div>
{{end}}
91 changes: 65 additions & 26 deletions src/DotNetApiDiff/Reporting/HtmlTemplates/main-layout.scriban
Original file line number Diff line number Diff line change
Expand Up @@ -56,39 +56,18 @@
<section class="summary">
<h2>πŸ“ˆ Summary</h2>
<div class="summary-cards">
{{if result.summary.added_count > 0}}
<div class="summary-card added clickable" onclick="navigateToSection('added-items')">
<div class="card-number">{{ result.summary.added_count }}</div>
<div class="card-label">Added</div>
</div>
{{else}}
<div class="summary-card added">
<div class="card-number">{{ result.summary.added_count }}</div>
<div class="card-label">Added</div>
</div>
{{end}}
{{if result.summary.removed_count > 0}}
<div class="summary-card removed clickable" onclick="navigateToSection('removed-items')">
<div class="card-number">{{ result.summary.removed_count }}</div>
<div class="card-label">Removed</div>
</div>
{{else}}
<div class="summary-card removed">
<div class="card-number">{{ result.summary.removed_count }}</div>
<div class="card-label">Removed</div>
</div>
{{end}}
{{if result.summary.modified_count > 0}}
<div class="summary-card modified clickable" onclick="navigateToSection('modified-items')">
<div class="card-number">{{ result.summary.modified_count }}</div>
<div class="card-label">Modified</div>
</div>
{{else}}
<div class="summary-card modified">
<div class="card-number">{{ result.summary.modified_count }}</div>
<div class="card-label">Modified</div>
</div>
{{end}}
{{if result.summary.breaking_changes_count > 0}}
<div class="summary-card breaking clickable" onclick="navigateToSection('breaking-changes')">
<div class="card-number">{{ result.summary.breaking_changes_count }}</div>
Expand Down Expand Up @@ -120,9 +99,9 @@
<!-- Breaking changes section -->
{{if result.has_breaking_changes}}
<section class="breaking-changes" id="breaking-changes">
<div class="section-header">
<div class="section-header" onclick="toggleSection('breaking-changes')">
<h2>⚠️ Breaking Changes</h2>
<button class="section-toggle collapsed" onclick="toggleSection('breaking-changes')">
<button class="section-toggle collapsed">
<span class="toggle-icon">β–Ά</span>
</button>
</div>
Expand Down Expand Up @@ -160,17 +139,77 @@
<!-- Change sections -->
{{for section in change_sections}}
<section class="changes-section" id="{{ section.change_type }}-items">
<div class="section-header">
<div class="section-header" onclick="toggleSection('{{ section.change_type }}-items')">
<h2>{{ section.icon }} {{ section.title }} ({{ section.count }})</h2>
<button class="section-toggle collapsed" onclick="toggleSection('{{ section.change_type }}-items')">
<button class="section-toggle collapsed">
<span class="toggle-icon">β–Ά</span>
</button>
</div>
<div class="section-content collapsed" id="{{ section.change_type }}-items-content">
{{if section.description}}
<p class="section-description">{{ section.description }}</p>
{{end}}
{{ render_change_group section }}
<!-- Type groups within this change category -->
{{for group in section.grouped_changes}}
<div class="type-group">
<div class="type-header" onclick="toggleTypeGroup('{{ section.change_type }}-{{ group.key | string.replace " " "-" | string.replace "." "-" | string.replace "`" "-" }}')">
<h3 class="type-name">
πŸ“ {{ group.key }}
<span class="type-summary">
({{ group.count }} change{{ if group.count != 1 }}s{{ end }}{{ if group.has_breaking_changes }}, {{ group.breaking_changes_count }} breaking{{ end }})
</span>
</h3>
<div class="type-header-controls">
{{if group.has_breaking_changes}}
<span class="breaking-type-badge">⚠️ Breaking Changes</span>
{{end}}
<button class="type-toggle collapsed">
<span class="toggle-icon">β–Ά</span>
</button>
</div>
</div>

<div class="type-changes collapsed" id="{{ section.change_type }}-{{ group.key | string.replace " " "-" | string.replace "." "-" | string.replace "`" "-" }}-content">
<div class="category-changes">
{{for change in group.changes}}
<div class="change-item {{ section.change_type }}">
<div class="change-header">
<div class="change-name">
<span class="element-type">{{ change.element_type }}</span>
<code>{{ change.element_name }}</code>
{{if change.is_breaking_change}}
<span class="breaking-badge">BREAKING</span>
{{end}}
</div>
<div class="change-description">{{ change.description }}</div>
</div>
{{if change.has_signatures}}
<div class="signature-toggle">
<button class="toggle-btn" onclick="toggleSignature('{{ change.details_id }}')">
<span class="toggle-icon">β–Ό</span> View Signature Details
</button>
</div>
<div id="{{ change.details_id }}" class="signature-details" style="display: none;">
{{if change.old_signature}}
<div class="signature-section">
<h5>Old Signature:</h5>
<pre><code class="csharp">{{ change.old_signature }}</code></pre>
</div>
{{end}}
{{if change.new_signature}}
<div class="signature-section">
<h5>New Signature:</h5>
<pre><code class="csharp">{{ change.new_signature }}</code></pre>
</div>
{{end}}
</div>
{{end}}
</div>
{{end}}
</div>
</div>
</div>
{{end}}
</div>
</section>
{{end}}
Expand Down
Loading
Loading