This commit is contained in:
@@ -197,6 +197,8 @@
|
||||
TextWrapping="Wrap" Opacity="0.65" FontSize="11"/>
|
||||
<TextBlock Text="FLOWBOX verteilt Text automatisch über beliebig viele Seiten. TEXTBOX bleibt ein fester Bereich."
|
||||
TextWrapping="Wrap" Foreground="#166534" FontSize="12"/>
|
||||
<TextBlock Text="Systemvariablen ohne Deklaration: $$today, $$curPage und $$maxPageNum (z. B. "Seite $$curPage von $$maxPageNum")."
|
||||
TextWrapping="Wrap" Foreground="#166534" FontSize="12"/>
|
||||
<TextBlock Text="DRAWBOX und FLOWDRAWBOX nehmen deklarative Zeichenbefehle (Text, Linien, Rechtecke, Bilder) aus einer externen App entgegen."
|
||||
TextWrapping="Wrap" Foreground="#166534" FontSize="12"/>
|
||||
</StackPanel>
|
||||
|
||||
@@ -31,6 +31,26 @@ public sealed class TemplatingTests : IDisposable
|
||||
Assert.Equal("line", Assert.IsType<ChartElement>(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<string, byte[]>());
|
||||
|
||||
var validation = new TemplateLoader().Validate(template, new Dictionary<string, PlaceholderType>());
|
||||
var pdf = new QuestTemplateRenderer().RenderToPdf(template,
|
||||
new DictionaryProvider(new Dictionary<string, PlaceholderValue>()));
|
||||
|
||||
Assert.True(validation.IsValid);
|
||||
Assert.Equal("%PDF", System.Text.Encoding.ASCII.GetString(pdf, 0, 4));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FlowBox_FliesstAufFolgeseitenUndPaketEnthaeltFolgeseitenLayout()
|
||||
{
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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);
|
||||
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<string, string> 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<TextSpanDescriptor> 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<string, string> TextAttributes(TemplateManifest manifest, string? placeholder,
|
||||
IReadOnlyDictionary<string, string> elementAttributes)
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user