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:
@@ -7,7 +7,7 @@ namespace Core.Notes;
|
||||
/// </summary>
|
||||
public static class ImportedFieldsTable
|
||||
{
|
||||
public const string Heading = "## Imported fields";
|
||||
public const string Heading = "## Additional fields";
|
||||
|
||||
public static string NormalizeValue(string? raw)
|
||||
{
|
||||
@@ -18,7 +18,7 @@ public static class ImportedFieldsTable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads Field/Value rows from the Imported fields section.
|
||||
/// Reads Field/Value rows from the Additional fields section.
|
||||
/// </summary>
|
||||
public static List<ImportedField> ParseFields(string? markdown)
|
||||
{
|
||||
@@ -50,7 +50,7 @@ public static class ImportedFieldsTable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Upserts incoming fields into the Imported fields section. Incoming values win.
|
||||
/// Upserts incoming fields into the Additional fields section. Incoming values win.
|
||||
/// Fields not in <paramref name="incoming"/> are kept. Identical values are not changes.
|
||||
/// </summary>
|
||||
public static ImportedFieldsMergeResult Merge(string? existingMarkdown, IReadOnlyList<ImportedField> incoming)
|
||||
@@ -126,7 +126,7 @@ public static class ImportedFieldsTable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unique imported field names across notes, first-seen casing, sorted A–Z.
|
||||
/// Unique additional-field names across notes, first-seen casing, sorted A–Z.
|
||||
/// </summary>
|
||||
public static List<string> DistinctFieldNames(IEnumerable<string?> markdowns)
|
||||
{
|
||||
@@ -150,11 +150,10 @@ public static class ImportedFieldsTable
|
||||
if (string.IsNullOrEmpty(markdown))
|
||||
return null;
|
||||
|
||||
var start = IndexOfHeading(markdown);
|
||||
if (start < 0)
|
||||
if (!TryFindHeading(markdown, out var start, out var headingLength))
|
||||
return null;
|
||||
|
||||
var afterHeading = start + Heading.Length;
|
||||
var afterHeading = start + headingLength;
|
||||
var nextHeading = FindNextHeading(markdown, afterHeading);
|
||||
return nextHeading < 0 ? markdown[start..] : markdown[start..nextHeading];
|
||||
}
|
||||
@@ -164,8 +163,7 @@ public static class ImportedFieldsTable
|
||||
if (string.IsNullOrWhiteSpace(existingMarkdown))
|
||||
return section.TrimEnd() + Environment.NewLine;
|
||||
|
||||
var start = IndexOfHeading(existingMarkdown);
|
||||
if (start < 0)
|
||||
if (!TryFindHeading(existingMarkdown, out var start, out var headingLength))
|
||||
{
|
||||
var prefix = existingMarkdown.TrimEnd();
|
||||
return string.IsNullOrEmpty(prefix)
|
||||
@@ -173,7 +171,7 @@ public static class ImportedFieldsTable
|
||||
: prefix + Environment.NewLine + Environment.NewLine + section;
|
||||
}
|
||||
|
||||
var afterHeading = start + Heading.Length;
|
||||
var afterHeading = start + headingLength;
|
||||
var nextHeading = FindNextHeading(existingMarkdown, afterHeading);
|
||||
var before = existingMarkdown[..start].TrimEnd();
|
||||
var after = nextHeading < 0 ? string.Empty : existingMarkdown[nextHeading..].TrimStart();
|
||||
@@ -199,8 +197,45 @@ public static class ImportedFieldsTable
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static int IndexOfHeading(string markdown) =>
|
||||
markdown.IndexOf(Heading, StringComparison.Ordinal);
|
||||
/// <summary>
|
||||
/// First <c>## … fields</c> heading (any prefix). New sections are written as <see cref="Heading"/>.
|
||||
/// </summary>
|
||||
private static bool TryFindHeading(string markdown, out int start, out int headingLength)
|
||||
{
|
||||
var index = 0;
|
||||
while (index < markdown.Length)
|
||||
{
|
||||
var lineEnd = markdown.IndexOf('\n', index);
|
||||
var end = lineEnd < 0 ? markdown.Length : lineEnd;
|
||||
var line = markdown[index..end].TrimEnd('\r');
|
||||
if (IsFieldsHeading(line))
|
||||
{
|
||||
start = index;
|
||||
headingLength = line.Length;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (lineEnd < 0)
|
||||
break;
|
||||
index = lineEnd + 1;
|
||||
}
|
||||
|
||||
start = -1;
|
||||
headingLength = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsFieldsHeading(string line)
|
||||
{
|
||||
var trimmed = line.Trim();
|
||||
if (!trimmed.StartsWith("## ", StringComparison.Ordinal)
|
||||
|| trimmed.StartsWith("###", StringComparison.Ordinal))
|
||||
return false;
|
||||
|
||||
var title = trimmed[3..].Trim();
|
||||
return title.EndsWith(" fields", StringComparison.OrdinalIgnoreCase)
|
||||
|| title.Equals("fields", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static int FindNextHeading(string markdown, int startIndex)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user