< Summary

Line coverage
92%
Covered lines: 59
Uncovered lines: 5
Coverable lines: 64
Total lines: 239
Line coverage: 92.1%
Branch coverage
100%
Covered branches: 12
Total branches: 12
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: HtmlRegex()100%11100%
File 2: .ctor(...)100%11100%
File 2: ToHtml(...)100%2262.5%
File 2: ToPlainText(...)100%2271.42%
File 2: GetPreview(...)100%22100%
File 2: IsHtml(...)100%22100%
File 2: EnsureHtml(...)100%44100%

File(s)

/home/runner/work/chronicis/chronicis/src/Chronicis.Client/obj/Release/net9.0/System.Text.RegularExpressions.Generator/System.Text.RegularExpressions.Generator.RegexGenerator/RegexGenerator.g.cs

File '/home/runner/work/chronicis/chronicis/src/Chronicis.Client/obj/Release/net9.0/System.Text.RegularExpressions.Generator/System.Text.RegularExpressions.Generator.RegexGenerator/RegexGenerator.g.cs' does not exist (any more).

/home/runner/work/chronicis/chronicis/src/Chronicis.Client/Services/MarkdownService.cs

#LineLine coverage
 1using System.Text.RegularExpressions;
 2using Chronicis.Shared.Extensions;
 3using Ganss.Xss;
 4using Markdig;
 5
 6namespace Chronicis.Client.Services;
 7
 8/// <summary>
 9/// Service for converting markdown to sanitized HTML
 10/// </summary>
 11public partial class MarkdownService : IMarkdownService
 12{
 13    private readonly MarkdownPipeline _pipeline;
 14    private readonly HtmlSanitizer _sanitizer;
 15    private readonly ILogger<MarkdownService> _logger;
 16
 17    [GeneratedRegex(@"<(p|h[1-6]|ul|ol|li|strong|em|a|pre|code|blockquote|div|span|br)[^>]*>", RegexOptions.Compiled | R
 18    private static partial Regex HtmlRegex();
 19
 3420    public MarkdownService(ILogger<MarkdownService> logger)
 21    {
 3422        _logger = logger;
 23
 24        // Configure Markdig pipeline with extensions
 3425        _pipeline = new MarkdownPipelineBuilder()
 3426            .UseAdvancedExtensions() // Tables, task lists, etc.
 3427            .UseEmojiAndSmiley()     // :smile: syntax
 3428            .UsePipeTables()         // GitHub-style tables
 3429            .UseGridTables()         // Complex tables
 3430            .UseAutoLinks()          // Auto-detect URLs
 3431            .UseGenericAttributes()  // Add CSS classes
 3432            .Build();
 33
 34        // Configure HTML sanitizer to prevent XSS
 3435        _sanitizer = new HtmlSanitizer();
 36
 37        // Allow common markdown HTML elements
 3438        _sanitizer.AllowedTags.Add("h1");
 3439        _sanitizer.AllowedTags.Add("h2");
 3440        _sanitizer.AllowedTags.Add("h3");
 3441        _sanitizer.AllowedTags.Add("h4");
 3442        _sanitizer.AllowedTags.Add("h5");
 3443        _sanitizer.AllowedTags.Add("h6");
 3444        _sanitizer.AllowedTags.Add("table");
 3445        _sanitizer.AllowedTags.Add("thead");
 3446        _sanitizer.AllowedTags.Add("tbody");
 3447        _sanitizer.AllowedTags.Add("tr");
 3448        _sanitizer.AllowedTags.Add("th");
 3449        _sanitizer.AllowedTags.Add("td");
 3450        _sanitizer.AllowedTags.Add("img");
 3451        _sanitizer.AllowedTags.Add("code");
 3452        _sanitizer.AllowedTags.Add("pre");
 3453        _sanitizer.AllowedTags.Add("blockquote");
 3454        _sanitizer.AllowedTags.Add("del");
 3455        _sanitizer.AllowedTags.Add("ins");
 56
 57        // Allow necessary attributes
 3458        _sanitizer.AllowedAttributes.Add("class");
 3459        _sanitizer.AllowedAttributes.Add("src");
 3460        _sanitizer.AllowedAttributes.Add("alt");
 3461        _sanitizer.AllowedAttributes.Add("href");
 3462        _sanitizer.AllowedAttributes.Add("title");
 63
 64        // Allow data attributes for syntax highlighting
 3465        _sanitizer.AllowDataAttributes = true;
 3466    }
 67
 68    /// <summary>
 69    /// Convert markdown text to sanitized HTML
 70    /// </summary>
 71    public string ToHtml(string markdown)
 72    {
 1373        if (string.IsNullOrWhiteSpace(markdown))
 374            return string.Empty;
 75
 76        try
 77        {
 78            // Convert markdown to HTML
 1079            var html = Markdown.ToHtml(markdown, _pipeline);
 80
 81            // Sanitize to prevent XSS
 1082            return _sanitizer.Sanitize(html);
 83        }
 084        catch (Exception ex)
 85        {
 86            // Log error and return escaped text as fallback
 087            _logger.LogErrorSanitized(ex, "Error converting markdown to HTML");
 088            return $"<p>{System.Net.WebUtility.HtmlEncode(markdown)}</p>";
 89        }
 1090    }
 91
 92    /// <summary>
 93    /// Convert markdown to plain text (strip formatting)
 94    /// </summary>
 95    public string ToPlainText(string markdown)
 96    {
 897        if (string.IsNullOrWhiteSpace(markdown))
 298            return string.Empty;
 99
 100        try
 101        {
 6102            var html = Markdown.ToPlainText(markdown, _pipeline);
 6103            return html;
 104        }
 0105        catch
 106        {
 0107            return markdown;
 108        }
 6109    }
 110
 111    /// <summary>
 112    /// Get a preview of the markdown (first N characters as plain text)
 113    /// </summary>
 114    public string GetPreview(string markdown, int maxLength = 200)
 115    {
 4116        var plainText = ToPlainText(markdown);
 117
 4118        if (plainText.Length <= maxLength)
 2119            return plainText;
 120
 2121        return string.Concat(plainText.AsSpan(0, maxLength), "...");
 122    }
 123
 124    /// <summary>
 125    /// Detects if content is HTML (vs markdown).
 126    /// HTML from TipTap will have tags like p, h1, ul, etc.
 127    /// Markdown will have #, *, -, etc. without HTML tags.
 128    /// </summary>
 129    public bool IsHtml(string content)
 130    {
 13131        if (string.IsNullOrWhiteSpace(content))
 2132            return false;
 133
 134        // Check for common HTML tags that TipTap produces
 11135        return HtmlRegex().IsMatch(content);
 136    }
 137
 138    /// <summary>
 139    /// Ensures content is HTML. If content appears to be markdown, converts it to HTML.
 140    /// If content is already HTML, returns it as-is (after sanitization).
 141    /// </summary>
 142    public string EnsureHtml(string content)
 143    {
 6144        if (string.IsNullOrWhiteSpace(content))
 2145            return string.Empty;
 146
 4147        if (IsHtml(content))
 148        {
 149            // Already HTML - just sanitize and return
 2150            return _sanitizer.Sanitize(content);
 151        }
 152
 153        // Content appears to be markdown - convert to HTML
 2154        return ToHtml(content);
 155    }
 156}