From c6efdab9ef36923d0b8b1dc2c09de4f98b5ee6cf Mon Sep 17 00:00:00 2001 From: Baddi86 Date: Mon, 31 Aug 2026 23:47:48 +0200 Subject: [PATCH] TemplateDesigner: SystemVars --- LehrerApp.TemplateDesigner/MainWindow.axaml | 2 + LehrerApp.Templating.Tests/TemplatingTests.cs | 20 ++++++++ LehrerApp.Templating/LayoutParser.cs | 1 + LehrerApp.Templating/QuestTemplateRenderer.cs | 46 ++++++++++++++++-- LehrerApp.Templating/README.md | 20 ++++++++ LehrerApp.Templating/SystemVariables.cs | 48 +++++++++++++++++++ LehrerApp.Templating/TemplateRichText.cs | 7 +++ 7 files changed, 139 insertions(+), 5 deletions(-) create mode 100644 LehrerApp.Templating/SystemVariables.cs diff --git a/LehrerApp.TemplateDesigner/MainWindow.axaml b/LehrerApp.TemplateDesigner/MainWindow.axaml index df3b324..9acb6ee 100644 --- a/LehrerApp.TemplateDesigner/MainWindow.axaml +++ b/LehrerApp.TemplateDesigner/MainWindow.axaml @@ -197,6 +197,8 @@ TextWrapping="Wrap" Opacity="0.65" FontSize="11"/> + diff --git a/LehrerApp.Templating.Tests/TemplatingTests.cs b/LehrerApp.Templating.Tests/TemplatingTests.cs index 150b3e8..0405340 100644 --- a/LehrerApp.Templating.Tests/TemplatingTests.cs +++ b/LehrerApp.Templating.Tests/TemplatingTests.cs @@ -31,6 +31,26 @@ public sealed class TemplatingTests : IDisposable Assert.Equal("line", Assert.IsType(layout.Elements[6]).ChartType); } + [Fact] + public void Systemvariablen_BrauchenKeineManifestDeklarationUndRendernMitSeitenzahlen() + { + var manifest = new TemplateManifest { Id = "system", Name = "Systemvariablen" }; + var layout = new LayoutParser().Parse(""" + PAGE 210 297 mm + TEXT 20 10 "Stand: $$today" size=9 + TEXT 140 285 "Seite $$curPage von $$maxPageNum" size=9 + FLOWBOX 20 20 170 250 "Langer Inhalt für zwei Seiten.\nLanger Inhalt für zwei Seiten." + """); + var template = new LoadedTemplate(manifest, layout, new Dictionary()); + + var validation = new TemplateLoader().Validate(template, new Dictionary()); + var pdf = new QuestTemplateRenderer().RenderToPdf(template, + new DictionaryProvider(new Dictionary())); + + Assert.True(validation.IsValid); + Assert.Equal("%PDF", System.Text.Encoding.ASCII.GetString(pdf, 0, 4)); + } + [Fact] public void FlowBox_FliesstAufFolgeseitenUndPaketEnthaeltFolgeseitenLayout() { diff --git a/LehrerApp.Templating/LayoutParser.cs b/LehrerApp.Templating/LayoutParser.cs index 4d738dc..9144e49 100644 --- a/LehrerApp.Templating/LayoutParser.cs +++ b/LehrerApp.Templating/LayoutParser.cs @@ -103,6 +103,7 @@ public sealed class LayoutParser private static (string? Name, string? Format) Reference(string value) { + if (SystemVariables.IsStandalone(value)) return (null, null); if (!value.StartsWith('$')) return (null, null); var parts = value[1..].Split('|', 2); if (string.IsNullOrWhiteSpace(parts[0])) throw new FormatException("Platzhaltername fehlt."); diff --git a/LehrerApp.Templating/QuestTemplateRenderer.cs b/LehrerApp.Templating/QuestTemplateRenderer.cs index 74e4bbf..23d7809 100644 --- a/LehrerApp.Templating/QuestTemplateRenderer.cs +++ b/LehrerApp.Templating/QuestTemplateRenderer.cs @@ -177,6 +177,15 @@ public sealed class QuestTemplateRenderer : ITemplateRenderer, ITemplatePreviewR { var aligned = attributes.GetValueOrDefault("align", "left").ToLowerInvariant() switch { "center" => container.AlignCenter(), "right" => container.AlignRight(), _ => container.AlignLeft() }; + if (SystemVariables.Contains(content)) + { + aligned.Text(text => + { + text.DefaultTextStyle(style => ApplyTextStyle(style, attributes)); + AppendSystemVariables(text, content); + }); + return; + } var descriptor = aligned.Text(content); descriptor.FontSize(ParseFloat(attributes, "size", 11)); if (ParseBool(attributes, "bold")) descriptor.SemiBold(); @@ -216,15 +225,42 @@ public sealed class QuestTemplateRenderer : ITemplateRenderer, ITemplatePreviewR { var content = run.Placeholder is null ? run.Text : values.TryGetValue(run.Placeholder, out var value) ? Format(value, run.Format) : ""; - var span = text.Span(content).FontSize(fontSize); - if (globalBold || run.Bold) span.SemiBold(); - if (globalItalic || run.Italic) span.Italic(); - if (globalUnderline || run.Underline) span.Underline(); - if (color is not null) span.FontColor(color); + foreach (var span in AppendSystemVariables(text, content)) + { + span.FontSize(fontSize); + if (globalBold || run.Bold) span.SemiBold(); + if (globalItalic || run.Italic) span.Italic(); + if (globalUnderline || run.Underline) span.Underline(); + if (color is not null) span.FontColor(color); + } } }); } + private static TextStyle ApplyTextStyle(TextStyle style, IReadOnlyDictionary attributes) + { + style = style.FontSize(ParseFloat(attributes, "size", 11)); + if (ParseBool(attributes, "bold")) style = style.SemiBold(); + if (ParseBool(attributes, "italic")) style = style.Italic(); + if (ParseBool(attributes, "underline")) style = style.Underline(); + if (attributes.TryGetValue("color", out var color)) style = style.FontColor(color); + return style; + } + + private static IEnumerable AppendSystemVariables(TextDescriptor text, string content) + { + foreach (var part in SystemVariables.Split(content)) + { + yield return part.Name switch + { + SystemVariables.CurrentPage => text.CurrentPageNumber(), + SystemVariables.MaximumPageNumber => text.TotalPages(), + SystemVariables.Today => text.Span(DateTime.Today.ToString("d", CultureInfo.GetCultureInfo("de-DE"))), + _ => text.Span(part.Text), + }; + } + } + private static IReadOnlyDictionary TextAttributes(TemplateManifest manifest, string? placeholder, IReadOnlyDictionary elementAttributes) { diff --git a/LehrerApp.Templating/README.md b/LehrerApp.Templating/README.md index 33dfc98..0f1614b 100644 --- a/LehrerApp.Templating/README.md +++ b/LehrerApp.Templating/README.md @@ -119,6 +119,26 @@ haben. Haupt- und Folgeseitenlayout müssen dieselbe Seitengröße besitzen; ihr technischen Gründen dieselbe Position und Größe haben. Ohne Folgeseitenlayout werden die statischen Elemente der ersten Seite auf jeder erzeugten Seite wiederholt. +## Systemvariablen + +Textinhalte können drei vom Renderer bereitgestellte Variablen verwenden. Sie werden nicht im +Manifest deklariert und nicht vom `ITemplateDataProvider` geliefert: + +- `$$today` – aktuelles lokales Datum im deutschen Kurzformat +- `$$curPage` – aktuelle Seitenzahl +- `$$maxPageNum` – Gesamtzahl der Seiten + +Sie können allein oder innerhalb eines Literals stehen, beispielsweise: + +```text +TEXT 20 10 "Stand: $$today" size=9 +TEXT 145 285 "Seite $$curPage von $$maxPageNum" size=9 align=right +``` + +QuestPDF löst aktuelle und gesamte Seitenzahl während der Dokumenterzeugung auf. Dafür ist kein +zusätzlicher Renderdurchlauf durch die Anwendung erforderlich. Die Variablen funktionieren auch in +konstanten Rich-Text-Platzhaltern. + ## Konstante Platzhalter und Hervorhebung Ein Platzhalter kann seinen Wert vollständig im Vorlagenpaket tragen. `IsConstant=true` bewirkt, diff --git a/LehrerApp.Templating/SystemVariables.cs b/LehrerApp.Templating/SystemVariables.cs new file mode 100644 index 0000000..8cf2648 --- /dev/null +++ b/LehrerApp.Templating/SystemVariables.cs @@ -0,0 +1,48 @@ +namespace LehrerApp.Templating; + +internal static class SystemVariables +{ + internal const string CurrentPage = "curPage"; + internal const string MaximumPageNumber = "maxPageNum"; + internal const string Today = "today"; + + private static readonly string[] Names = [CurrentPage, MaximumPageNumber, Today]; + + internal static bool IsStandalone(string value) => + TryRead(value, 0, out var length) && length == value.Length; + + internal static bool Contains(string value) => Names.Any(name => + value.Contains("$$" + name, StringComparison.Ordinal)); + + internal static bool TryRead(string source, int index, out int length) + { + length = 0; + if (index < 0 || index + 2 > source.Length || source[index] != '$' || source[index + 1] != '$') + return false; + foreach (var name in Names) + { + var token = "$$" + name; + if (!source.AsSpan(index).StartsWith(token, StringComparison.Ordinal)) continue; + var end = index + token.Length; + if (end < source.Length && (char.IsLetterOrDigit(source[end]) || source[end] is '_' or '-' or '.')) + continue; + length = token.Length; + return true; + } + return false; + } + + internal static IEnumerable<(string Text, string? Name)> Split(string source) + { + var literalStart = 0; + for (var index = 0; index < source.Length;) + { + if (!TryRead(source, index, out var length)) { index++; continue; } + if (index > literalStart) yield return (source[literalStart..index], null); + yield return (source.Substring(index, length), source.Substring(index + 2, length - 2)); + index += length; + literalStart = index; + } + if (literalStart < source.Length) yield return (source[literalStart..], null); + } +} diff --git a/LehrerApp.Templating/TemplateRichText.cs b/LehrerApp.Templating/TemplateRichText.cs index cd9ad14..cfc5789 100644 --- a/LehrerApp.Templating/TemplateRichText.cs +++ b/LehrerApp.Templating/TemplateRichText.cs @@ -30,6 +30,13 @@ public static class TemplateRichText && 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();