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:
2026-08-30 15:38:43 -04:00
co-authored by Cursor
parent 4cfd85b902
commit 3f50d6e635
39 changed files with 2199 additions and 69 deletions
+119
View File
@@ -0,0 +1,119 @@
using Core.Printing;
namespace Tests.Printing;
[TestFixture]
public class NoteTemplateMerger_Tests
{
[Test]
public void Merge_ReplacesKnownTokens()
{
var tokens = PrintTokenMap.Create();
tokens["FirstName"] = "Aria";
tokens["LastName"] = "Cole";
var result = NoteTemplateMerger.Merge("Hello {{FirstName}} {{LastName}}", tokens);
Assert.That(result, Is.EqualTo("Hello Aria Cole"));
}
[Test]
public void Merge_SupportsDottedAndSpacedNames()
{
var tokens = PrintTokenMap.Create();
tokens["Chapter.Name"] = "North TSA";
tokens["Interview Time"] = "3:20-3:35";
var result = NoteTemplateMerger.Merge(
"{{Chapter.Name}} at {{Interview Time}}",
tokens);
Assert.That(result, Is.EqualTo("North TSA at 3:20-3:35"));
}
[Test]
public void Merge_IsCaseAndWhitespaceTolerant()
{
var tokens = PrintTokenMap.Create();
tokens["FirstName"] = "Aria";
var result = NoteTemplateMerger.Merge("{{ firstname }}", tokens);
Assert.That(result, Is.EqualTo("Aria"));
}
[Test]
public void Merge_LeavesUnknownTokensInPlace()
{
var tokens = PrintTokenMap.Create();
tokens["FirstName"] = "Aria";
var result = NoteTemplateMerger.Merge("{{FirstName}} {{Missing}}", tokens);
Assert.That(result, Is.EqualTo("Aria {{Missing}}"));
}
[Test]
public void Merge_KnownEmptyBecomesBlank()
{
var tokens = PrintTokenMap.Create();
tokens["Interview Time"] = string.Empty;
var result = NoteTemplateMerger.Merge("Time: {{Interview Time}}.", tokens);
Assert.That(result, Is.EqualTo("Time: ."));
}
[Test]
public void Merge_EmptyTemplate_IsEmpty()
{
Assert.That(NoteTemplateMerger.Merge(null, PrintTokenMap.Create()), Is.EqualTo(string.Empty));
Assert.That(NoteTemplateMerger.Merge("", PrintTokenMap.Create()), Is.EqualTo(string.Empty));
}
[Test]
public void Merge_PageBreak_IsSentinel()
{
var result = NoteTemplateMerger.Merge(
"Above\n{{PageBreak}}\nBelow",
PrintTokenMap.Create());
Assert.That(result, Does.Contain(NoteTemplateMerger.PageBreakSentinel));
Assert.That(result, Does.Not.Contain("{{PageBreak}}"));
}
[Test]
public void ApplyLayout_ReplacesSentinelAndParagraphWrap()
{
var raw = NoteTemplateMerger.ApplyLayout(
$"x{NoteTemplateMerger.PageBreakSentinel}y");
var wrapped = NoteTemplateMerger.ApplyLayout(
$"<p>{NoteTemplateMerger.PageBreakSentinel}</p>");
Assert.That(raw, Is.EqualTo($"x{NoteTemplateMerger.PageBreakHtml}y"));
Assert.That(wrapped, Is.EqualTo(NoteTemplateMerger.PageBreakHtml));
}
[Test]
public void Merge_AnswerSpace_IsSentinel()
{
var result = NoteTemplateMerger.Merge(
"Q?\n{{AnswerSpace}}\nNext",
PrintTokenMap.Create());
Assert.That(result, Does.Contain(NoteTemplateMerger.AnswerSpaceSentinel));
Assert.That(result, Does.Not.Contain("{{AnswerSpace}}"));
}
[Test]
public void ApplyLayout_ReplacesAnswerSpaceSentinelAndParagraphWrap()
{
var raw = NoteTemplateMerger.ApplyLayout(
$"x{NoteTemplateMerger.AnswerSpaceSentinel}y");
var wrapped = NoteTemplateMerger.ApplyLayout(
$"<p>{NoteTemplateMerger.AnswerSpaceSentinel}</p>");
Assert.That(raw, Is.EqualTo($"x{NoteTemplateMerger.AnswerSpaceHtml}y"));
Assert.That(wrapped, Is.EqualTo(NoteTemplateMerger.AnswerSpaceHtml));
}
}