Files
LehrerApp/LehrerApp.Templating/LayoutParser.cs
T
2026-08-31 22:10:06 +02:00

167 lines
8.2 KiB
C#

using System.Globalization;
using System.Text;
namespace LehrerApp.Templating;
public sealed class LayoutParser
{
public TemplateLayout Parse(string source)
{
var issues = new List<ValidationIssue>();
var elements = new List<TemplateElement>();
float width = 0, height = 0;
var unit = "mm";
var pageSeen = false;
var lines = source.Replace("\r\n", "\n", StringComparison.Ordinal).Split('\n');
for (var index = 0; index < lines.Length; index++)
{
var lineNumber = index + 1;
var raw = lines[index].Trim();
if (raw.Length == 0 || raw.StartsWith('#')) continue;
try
{
var tokens = Tokenize(raw);
if (tokens.Count == 0) continue;
var keyword = tokens[0].ToUpperInvariant();
if (!pageSeen && keyword != "PAGE")
throw new FormatException("PAGE muss das erste Statement sein.");
switch (keyword)
{
case "PAGE":
if (pageSeen) throw new FormatException("PAGE darf nur einmal vorkommen.");
Require(tokens, 4);
width = Number(tokens[1]); height = Number(tokens[2]); unit = tokens[3].ToLowerInvariant();
if (width <= 0 || height <= 0) throw new FormatException("Seitengröße muss positiv sein.");
if (unit is not ("mm" or "cm" or "pt" or "in"))
throw new FormatException("Einheit muss mm, cm, pt oder in sein.");
pageSeen = true;
break;
case "BG":
Require(tokens, 2); elements.Add(new BackgroundElement(lineNumber, tokens[1])); break;
case "IMG":
Require(tokens, 6);
var imageAttributes = Attributes(tokens, 6);
if (imageAttributes.TryGetValue("scale", out var scale)) Percentage(scale);
elements.Add(new ImageElement(lineNumber, tokens[1],
Number(tokens[2]), Number(tokens[3]), Number(tokens[4]), Number(tokens[5]), imageAttributes));
break;
case "TEXT":
Require(tokens, 4);
var textRef = Reference(tokens[3]);
elements.Add(new TextElement(lineNumber, Number(tokens[1]), Number(tokens[2]),
tokens[3], textRef.Name, textRef.Format, Attributes(tokens, 4))); break;
case "TEXTBOX":
Require(tokens, 6);
var boxRef = Reference(tokens[5]);
elements.Add(new TextBoxElement(lineNumber, Number(tokens[1]), Number(tokens[2]),
Number(tokens[3]), Number(tokens[4]), tokens[5], boxRef.Name, boxRef.Format,
Attributes(tokens, 6))); break;
case "FLOWBOX":
Require(tokens, 6);
var flowRef = Reference(tokens[5]);
elements.Add(new FlowBoxElement(lineNumber, Number(tokens[1]), Number(tokens[2]),
Number(tokens[3]), Number(tokens[4]), tokens[5], flowRef.Name, flowRef.Format,
Attributes(tokens, 6))); break;
case "DRAWBOX":
Require(tokens, 6);
elements.Add(new DrawBoxElement(lineNumber, Number(tokens[1]), Number(tokens[2]),
Number(tokens[3]), Number(tokens[4]), RequiredReference(tokens[5]), Attributes(tokens, 6)));
break;
case "FLOWDRAWBOX":
Require(tokens, 6);
elements.Add(new FlowDrawBoxElement(lineNumber, Number(tokens[1]), Number(tokens[2]),
Number(tokens[3]), Number(tokens[4]), RequiredReference(tokens[5]), Attributes(tokens, 6)));
break;
case "TABLE":
Require(tokens, 6);
elements.Add(new TableElement(lineNumber, Number(tokens[1]), Number(tokens[2]),
Number(tokens[3]), Number(tokens[4]), RequiredReference(tokens[5]), Attributes(tokens, 6)));
break;
case "CHART":
Require(tokens, 6);
var attributes = Attributes(tokens, 6);
var chartType = attributes.GetValueOrDefault("type", "bar").ToLowerInvariant();
if (chartType is not ("line" or "bar")) throw new FormatException("CHART type muss line oder bar sein.");
elements.Add(new ChartElement(lineNumber, Number(tokens[1]), Number(tokens[2]),
Number(tokens[3]), Number(tokens[4]), RequiredReference(tokens[5]), chartType, attributes));
break;
default: throw new FormatException($"Unbekanntes Element „{tokens[0]}“.");
}
}
catch (FormatException ex)
{
issues.Add(new(ValidationSeverity.Error, $"Zeile {lineNumber}: {ex.Message}", lineNumber));
}
}
if (!pageSeen) issues.Add(new(ValidationSeverity.Error, "PAGE fehlt."));
if (issues.Count > 0) throw new TemplateValidationException(new(issues));
return new(width, height, unit, elements);
}
private static (string? Name, string? Format) Reference(string value)
{
if (!value.StartsWith('$')) return (null, null);
var parts = value[1..].Split('|', 2);
if (string.IsNullOrWhiteSpace(parts[0])) throw new FormatException("Platzhaltername fehlt.");
return (parts[0], parts.Length == 2 ? parts[1] : null);
}
private static string RequiredReference(string value) => Reference(value).Name
?? throw new FormatException("Das Element erwartet einen $Platzhalter.");
private static float Number(string value) => float.TryParse(value, NumberStyles.Float,
CultureInfo.InvariantCulture, out var result) ? result : throw new FormatException($"„{value}“ ist keine Zahl.");
public static float Percentage(string value)
{
var normalized = value.EndsWith('%') ? value[..^1] : value;
var result = Number(normalized);
if (result <= 0 || result > 1000)
throw new FormatException("scale muss zwischen 0 und 1000 Prozent liegen.");
return result / 100f;
}
private static Dictionary<string, string> Attributes(IReadOnlyList<string> tokens, int start)
{
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
for (var i = start; i < tokens.Count; i++)
{
var separator = tokens[i].IndexOf('=');
if (separator <= 0 || separator == tokens[i].Length - 1)
throw new FormatException($"Ungültiges Attribut „{tokens[i]}“.");
result[tokens[i][..separator]] = tokens[i][(separator + 1)..];
}
return result;
}
private static void Require(IReadOnlyCollection<string> tokens, int minimum)
{
if (tokens.Count < minimum) throw new FormatException("Zu wenige Argumente.");
}
internal static List<string> Tokenize(string line)
{
var tokens = new List<string>();
var current = new StringBuilder();
var quoted = false;
var escaped = false;
foreach (var character in line)
{
if (escaped) { current.Append(character); escaped = false; continue; }
if (character == '\\' && quoted) { escaped = true; continue; }
if (character == '"') { quoted = !quoted; continue; }
if (char.IsWhiteSpace(character) && !quoted)
{
if (current.Length > 0) { tokens.Add(current.ToString()); current.Clear(); }
}
else current.Append(character);
}
if (quoted) throw new FormatException("Nicht abgeschlossenes Anführungszeichen.");
if (current.Length > 0) tokens.Add(current.ToString());
return tokens;
}
}