feat: add a Tools page printer for note merge and print presets
Chapter officers can merge a markdown note onto filtered students, teams, or events and save the recipe. Extra student-note columns are additional fields (any ## … fields heading) so they work as print tokens whether imported or typed by hand. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Core.Printing;
|
||||
|
||||
/// <summary>
|
||||
/// Replaces <c>{{Token}}</c> placeholders from a case-insensitive map.
|
||||
/// Unknown tokens are left unchanged. Known empty values become blank.
|
||||
/// <c>{{PageBreak}}</c> becomes a print page break after HTML conversion.
|
||||
/// <c>{{AnswerSpace}}</c> becomes ruled write-in space after HTML conversion.
|
||||
/// </summary>
|
||||
public static class NoteTemplateMerger
|
||||
{
|
||||
public const string PageBreakToken = "PageBreak";
|
||||
public const string PageBreakSentinel = "<!--tsa-page-break-->";
|
||||
public const string PageBreakHtml = "<div class=\"pagebreak\"></div>";
|
||||
|
||||
public const string AnswerSpaceToken = "AnswerSpace";
|
||||
public const string AnswerSpaceSentinel = "<!--tsa-answer-space-->";
|
||||
public const string AnswerSpaceHtml = "<div class=\"print-answer-space\"></div>";
|
||||
|
||||
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<string, string> 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;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Turns layout sentinels into HTML after markdown has been rendered.
|
||||
/// </summary>
|
||||
public static string ApplyLayout(string? html)
|
||||
{
|
||||
if (string.IsNullOrEmpty(html))
|
||||
return string.Empty;
|
||||
|
||||
foreach (var layout in LayoutTokens)
|
||||
{
|
||||
html = html
|
||||
.Replace($"<p>{layout.Sentinel}</p>", layout.Html, StringComparison.Ordinal)
|
||||
.Replace(layout.Sentinel, layout.Html, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user