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. /// 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 = "
"; private readonly record struct LayoutToken(string Name, string Sentinel, string Html); private static readonly LayoutToken[] LayoutTokens = [ new(PageBreakToken, PageBreakSentinel, PageBreakHtml), new(AnswerSpaceToken, AnswerSpaceSentinel, AnswerSpaceHtml) ]; 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; } 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) { 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); } return html; } }