108 lines
4.5 KiB
C#
108 lines
4.5 KiB
C#
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<TemplateRichTextRun> Parse(string source)
|
|
{
|
|
var runs = new List<TemplateRichTextRun>();
|
|
var literal = new StringBuilder();
|
|
var styles = new Stack<char>();
|
|
|
|
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<string> 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<TemplateRichTextRun> 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);
|
|
}
|
|
}
|