namespace Core.Printing;
///
/// Builds a case-insensitive token map. Fill order is imported, then entity, then chapter
/// so built-in names win over an imported column with the same name.
///
public static class PrintTokenMap
{
public static Dictionary Create() =>
new(StringComparer.OrdinalIgnoreCase);
public static Dictionary Build(
IReadOnlyDictionary? imported,
IReadOnlyDictionary? entity,
IReadOnlyDictionary? chapter)
{
var map = Create();
Apply(map, imported);
Apply(map, entity);
Apply(map, chapter);
return map;
}
public static void Apply(IDictionary map, IReadOnlyDictionary? values)
{
if (values is null)
return;
foreach (var (key, value) in values)
{
if (string.IsNullOrWhiteSpace(key))
continue;
map[key] = Escape(value);
}
}
///
/// Treats substituted values as plain text so they cannot change markdown or inject HTML.
/// Line breaks are flattened so a value cannot end a markdown table row.
///
public static string Escape(string? value)
{
if (string.IsNullOrEmpty(value))
return string.Empty;
return FlattenLines(value)
.Replace("\\", "\\\\", StringComparison.Ordinal)
.Replace("|", "\\|", StringComparison.Ordinal)
.Replace("*", "\\*", StringComparison.Ordinal)
.Replace("_", "\\_", StringComparison.Ordinal)
.Replace("`", "\\`", StringComparison.Ordinal)
.Replace("[", "\\[", StringComparison.Ordinal)
.Replace("]", "\\]", StringComparison.Ordinal)
.Replace("<", "<", StringComparison.Ordinal)
.Replace(">", ">", StringComparison.Ordinal);
}
///
/// Escapes a value for insertion into generated print HTML (badge labels).
///
public static string EscapeHtml(string? value)
{
if (string.IsNullOrEmpty(value))
return string.Empty;
return FlattenLines(value)
.Replace("&", "&", StringComparison.Ordinal)
.Replace("<", "<", StringComparison.Ordinal)
.Replace(">", ">", StringComparison.Ordinal)
.Replace("\"", """, StringComparison.Ordinal);
}
private static string FlattenLines(string value) =>
string.Join(' ',
value.Split(['\r', '\n', '\u2028', '\u2029'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries));
}