using System.Text.RegularExpressions; namespace Core.Printing; /// /// Replaces {{Token}} placeholders from a case-insensitive map. /// Unknown tokens are left unchanged. Known empty values become blank. /// {{PageBreak}} becomes a print page break after HTML conversion. /// {{AnswerSpace}} becomes ruled write-in space after HTML conversion. /// {{RankedEvents}} and {{RankedStudents}} become ranking-index badges. /// {{Legend}} becomes the shared attribute-mark legend. /// public static class NoteTemplateMerger { public const string PageBreakToken = "PageBreak"; public const string PageBreakSentinel = ""; public const string PageBreakHtml = "
"; public const string AnswerSpaceToken = "AnswerSpace"; public const string AnswerSpaceSentinel = ""; public const string AnswerSpaceHtml = "
 
"; public const string LegendToken = "Legend"; public const string LegendSentinel = ""; public const string RankedEventsToken = "RankedEvents"; public const string RankedStudentsToken = "RankedStudents"; public static readonly string[] HtmlFragmentTokens = [ RankedEventsToken, RankedStudentsToken ]; public static string HtmlFragmentSentinel(string name) => $""; private readonly record struct LayoutToken(string Name, string Sentinel, string Html); private static readonly LayoutToken[] LayoutTokens = [ new(PageBreakToken, PageBreakSentinel, PageBreakHtml), new(AnswerSpaceToken, AnswerSpaceSentinel, AnswerSpaceHtml), new(LegendToken, LegendSentinel, PrintRankBadgeHtml.Legend()) ]; private static readonly Regex TokenRegex = new(@"\{\{([^}]+)\}\}", RegexOptions.Compiled); public static string Merge(string? template, IReadOnlyDictionary tokens) { if (string.IsNullOrEmpty(template)) return string.Empty; ArgumentNullException.ThrowIfNull(tokens); return TokenRegex.Replace(template, match => { var key = match.Groups[1].Value.Trim(); if (key.Length == 0) return match.Value; foreach (var layout in LayoutTokens) { if (key.Equals(layout.Name, StringComparison.OrdinalIgnoreCase)) return layout.Sentinel; } foreach (var htmlName in HtmlFragmentTokens) { if (key.Equals(htmlName, StringComparison.OrdinalIgnoreCase)) return HtmlFragmentSentinel(htmlName); } return tokens.TryGetValue(key, out var value) ? value ?? string.Empty : match.Value; }); } /// /// Turns layout sentinels into HTML after markdown has been rendered. /// public static string ApplyLayout( string? html, IReadOnlyDictionary? htmlFragments = null) { if (string.IsNullOrEmpty(html)) return string.Empty; foreach (var layout in LayoutTokens) { html = html .Replace($"

{layout.Sentinel}

", layout.Html, StringComparison.Ordinal) .Replace(layout.Sentinel, layout.Html, StringComparison.Ordinal); } foreach (var name in HtmlFragmentTokens) { var sentinel = HtmlFragmentSentinel(name); var fragment = htmlFragments is not null && htmlFragments.TryGetValue(name, out var value) ? value ?? string.Empty : string.Empty; html = html .Replace($"

{sentinel}

", fragment, StringComparison.Ordinal) .Replace(sentinel, fragment, StringComparison.Ordinal); } return html; } }