This commit is contained in:
@@ -197,6 +197,8 @@
|
|||||||
TextWrapping="Wrap" Opacity="0.65" FontSize="11"/>
|
TextWrapping="Wrap" Opacity="0.65" FontSize="11"/>
|
||||||
<TextBlock Text="FLOWBOX verteilt Text automatisch über beliebig viele Seiten. TEXTBOX bleibt ein fester Bereich."
|
<TextBlock Text="FLOWBOX verteilt Text automatisch über beliebig viele Seiten. TEXTBOX bleibt ein fester Bereich."
|
||||||
TextWrapping="Wrap" Foreground="#166534" FontSize="12"/>
|
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."
|
<TextBlock Text="DRAWBOX und FLOWDRAWBOX nehmen deklarative Zeichenbefehle (Text, Linien, Rechtecke, Bilder) aus einer externen App entgegen."
|
||||||
TextWrapping="Wrap" Foreground="#166534" FontSize="12"/>
|
TextWrapping="Wrap" Foreground="#166534" FontSize="12"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|||||||
@@ -31,6 +31,26 @@ public sealed class TemplatingTests : IDisposable
|
|||||||
Assert.Equal("line", Assert.IsType<ChartElement>(layout.Elements[6]).ChartType);
|
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]
|
[Fact]
|
||||||
public void FlowBox_FliesstAufFolgeseitenUndPaketEnthaeltFolgeseitenLayout()
|
public void FlowBox_FliesstAufFolgeseitenUndPaketEnthaeltFolgeseitenLayout()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ public sealed class LayoutParser
|
|||||||
|
|
||||||
private static (string? Name, string? Format) Reference(string value)
|
private static (string? Name, string? Format) Reference(string value)
|
||||||
{
|
{
|
||||||
|
if (SystemVariables.IsStandalone(value)) return (null, null);
|
||||||
if (!value.StartsWith('$')) return (null, null);
|
if (!value.StartsWith('$')) return (null, null);
|
||||||
var parts = value[1..].Split('|', 2);
|
var parts = value[1..].Split('|', 2);
|
||||||
if (string.IsNullOrWhiteSpace(parts[0])) throw new FormatException("Platzhaltername fehlt.");
|
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
|
var aligned = attributes.GetValueOrDefault("align", "left").ToLowerInvariant() switch
|
||||||
{ "center" => container.AlignCenter(), "right" => container.AlignRight(), _ => container.AlignLeft() };
|
{ "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);
|
var descriptor = aligned.Text(content);
|
||||||
descriptor.FontSize(ParseFloat(attributes, "size", 11));
|
descriptor.FontSize(ParseFloat(attributes, "size", 11));
|
||||||
if (ParseBool(attributes, "bold")) descriptor.SemiBold();
|
if (ParseBool(attributes, "bold")) descriptor.SemiBold();
|
||||||
@@ -216,15 +225,42 @@ public sealed class QuestTemplateRenderer : ITemplateRenderer, ITemplatePreviewR
|
|||||||
{
|
{
|
||||||
var content = run.Placeholder is null ? run.Text
|
var content = run.Placeholder is null ? run.Text
|
||||||
: values.TryGetValue(run.Placeholder, out var value) ? Format(value, run.Format) : "";
|
: 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))
|
||||||
if (globalBold || run.Bold) span.SemiBold();
|
{
|
||||||
if (globalItalic || run.Italic) span.Italic();
|
span.FontSize(fontSize);
|
||||||
if (globalUnderline || run.Underline) span.Underline();
|
if (globalBold || run.Bold) span.SemiBold();
|
||||||
if (color is not null) span.FontColor(color);
|
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,
|
private static IReadOnlyDictionary<string, string> TextAttributes(TemplateManifest manifest, string? placeholder,
|
||||||
IReadOnlyDictionary<string, string> elementAttributes)
|
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
|
technischen Gründen dieselbe Position und Größe haben. Ohne Folgeseitenlayout werden die statischen
|
||||||
Elemente der ersten Seite auf jeder erzeugten Seite wiederholt.
|
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
|
## Konstante Platzhalter und Hervorhebung
|
||||||
|
|
||||||
Ein Platzhalter kann seinen Wert vollständig im Vorlagenpaket tragen. `IsConstant=true` bewirkt,
|
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 '$')
|
&& source[index + 1] is '\\' or '[' or '$')
|
||||||
{ literal.Append(source[index + 1]); index += 2; continue; }
|
{ 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))
|
if (TryTag(source, index, out var tag, out var closing, out var tagLength))
|
||||||
{
|
{
|
||||||
Flush();
|
Flush();
|
||||||
|
|||||||
Reference in New Issue
Block a user