Skip to content

Conversation

@jbrinkman
Copy link
Owner

Overview

This PR replaces the StringBuilder-based HTML generation with a Scriban template engine implementation, providing better maintainability and separation of concerns while preserving single-file deployment capability.

Key Changes

New Files Added

  • HtmlFormatterScriban.cs - New Scriban-based HTML formatter
  • EmbeddedTemplateLoader.cs - Utility for loading embedded template resources
  • CustomTemplateLoader.cs - Scriban ITemplateLoader implementation for include directives
  • Reporting/HtmlTemplates/ directory with separated template files:
    • main-layout.scriban - Main HTML document structure
    • breaking-changes.scriban - Breaking changes section
    • change-group.scriban - Individual change group display
    • configuration.scriban - Configuration details section
    • config-*.scriban - Configuration subsection partials
    • styles.css - Modern responsive CSS styling
    • scripts.js - Interactive JavaScript functionality

Modified Files

  • DotNetApiDiff.csproj - Added Scriban package dependency and embedded resource configuration
  • ReportGenerator.cs - Updated to use new HtmlFormatterScriban instead of HtmlFormatter

Benefits

Improved Maintainability: Template files are now separate and easily editable
Better Separation of Concerns: Presentation logic separated from business logic
Enhanced Styling: Modern responsive CSS with professional appearance
Interactive UI: Collapsible configuration section and better UX
Template Reusability: Include directives allow template composition
Single-File Deployment: All templates embedded as resources during build

Testing

  • All existing tests pass (345 tests successful)
  • HTML output generation verified with template rendering
  • Template include functionality confirmed working
  • Configuration data binding validated

Breaking Changes

None - this is an internal implementation change that maintains the same public API.

Screenshots/Output

The generated HTML reports now feature:

  • Clean, modern responsive design
  • Collapsible configuration section
  • Better visual hierarchy and readability
  • Interactive elements for improved UX

Ready for review! 🚀

- Replace StringBuilder-based HTML generation with Scriban template engine
- Add HtmlFormatterScriban.cs as new template-based formatter
- Create separate template files in Reporting/HtmlTemplates/ directory:
  - main-layout.scriban: Main HTML document structure
  - breaking-changes.scriban: Breaking changes section
  - change-group.scriban: Individual change group display
  - configuration.scriban: Configuration details section
  - config-*.scriban: Configuration subsection partials
  - styles.css: Modern responsive CSS styling
  - scripts.js: Interactive JavaScript functionality
- Add EmbeddedTemplateLoader.cs for resource loading
- Add CustomTemplateLoader.cs for Scriban include functionality
- Configure embedded resources in DotNetApiDiff.csproj
- Update ReportGenerator.cs to use new Scriban formatter
- Maintain single-file deployment capability with embedded resources
- Remove conflicting TestTemplateLoading.cs test file

Benefits:
- Improved maintainability with separated template files
- Better separation of concerns (logic vs presentation)
- Enhanced styling with modern responsive design
- Interactive UI elements (collapsible configuration)
- Template reusability through include directives
Signed-off-by: jbrinkman <[email protected]>
@jbrinkman jbrinkman requested a review from Copilot July 26, 2025 23:16
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR replaces the StringBuilder-based HTML generation with a Scriban template engine implementation, providing better maintainability and separation of concerns while preserving single-file deployment capability.

  • Implements a new Scriban-based HTML formatter that uses separate template files for better organization
  • Adds modern responsive CSS styling and interactive JavaScript functionality
  • Maintains single-file deployment by embedding all templates as resources during build

Reviewed Changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
ReportGenerator.cs Updates to use new HtmlFormatterScriban instead of old HtmlFormatter
HtmlFormatterScriban.cs New Scriban-based HTML formatter implementation
HtmlFormatter.cs Removes old StringBuilder-based HTML formatter
EmbeddedTemplateLoader.cs Utility for loading embedded template resources
CustomTemplateLoader.cs Scriban ITemplateLoader implementation for include directives
DotNetApiDiff.csproj Adds Scriban package dependency and embedded resource configuration
HtmlTemplates/*.scriban Separated template files for main layout, components, and configuration
HtmlTemplates/styles.css Modern responsive CSS styling
HtmlTemplates/scripts.js Interactive JavaScript functionality

Comment on lines 28 to 29
// Get the source code of the template
return template.Page.Body.ToString();
Copy link

Copilot AI Jul 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accessing template.Page.Body.ToString() directly exposes internal implementation details of the Scriban library. This may break if the library changes its internal structure. Consider using the template's own rendering method or a more stable API.

Suggested change
// Get the source code of the template
return template.Page.Body.ToString();
// Render the template using the provided context
return template.Render(context);

Copilot uses AI. Check for mistakes.
catch (Exception ex)
{
// Fallback to basic template if templates can't be loaded
Console.WriteLine($"Warning: Could not load templates, using fallback: {ex.Message}");
Copy link

Copilot AI Jul 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Console.WriteLine directly for error logging is not ideal in a library. Consider using the ILogger that's available in the application or throwing a more specific exception that can be handled by the caller.

Copilot uses AI. Check for mistakes.
Comment on lines 254 to 291
private object PrepareConfigurationData(DotNetApiDiff.Models.Configuration.ComparisonConfiguration config)
{
return new
{
filters = new
{
include_internals = config.Filters.IncludeInternals,
include_compiler_generated = config.Filters.IncludeCompilerGenerated,
include_namespaces = config.Filters.IncludeNamespaces,
exclude_namespaces = config.Filters.ExcludeNamespaces,
include_types = config.Filters.IncludeTypes,
exclude_types = config.Filters.ExcludeTypes
},
mappings = new
{
auto_map_same_name_types = config.Mappings.AutoMapSameNameTypes,
ignore_case = config.Mappings.IgnoreCase,
type_mappings = config.Mappings.TypeMappings.Select(kvp => new { key = kvp.Key, value = kvp.Value }),
namespace_mappings = config.Mappings.NamespaceMappings.Select(kvp => new { key = kvp.Key, value = kvp.Value })
},
exclusions = new
{
excluded_types = config.Exclusions.ExcludedTypes,
excluded_members = config.Exclusions.ExcludedMembers,
excluded_type_patterns = config.Exclusions.ExcludedTypePatterns,
excluded_member_patterns = config.Exclusions.ExcludedMemberPatterns
},
breaking_change_rules = new
{
treat_type_removal_as_breaking = config.BreakingChangeRules.TreatTypeRemovalAsBreaking,
treat_member_removal_as_breaking = config.BreakingChangeRules.TreatMemberRemovalAsBreaking,
treat_signature_change_as_breaking = config.BreakingChangeRules.TreatSignatureChangeAsBreaking,
treat_reduced_accessibility_as_breaking = config.BreakingChangeRules.TreatReducedAccessibilityAsBreaking
},
output_format = config.OutputFormat,
fail_on_breaking_changes = config.FailOnBreakingChanges,
output_path = config.OutputPath
};
Copy link

Copilot AI Jul 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PrepareConfigurationData method is duplicated with PrepareConfigData at line 80. Both methods appear to do similar things but with different object structures. Consider consolidating these methods to avoid code duplication.

Suggested change
private object PrepareConfigurationData(DotNetApiDiff.Models.Configuration.ComparisonConfiguration config)
{
return new
{
filters = new
{
include_internals = config.Filters.IncludeInternals,
include_compiler_generated = config.Filters.IncludeCompilerGenerated,
include_namespaces = config.Filters.IncludeNamespaces,
exclude_namespaces = config.Filters.ExcludeNamespaces,
include_types = config.Filters.IncludeTypes,
exclude_types = config.Filters.ExcludeTypes
},
mappings = new
{
auto_map_same_name_types = config.Mappings.AutoMapSameNameTypes,
ignore_case = config.Mappings.IgnoreCase,
type_mappings = config.Mappings.TypeMappings.Select(kvp => new { key = kvp.Key, value = kvp.Value }),
namespace_mappings = config.Mappings.NamespaceMappings.Select(kvp => new { key = kvp.Key, value = kvp.Value })
},
exclusions = new
{
excluded_types = config.Exclusions.ExcludedTypes,
excluded_members = config.Exclusions.ExcludedMembers,
excluded_type_patterns = config.Exclusions.ExcludedTypePatterns,
excluded_member_patterns = config.Exclusions.ExcludedMemberPatterns
},
breaking_change_rules = new
{
treat_type_removal_as_breaking = config.BreakingChangeRules.TreatTypeRemovalAsBreaking,
treat_member_removal_as_breaking = config.BreakingChangeRules.TreatMemberRemovalAsBreaking,
treat_signature_change_as_breaking = config.BreakingChangeRules.TreatSignatureChangeAsBreaking,
treat_reduced_accessibility_as_breaking = config.BreakingChangeRules.TreatReducedAccessibilityAsBreaking
},
output_format = config.OutputFormat,
fail_on_breaking_changes = config.FailOnBreakingChanges,
output_path = config.OutputPath
};
private object PrepareConfigData(object config)
{
if (config is DotNetApiDiff.Models.Configuration.ComparisonConfiguration comparisonConfig)
{
return new
{
filters = new
{
include_internals = comparisonConfig.Filters.IncludeInternals,
include_compiler_generated = comparisonConfig.Filters.IncludeCompilerGenerated,
include_namespaces = comparisonConfig.Filters.IncludeNamespaces,
exclude_namespaces = comparisonConfig.Filters.ExcludeNamespaces,
include_types = comparisonConfig.Filters.IncludeTypes,
exclude_types = comparisonConfig.Filters.ExcludeTypes
},
mappings = new
{
auto_map_same_name_types = comparisonConfig.Mappings.AutoMapSameNameTypes,
ignore_case = comparisonConfig.Mappings.IgnoreCase,
type_mappings = comparisonConfig.Mappings.TypeMappings.Select(kvp => new { key = kvp.Key, value = kvp.Value }),
namespace_mappings = comparisonConfig.Mappings.NamespaceMappings.Select(kvp => new { key = kvp.Key, value = kvp.Value })
},
exclusions = new
{
excluded_types = comparisonConfig.Exclusions.ExcludedTypes,
excluded_members = comparisonConfig.Exclusions.ExcludedMembers,
excluded_type_patterns = comparisonConfig.Exclusions.ExcludedTypePatterns,
excluded_member_patterns = comparisonConfig.Exclusions.ExcludedMemberPatterns
},
breaking_change_rules = new
{
treat_type_removal_as_breaking = comparisonConfig.BreakingChangeRules.TreatTypeRemovalAsBreaking,
treat_member_removal_as_breaking = comparisonConfig.BreakingChangeRules.TreatMemberRemovalAsBreaking,
treat_signature_change_as_breaking = comparisonConfig.BreakingChangeRules.TreatSignatureChangeAsBreaking,
treat_reduced_accessibility_as_breaking = comparisonConfig.BreakingChangeRules.TreatReducedAccessibilityAsBreaking
},
output_format = comparisonConfig.OutputFormat,
fail_on_breaking_changes = comparisonConfig.FailOnBreakingChanges,
output_path = comparisonConfig.OutputPath
};
}
// Add handling for other configuration types here if needed
throw new ArgumentException("Unsupported configuration type", nameof(config));

Copilot uses AI. Check for mistakes.
Comment on lines 316 to 317
catch
{
Copy link

Copilot AI Jul 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty catch blocks without specific exception types can hide important errors. Consider catching specific exceptions or at least logging the error information.

Suggested change
catch
{
catch (Exception ex)
{
Console.Error.WriteLine($"Error loading CSS styles: {ex.Message}");

Copilot uses AI. Check for mistakes.
Comment on lines 328 to 329
catch
{
Copy link

Copilot AI Jul 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty catch blocks without specific exception types can hide important errors. Consider catching specific exceptions or at least logging the error information.

Suggested change
catch
{
catch (Exception ex)
{
Console.Error.WriteLine($"Error loading JavaScript: {ex.Message}");

Copilot uses AI. Check for mistakes.
- Fix CustomTemplateLoader to avoid using internal Scriban API
- Replace Console.WriteLine with proper exception handling
- Remove duplicate PrepareConfigurationData method
- Improve error handling in GetCssStyles() and GetJavaScriptCode()
- Use Debug.WriteLine for non-critical warnings instead of empty catch blocks

These changes improve code robustness, eliminate code duplication, and follow better error handling practices.
@jbrinkman
Copy link
Owner Author

Code Review Response

I've reviewed and addressed all the feedback from the PR review. Here's my evaluation and the changes made:

Comment 1: CustomTemplateLoader internal API usage

Issue: Accessing template.Page.Body.ToString() exposes internal Scriban implementation details.
Evaluation: ✅ Valid concern - this approach was brittle and could break with library updates.
Resolution: Refactored CustomTemplateLoader to load template source directly from embedded resources instead of relying on compiled Template objects' internal structure.

Comment 2: Console.WriteLine for error logging

Issue: Using Console.WriteLine directly for error logging is not ideal in a library.
Evaluation: ✅ Valid concern - libraries should not write directly to console.
Resolution: Replaced Console.WriteLine with proper exception throwing. The constructor now throws InvalidOperationException with clear error messages when templates cannot be loaded, allowing calling code to handle the error appropriately.

Comment 3: Duplicate PrepareConfigurationData method

Issue: Two similar methods (PrepareConfigData and PrepareConfigurationData) doing similar work.
Evaluation: ✅ Valid concern - code duplication should be eliminated.
Resolution: Removed the duplicate PrepareConfigurationData method and updated all usages to use the consolidated PrepareConfigData method, which includes proper null-safe handling with ?.ToList() ?? new List<string>().

Comments 4 & 5: Empty catch blocks

Issue: Empty catch blocks in GetCssStyles() and GetJavaScriptCode() hide important errors.
Evaluation: ✅ Valid concern - errors should be logged for debugging purposes.
Resolution: Added proper exception handling that logs warnings using System.Diagnostics.Debug.WriteLine() instead of silently ignoring errors. This provides visibility for debugging while gracefully falling back to default styles/scripts.

Summary

All review feedback has been addressed with appropriate solutions that improve:

  • API Stability: No longer dependent on internal Scriban APIs
  • Error Handling: Proper exception throwing and debug logging
  • Code Quality: Eliminated duplication and improved maintainability
  • Library Design: Removed direct console output in favor of proper error propagation

The formatter functionality remains intact and all tests continue to pass. The changes make the code more robust and follow better engineering practices.

@jbrinkman jbrinkman merged commit 2b3bc20 into main Jul 26, 2025
7 checks passed
@jbrinkman jbrinkman deleted the feature/scriban-html-formatter branch July 26, 2025 23:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants