using System.Text; namespace LehrerApp.Templating; public sealed record TemplateRichTextRun(string Text, string? Placeholder, string? Format, bool Bold, bool Italic, bool Underline) { public bool IsPlaceholder => Placeholder is not null; } public static class TemplateRichText { public static IReadOnlyList Parse(string source) { var runs = new List(); var literal = new StringBuilder(); var styles = new Stack(); void Flush() { if (literal.Length == 0) return; AddRun(runs, new(literal.ToString(), null, null, styles.Contains('b'), styles.Contains('i'), styles.Contains('u'))); literal.Clear(); } for (var index = 0; index < source.Length;) { if (source[index] == '\\' && index + 1 < source.Length && source[index + 1] is '\\' or '[' or '$') { literal.Append(source[index + 1]); index += 2; continue; } if (TryTag(source, index, out var tag, out var closing, out var tagLength)) { Flush(); if (closing) { if (styles.Count == 0 || styles.Peek() != tag) throw new InvalidDataException($"Rich-Text-Tag [/{tag}] ist nicht passend geƶffnet."); styles.Pop(); } else styles.Push(tag); index += tagLength; continue; } if (source[index] == '$' && TryPlaceholder(source, index, out var name, out var format, out var length)) { Flush(); AddRun(runs, new("", name, format, styles.Contains('b'), styles.Contains('i'), styles.Contains('u'))); index += length; continue; } literal.Append(source[index++]); } Flush(); if (styles.Count > 0) throw new InvalidDataException($"Rich-Text-Tag [{styles.Peek()}] wurde nicht geschlossen."); return runs; } public static IReadOnlyList UsedPlaceholders(string source) => Parse(source) .Where(x => x.Placeholder is not null).Select(x => x.Placeholder!).Distinct(StringComparer.Ordinal).ToList(); private static bool TryTag(string source, int index, out char tag, out bool closing, out int length) { tag = default; closing = false; length = 0; if (index + 2 < source.Length && source[index] == '[' && source[index + 2] == ']' && source[index + 1] is 'b' or 'i' or 'u') { tag = source[index + 1]; length = 3; return true; } if (index + 3 < source.Length && source[index] == '[' && source[index + 1] == '/' && source[index + 3] == ']' && source[index + 2] is 'b' or 'i' or 'u') { tag = source[index + 2]; closing = true; length = 4; return true; } return false; } private static bool TryPlaceholder(string source, int index, out string name, out string? format, out int length) { name = ""; format = null; length = 0; if (index + 1 >= source.Length) return false; if (source[index + 1] == '{') { var end = source.IndexOf('}', index + 2); if (end < 0) throw new InvalidDataException("Platzhalter mit '${' wurde nicht mit '}' geschlossen."); var content = source[(index + 2)..end]; var parts = content.Split('|', 2); name = parts[0].Trim(); format = parts.Length == 2 ? parts[1] : null; if (name.Length == 0) throw new InvalidDataException("Ein eingebetteter Platzhaltername darf nicht leer sein."); length = end - index + 1; return true; } if (!IsNameStart(source[index + 1])) return false; var cursor = index + 2; while (cursor < source.Length && IsNamePart(source[cursor])) cursor++; name = source[(index + 1)..cursor]; length = cursor - index; return true; } private static bool IsNameStart(char value) => char.IsLetter(value) || value == '_'; private static bool IsNamePart(char value) => char.IsLetterOrDigit(value) || value is '_' or '.' or '-'; private static void AddRun(List runs, TemplateRichTextRun run) { if (!run.IsPlaceholder && runs.LastOrDefault() is { IsPlaceholder: false } previous && previous.Bold == run.Bold && previous.Italic == run.Italic && previous.Underline == run.Underline) runs[^1] = previous with { Text = previous.Text + run.Text }; else runs.Add(run); } }