TemplateDesigner: Redesign des TemplateSkripts
This commit is contained in:
@@ -9,6 +9,12 @@ public sealed class LayoutParser
|
||||
{
|
||||
var issues = new List<ValidationIssue>();
|
||||
var elements = new List<TemplateElement>();
|
||||
var pageTemplates = new List<PageTemplateDefinition>();
|
||||
var contentFlows = new List<ContentFlowDefinition>();
|
||||
string? currentPageName = null, currentFlowName = null;
|
||||
List<TemplateElement>? currentPageElements = null, currentFlowElements = null;
|
||||
List<FlowSlotDefinition>? currentSlots = null;
|
||||
var formatVersion = 1;
|
||||
float width = 0, height = 0;
|
||||
var unit = "mm";
|
||||
var pageSeen = false;
|
||||
@@ -18,15 +24,75 @@ public sealed class LayoutParser
|
||||
{
|
||||
var lineNumber = index + 1;
|
||||
var raw = lines[index].Trim();
|
||||
if (raw.Length == 0 || raw.StartsWith('#')) continue;
|
||||
if (raw.Length == 0) continue;
|
||||
try
|
||||
{
|
||||
if (raw.StartsWith("#pragma", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var pragma = Tokenize(raw);
|
||||
Require(pragma, 2);
|
||||
switch (pragma[1].ToLowerInvariant())
|
||||
{
|
||||
case "format-version":
|
||||
Require(pragma, 3);
|
||||
if (!int.TryParse(pragma[2], out formatVersion) || formatVersion is < 1 or > 3)
|
||||
throw new FormatException("format-version muss zwischen 1 und 3 liegen.");
|
||||
break;
|
||||
case "page-template":
|
||||
Require(pragma, 3);
|
||||
if (currentPageName is not null || currentFlowName is not null)
|
||||
throw new FormatException("Verschachtelte Bereiche sind nicht erlaubt.");
|
||||
currentPageName = pragma[2]; currentPageElements = []; currentSlots = [];
|
||||
break;
|
||||
case "end-page-template":
|
||||
if (currentPageName is null || currentPageElements is null || currentSlots is null)
|
||||
throw new FormatException("Kein page-template ist geöffnet.");
|
||||
if (pageTemplates.Any(x => x.Name.Equals(currentPageName, StringComparison.OrdinalIgnoreCase)))
|
||||
throw new FormatException($"page-template „{currentPageName}“ ist mehrfach definiert.");
|
||||
pageTemplates.Add(new(currentPageName, currentPageElements, currentSlots));
|
||||
currentPageName = null; currentPageElements = null; currentSlots = null;
|
||||
break;
|
||||
case "flow-slot":
|
||||
Require(pragma, 3);
|
||||
if (currentPageName is null || currentSlots is null)
|
||||
throw new FormatException("flow-slot muss innerhalb eines page-template stehen.");
|
||||
var slotAttributes = Attributes(pragma, 3);
|
||||
foreach (var required in new[] { "x", "y", "w", "h" })
|
||||
if (!slotAttributes.ContainsKey(required))
|
||||
throw new FormatException($"flow-slot benötigt {required}=…");
|
||||
var slot = new FlowSlotDefinition(lineNumber, pragma[2], Number(slotAttributes["x"]),
|
||||
Number(slotAttributes["y"]), Number(slotAttributes["w"]), Number(slotAttributes["h"]));
|
||||
if (slot.Width <= 0 || slot.Height <= 0) throw new FormatException("Flow-Slot muss eine positive Größe haben.");
|
||||
if (currentSlots.Any(x => x.Name.Equals(slot.Name, StringComparison.OrdinalIgnoreCase)))
|
||||
throw new FormatException($"flow-slot „{slot.Name}“ ist in dieser Seitenvorlage mehrfach definiert.");
|
||||
currentSlots.Add(slot);
|
||||
break;
|
||||
case "content-flow":
|
||||
Require(pragma, 3);
|
||||
if (currentPageName is not null || currentFlowName is not null)
|
||||
throw new FormatException("Verschachtelte Bereiche sind nicht erlaubt.");
|
||||
currentFlowName = pragma[2]; currentFlowElements = [];
|
||||
break;
|
||||
case "end-content-flow":
|
||||
if (currentFlowName is null || currentFlowElements is null)
|
||||
throw new FormatException("Kein content-flow ist geöffnet.");
|
||||
if (contentFlows.Any(x => x.Name.Equals(currentFlowName, StringComparison.OrdinalIgnoreCase)))
|
||||
throw new FormatException($"content-flow „{currentFlowName}“ ist mehrfach definiert.");
|
||||
contentFlows.Add(new(currentFlowName, currentFlowElements));
|
||||
currentFlowName = null; currentFlowElements = null;
|
||||
break;
|
||||
default: throw new FormatException($"Unbekanntes Pragma „{pragma[1]}“.");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (raw.StartsWith('#')) continue;
|
||||
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.");
|
||||
|
||||
TemplateElement? parsedElement = null;
|
||||
switch (keyword)
|
||||
{
|
||||
case "PAGE":
|
||||
@@ -39,40 +105,64 @@ public sealed class LayoutParser
|
||||
pageSeen = true;
|
||||
break;
|
||||
case "BG":
|
||||
Require(tokens, 2); elements.Add(new BackgroundElement(lineNumber, tokens[1])); break;
|
||||
if (currentFlowName is not null) throw new FormatException("BG ist in einem content-flow nicht erlaubt.");
|
||||
Require(tokens, 2); parsedElement = new BackgroundElement(lineNumber, tokens[1]); break;
|
||||
case "IMG":
|
||||
Require(tokens, 6);
|
||||
var imageAttributes = Attributes(tokens, 6);
|
||||
var flowImage = currentFlowName is not null;
|
||||
Require(tokens, flowImage ? 2 : 6);
|
||||
var imageAttributes = Attributes(tokens, flowImage ? 2 : 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));
|
||||
parsedElement = flowImage
|
||||
? new ImageElement(lineNumber, tokens[1], 0, 0,
|
||||
OptionalNumber(imageAttributes, "w"), OptionalNumber(imageAttributes, "h"), imageAttributes)
|
||||
: 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;
|
||||
var flowText = currentFlowName is not null;
|
||||
Require(tokens, flowText ? 2 : 4);
|
||||
var textContent = tokens[flowText ? 1 : 3];
|
||||
var textRef = Reference(textContent);
|
||||
parsedElement = new TextElement(lineNumber, flowText ? 0 : Number(tokens[1]),
|
||||
flowText ? 0 : Number(tokens[2]), textContent, textRef.Name, textRef.Format,
|
||||
Attributes(tokens, flowText ? 2 : 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;
|
||||
var flowBox = currentFlowName is not null;
|
||||
Require(tokens, flowBox ? 2 : 6);
|
||||
var boxContent = tokens[flowBox ? 1 : 5];
|
||||
var boxRef = Reference(boxContent);
|
||||
parsedElement = new TextBoxElement(lineNumber, flowBox ? 0 : Number(tokens[1]),
|
||||
flowBox ? 0 : Number(tokens[2]), flowBox ? 0 : Number(tokens[3]),
|
||||
flowBox ? 0 : Number(tokens[4]), boxContent, boxRef.Name, boxRef.Format,
|
||||
Attributes(tokens, flowBox ? 2 : 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)));
|
||||
var flowTable = currentFlowName is not null;
|
||||
Require(tokens, flowTable ? 2 : 6);
|
||||
parsedElement = new TableElement(lineNumber, flowTable ? 0 : Number(tokens[1]),
|
||||
flowTable ? 0 : Number(tokens[2]), flowTable ? 0 : Number(tokens[3]),
|
||||
flowTable ? 0 : Number(tokens[4]), RequiredReference(tokens[flowTable ? 1 : 5]),
|
||||
Attributes(tokens, flowTable ? 2 : 6));
|
||||
break;
|
||||
case "CHART":
|
||||
Require(tokens, 6);
|
||||
var attributes = Attributes(tokens, 6);
|
||||
var flowChart = currentFlowName is not null;
|
||||
Require(tokens, flowChart ? 2 : 6);
|
||||
var attributes = Attributes(tokens, flowChart ? 2 : 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));
|
||||
parsedElement = new ChartElement(lineNumber, flowChart ? 0 : Number(tokens[1]),
|
||||
flowChart ? 0 : Number(tokens[2]), flowChart ? 0 : Number(tokens[3]),
|
||||
flowChart ? 0 : Number(tokens[4]), RequiredReference(tokens[flowChart ? 1 : 5]), chartType, attributes);
|
||||
break;
|
||||
default: throw new FormatException($"Unbekanntes Element „{tokens[0]}“.");
|
||||
}
|
||||
if (parsedElement is not null)
|
||||
{
|
||||
elements.Add(parsedElement);
|
||||
if (currentPageElements is not null) currentPageElements.Add(parsedElement);
|
||||
else if (currentFlowElements is not null) currentFlowElements.Add(parsedElement);
|
||||
else if (pageTemplates.Count > 0 || contentFlows.Count > 0 || formatVersion >= 3)
|
||||
throw new FormatException("Elemente müssen in page-template oder content-flow stehen.");
|
||||
}
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
@@ -81,8 +171,32 @@ public sealed class LayoutParser
|
||||
}
|
||||
|
||||
if (!pageSeen) issues.Add(new(ValidationSeverity.Error, "PAGE fehlt."));
|
||||
if (currentPageName is not null) issues.Add(new(ValidationSeverity.Error, $"page-template „{currentPageName}“ wurde nicht geschlossen."));
|
||||
if (currentFlowName is not null) issues.Add(new(ValidationSeverity.Error, $"content-flow „{currentFlowName}“ wurde nicht geschlossen."));
|
||||
if (pageTemplates.Count > 0 || contentFlows.Count > 0)
|
||||
{
|
||||
var firstTemplate = pageTemplates.FirstOrDefault(x => x.Name.Equals("first", StringComparison.OrdinalIgnoreCase));
|
||||
var continuationTemplate = pageTemplates.FirstOrDefault(x => x.Name.Equals("continuation", StringComparison.OrdinalIgnoreCase));
|
||||
if (firstTemplate is null)
|
||||
issues.Add(new(ValidationSeverity.Error, "Die Seitenvorlage „first“ fehlt."));
|
||||
foreach (var flow in contentFlows)
|
||||
{
|
||||
if (firstTemplate is not null && !firstTemplate.FlowSlots.Any(slot => slot.Name.Equals(flow.Name, StringComparison.OrdinalIgnoreCase)))
|
||||
issues.Add(new(ValidationSeverity.Error, $"Für content-flow „{flow.Name}“ fehlt der gleichnamige flow-slot im Seitentyp „first“."));
|
||||
if (continuationTemplate is not null && !continuationTemplate.FlowSlots.Any(slot => slot.Name.Equals(flow.Name, StringComparison.OrdinalIgnoreCase)))
|
||||
issues.Add(new(ValidationSeverity.Error, $"Für content-flow „{flow.Name}“ fehlt der gleichnamige flow-slot im Seitentyp „continuation“."));
|
||||
}
|
||||
foreach (var slot in pageTemplates.SelectMany(page => page.FlowSlots))
|
||||
if (slot.X < 0 || slot.Y < 0 || slot.X + slot.Width > width || slot.Y + slot.Height > height)
|
||||
issues.Add(new(ValidationSeverity.Error, $"Flow-Slot „{slot.Name}“ in Zeile {slot.Line} liegt außerhalb der Seite.", slot.Line));
|
||||
}
|
||||
if (issues.Count > 0) throw new TemplateValidationException(new(issues));
|
||||
return new(width, height, unit, elements);
|
||||
return new(width, height, unit, elements)
|
||||
{
|
||||
FormatVersion = formatVersion,
|
||||
PageTemplates = pageTemplates,
|
||||
ContentFlows = contentFlows,
|
||||
};
|
||||
}
|
||||
|
||||
private static (string? Name, string? Format) Reference(string value)
|
||||
@@ -99,6 +213,9 @@ public sealed class LayoutParser
|
||||
private static float Number(string value) => float.TryParse(value, NumberStyles.Float,
|
||||
CultureInfo.InvariantCulture, out var result) ? result : throw new FormatException($"„{value}“ ist keine Zahl.");
|
||||
|
||||
private static float OptionalNumber(IReadOnlyDictionary<string, string> attributes, string name) =>
|
||||
attributes.TryGetValue(name, out var value) ? Number(value) : 0;
|
||||
|
||||
public static float Percentage(string value)
|
||||
{
|
||||
var normalized = value.EndsWith('%') ? value[..^1] : value;
|
||||
|
||||
Reference in New Issue
Block a user