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)); }