Files
admin c6efdab9ef
CI / build-and-test (push) Canceled after 0s
TemplateDesigner: SystemVars
2026-08-31 23:47:48 +02:00

115 lines
4.8 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 (source[index] == '$' && SystemVariables.TryRead(source, index, out var systemLength))
{
literal.Append(source, index, systemLength);
index += systemLength;
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);
}
}