< Summary

Information
Class: Chronicis.Api.Services.WikiLinkTitleRewriter
Assembly: Chronicis.Api
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Api/Services/WikiLinkTitleRewriter.cs
Line coverage
100%
Covered lines: 35
Uncovered lines: 0
Coverable lines: 35
Total lines: 64
Line coverage: 100%
Branch coverage
100%
Covered branches: 2
Total branches: 2
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
Rewrite(...)100%22100%

File(s)

/home/runner/work/chronicis/chronicis/src/Chronicis.Api/Services/WikiLinkTitleRewriter.cs

#LineLine coverage
 1using System.Net;
 2using System.Text.RegularExpressions;
 3
 4namespace Chronicis.Api.Services;
 5
 6/// <summary>
 7/// Regex-based implementation of <see cref="IWikiLinkTitleRewriter"/>.
 8/// Operates on HTML string content produced by the TipTap editor.
 9/// No external HTML parser dependency — uses a single <see cref="GeneratedRegex"/>
 10/// mirroring the span format used by the wiki-link TipTap extension.
 11/// </summary>
 12public sealed partial class WikiLinkTitleRewriter : IWikiLinkTitleRewriter
 13{
 14    /// <inheritdoc/>
 15    public (string Body, bool Changed) Rewrite(string? body, Guid targetArticleId, string newTitle)
 16    {
 1617        if (string.IsNullOrEmpty(body))
 218            return (string.Empty, false);
 19
 1420        var targetIdStr = targetArticleId.ToString("D");
 1421        var encodedTitle = WebUtility.HtmlEncode(newTitle);
 1422        var changed = false;
 23
 1424        var result = WikiLinkSpanRegex().Replace(body, match =>
 1425        {
 1426            var attrs = match.Groups[1].Value;
 1427            var innerText = match.Groups[2].Value;
 1428
 1429            // Must be a wiki-link type span
 1430            if (!attrs.Contains("data-type=\"wiki-link\"", StringComparison.OrdinalIgnoreCase))
 1431                return match.Value;
 1432
 1433            // Must target the renamed article (case-insensitive GUID match)
 1434            if (!attrs.Contains($"data-target-id=\"{targetIdStr}\"", StringComparison.OrdinalIgnoreCase))
 1435                return match.Value;
 1436
 1437            // Skip if user supplied a custom label — presence of data-display= disqualifies,
 1438            // regardless of value (including empty string).
 1439            if (attrs.Contains("data-display=", StringComparison.OrdinalIgnoreCase))
 1440                return match.Value;
 1441
 1442            // Skip map chips (wiki-link spans with data-map-id attribute)
 1443            if (attrs.Contains("data-map-id=", StringComparison.OrdinalIgnoreCase))
 1444                return match.Value;
 1445
 1446            // Skip spans explicitly marked broken
 1447            if (attrs.Contains("data-broken=\"true\"", StringComparison.OrdinalIgnoreCase))
 1448                return match.Value;
 1449
 1450            changed = true;
 1451            return $"<span{attrs}>{encodedTitle}</span>";
 1452        });
 53
 1454        return (result, changed);
 55    }
 56
 57    /// <summary>
 58    /// Matches any &lt;span&gt; element, capturing the attribute block (group 1) and
 59    /// plain-text inner content (group 2).  <c>[^&lt;]*</c> in group 2 ensures spans
 60    /// with nested markup are not matched, keeping inner-text-only spans eligible.
 61    /// </summary>
 62    [GeneratedRegex(@"<span(\s[^>]*?)>([^<]*)</span>", RegexOptions.IgnoreCase)]
 63    private static partial Regex WikiLinkSpanRegex();
 64}