`) | `FlexElement { Padding, Background }` | +| Horizontal rule (`---` or `
`) | `SeparatorElement` | +| Image (`` or ``) | `ImageElement` | +| Code (`` `code` `` or `
`) | `TextElement { Background = "#f0f0f0" }` | + +### Example: Markdown Content + +```yaml +template: + name: "receipt" + +canvas: + fixed: width + width: 400 + background: "#ffffff" + +layout: + - type: text + content: "Order Receipt" + fontWeight: bold + size: "1.5em" + padding: "16" + + - type: content + source: "{{orderDetails}}" + format: markdown + padding: "12 16" +``` + +Data: +```json +{ + "orderDetails": "## Items\n\n- Widget A — $9.99\n- **Gadget B** — $24.99\n\n> Total: **$34.98**" +} +``` + +### Example: HTML Content with Inline Styles + +```yaml +- type: content + source: "{{productInfo}}" + format: html + padding: "8" +``` + +Data: +```json +{ + "productInfo": "Price: $29.99
" +} +``` + +### Registration + +```csharp +var render = new FlexRenderBuilder() + .WithMarkdown() // FlexRender.Content.Markdown + .WithHtml() // FlexRender.Content.Html + .WithSkia() + .Build(); +``` + +### Template Caching + +The `content` element is expanded at render time (like `each` and `if`), so parsed templates can be safely cached and rendered with different data. + +--- + ## each (Control Flow) Iterates over an array in the template data, rendering the `children` template once for each array item. This is the primary mechanism for dynamic, data-driven lists. diff --git a/docs/wiki/Getting-Started.md b/docs/wiki/Getting-Started.md index f49040e..9ea3d73 100644 --- a/docs/wiki/Getting-Started.md +++ b/docs/wiki/Getting-Started.md @@ -46,6 +46,8 @@ Install only what you need: | `FlexRender.SvgElement.Skia.Render` | SVG elements for Skia | Svg.Skia | | `FlexRender.SvgElement.Svg.Render` | SVG elements for SVG output | None | | `FlexRender.SvgElement` | SvgElement meta-package (all renderers) | Svg.Skia | +| `FlexRender.Content.Markdown` | Markdown content parsing for `type: content` | Markdig | +| `FlexRender.Content.Html` | HTML content parsing for `type: content` | HtmlAgilityPack | | `FlexRender.HarfBuzz` | HarfBuzz text shaping for Arabic/Hebrew | SkiaSharp.HarfBuzz | | `FlexRender.Http` | HTTP/HTTPS resource loading | None | | `FlexRender.DependencyInjection` | Microsoft DI integration | Microsoft.Extensions.DI | @@ -192,6 +194,8 @@ Native rendering via SkiaSharp. Best quality, widest feature set. ```csharp var render = new FlexRenderBuilder() + .WithMarkdown() // Markdown content parsing + .WithHtml() // HTML content parsing .WithSkia(skia => skia .WithQr() // QR code support .WithBarcode() // Barcode support @@ -201,7 +205,7 @@ var render = new FlexRenderBuilder() - **Formats:** PNG, JPEG, BMP, Raw - **Requires:** `SkiaSharp.NativeAssets.Linux` on Linux/Docker -- **Optional:** `.WithHarfBuzz()` for Arabic/Hebrew text shaping +- **Optional:** `.WithHarfBuzz()` for Arabic/Hebrew text shaping, `.WithMarkdown()` / `.WithHtml()` for content parsing - **Best for:** Desktop apps, servers with native library support ### ImageSharp Backend diff --git a/docs/wiki/Template-Syntax.md b/docs/wiki/Template-Syntax.md index d04db1d..de69465 100644 --- a/docs/wiki/Template-Syntax.md +++ b/docs/wiki/Template-Syntax.md @@ -1,6 +1,6 @@ # Template Syntax -FlexRender templates are YAML files that define image layouts using a tree of typed elements. This page covers the template structure, all 10 element types, common properties, and supported units. +FlexRender templates are YAML files that define image layouts using a tree of typed elements. This page covers the template structure, all 11 element types, common properties, and supported units. For template expressions (variables, loops, conditionals), see [[Template-Expressions]]. For flexbox layout properties, see [[Flexbox-Layout]]. @@ -15,9 +15,9 @@ template: # Required: metadata version: 1 # Template version (int) culture: "ru-RU" # Culture for number/date formatting (optional) -fonts: # Optional: font definitions - default: "assets/fonts/Inter-Regular.ttf" - bold: "assets/fonts/Inter-Bold.ttf" +fonts: # Optional: font definitions (list or dictionary) + - "assets/fonts/Inter-Regular.ttf" # First unnamed = "default"/"main" + - "assets/fonts/Inter-Bold.ttf" canvas: # Required: canvas configuration fixed: width # Which dimension is fixed @@ -70,16 +70,165 @@ Rotation is applied **after** rendering. For thermal printers: use `"right"` to ## Fonts -Fonts are defined as key-value pairs. The key is a reference name used in `font:` properties, and the value is the font source: +FlexRender supports two formats for font registration: **dictionary format** (legacy) and **list format** (recommended). Supported file types: `.ttf` and `.otf`. Font sources can be local file paths, `embedded://` resources, or `http://` URLs. + +### Dictionary Format (Legacy) + +Key-value pairs where the key is a reference name used in `font:` properties: + +```yaml +fonts: + default: "assets/fonts/Inter-Regular.ttf" + heading: "assets/fonts/Roboto-Regular.ttf" + icon: "embedded://MyApp.Fonts.icons.ttf" + remote: "https://example.com/font.ttf" +``` + +### List Format (Recommended) + +An array of font entries. Simple strings and objects with `path`/`name`/`fallback` can be mixed freely: + +```yaml +fonts: + # Simple strings -- first unnamed font automatically becomes "default" (and "main") + - "assets/fonts/Inter-Regular.ttf" + - "assets/fonts/Inter-Bold.ttf" + - "assets/fonts/Inter-Italic.ttf" + + # With optional name and fallback + - path: "assets/fonts/Roboto-Regular.ttf" + name: heading + fallback: "Arial" +``` + +**Rules:** +- The first unnamed font automatically becomes `default` (and `main`) +- Fonts can be mixed: simple strings and objects with `path`/`name`/`fallback` +- Named fonts are referenced via `font:` on elements (e.g., `font: heading`) + +### Font Properties on Text Elements + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `font` | string | `main` | Reference to a registered font name | +| `fontFamily` | string | (empty) | CSS-like font family name -- searches registered fonts by FamilyName, then system fonts | +| `fontWeight` | string/number | `normal` | Font weight: `thin`(100), `extra-light`(200), `light`(300), `normal`(400), `medium`(500), `semi-bold`(600), `bold`(700), `extra-bold`(800), `black`(900), or numeric | +| `fontStyle` | string | `normal` | Font style: `normal`, `italic`, `oblique` | + +### Font Resolution Priority + +``` +font (registered name) > fontFamily (family name) > fallback (default) +``` + +- If `font` is set to a non-default value, resolve by registered name +- If `fontFamily` is set, search registered fonts by FamilyName metadata, then system fonts +- Otherwise, use the default font (`main`) + +For weight/style variants: **automatic sibling discovery** scans the same directory as the base font file for `.ttf`/`.otf` files with matching family name and weight/style. + +### Automatic Sibling Font Discovery + +You only need to register the regular/default font for each family. When `fontWeight` or `fontStyle` is used on a text element, FlexRender automatically scans the same directory for font files with a matching family name and weight/style (within +/-100 units for weight, case-insensitive). + +**Convention:** Place all weight/style variants of a font family in the same directory: + +``` +assets/fonts/ + Inter-Regular.ttf # weight 400, upright + Inter-Bold.ttf # weight 700, upright + Inter-SemiBold.ttf # weight 600, upright + Inter-Italic.ttf # weight 400, italic + Inter-BoldItalic.ttf # weight 700, italic +``` + +Then `fontWeight: bold` on a text element will automatically use `Inter-Bold.ttf` without any extra font registration. + +### Table Header Font Properties + +Tables support font properties on the header row: + +| Property | Aliases | Description | +|----------|---------|-------------| +| `headerFont` | `header-font` | Font name for header cells | +| `headerFontWeight` | `header-fontWeight` | Font weight for headers | +| `headerFontStyle` | `header-fontStyle` | Font style for headers (normal, italic, oblique) | +| `headerFontFamily` | `header-fontFamily` | CSS-like font family for headers | + +### fontWeight Values + +| Value | Numeric | +|-------|---------| +| `thin` | 100 | +| `extra-light` | 200 | +| `light` | 300 | +| `normal` (default) | 400 | +| `medium` | 500 | +| `semi-bold` | 600 | +| `bold` | 700 | +| `extra-bold` | 800 | +| `black` | 900 | + +Numeric values (100-900) are also accepted directly: `fontWeight: 600`. + +### fontStyle Values + +`normal` (default), `italic`, `oblique`. + +### Examples + +**Minimal (system fonts only, no registration):** + +```yaml +layout: + - type: text + content: "Hello" + fontFamily: "Arial" + fontWeight: bold +``` + +**List registration with fontWeight/fontStyle:** + +```yaml +fonts: + - "assets/fonts/Inter-Regular.ttf" + - "assets/fonts/Inter-Bold.ttf" + - "assets/fonts/Inter-Italic.ttf" + +layout: + - type: text + content: "Bold text" + fontWeight: bold + - type: text + content: "Italic text" + fontStyle: italic +``` + +**Mixed: named + unnamed, font + fontFamily:** ```yaml fonts: - default: "assets/fonts/Inter-Regular.ttf" # Local file - bold: "assets/fonts/Inter-Bold.ttf" # Local file - icon: "embedded://MyApp.Fonts.icons.ttf" # Embedded resource - remote: "https://example.com/font.ttf" # HTTP URL + - "assets/fonts/Inter-Regular.ttf" + - path: "assets/fonts/NotoSansArabic-Regular.ttf" + name: arabic + +layout: + - type: text + content: "Inter bold" + fontWeight: bold + - type: text + content: "System Georgia" + fontFamily: "Georgia" + - type: text + content: "Arabic" + font: arabic ``` +### Limitations + +- **Variable fonts are NOT supported** -- SkiaSharp 3.x does not expose an API for font variation axes. Use separate static font files per weight/style instead. +- Sibling discovery relies on font file metadata (family name, weight, slant). If files use non-standard naming, register them explicitly. + ## Color Format Colors are specified in hex format: @@ -112,6 +261,7 @@ Renders text content with font, size, color, alignment, and wrapping options. |----------|------|---------|-------------| | `content` | string | `""` | Text content, may contain `{{variable}}` expressions | | `font` | string | `"main"` | Font reference name from `fonts` section | +| `fontFamily` | string | `""` | CSS-like font family name -- searches registered fonts by FamilyName, then system fonts | | `size` | string | `"1em"` | Font size (px, em, %) | | `color` | string | `"#000000"` | Text color in hex | | `align` | TextAlign | `left` | Text alignment: `left`, `center`, `right`, `start` (logical), `end` (logical) | @@ -119,6 +269,8 @@ Renders text content with font, size, color, alignment, and wrapping options. | `overflow` | TextOverflow | `ellipsis` | Overflow handling: `ellipsis`, `clip`, `visible` | | `maxLines` | int? | `null` | Maximum number of lines (null = unlimited) | | `lineHeight` | string | `""` | Line height for multi-line text | +| `fontWeight` | FontWeight | `normal` | Font weight: `thin`, `extra-light`, `light`, `normal`, `medium`, `semi-bold`, `bold`, `extra-bold`, `black`, or numeric 100-900 | +| `fontStyle` | FontStyle | `normal` | Font style: `normal`, `italic`, `oblique` | **lineHeight values:** @@ -321,6 +473,9 @@ Renders tabular data with configurable columns, optional headers, and support fo | `columns` | column[] | required | Column definitions | | `rows` | row[] | `[]` | Static rows (alternative to `array`) | | `headerFont` | string? | `null` | Font for header row | +| `headerFontWeight` | string? | `null` | Font weight for header row | +| `headerFontStyle` | string? | `null` | Font style for header row (normal, italic, oblique) | +| `headerFontFamily` | string? | `null` | CSS-like font family for header row | | `headerColor` | string? | `null` | Text color for header row | | `headerSize` | string? | `null` | Font size for header row | | `headerBackground` | string? | `null` | Background color for header row | @@ -350,6 +505,25 @@ Renders tabular data with configurable columns, optional headers, and support fo --- +### content + +Embeds dynamically formatted text (Markdown, HTML, etc.) from template data using pluggable content parsers. Like `each` and `if`, this is a control-flow element expanded at render time. + +```yaml +- type: content + source: "{{body}}" + format: markdown +``` + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `source` | string | `""` | The formatted text to parse. Usually bound to a `{{variable}}`. | +| `format` | string | `""` | Content format: `markdown`, `html`, or any registered parser name. | + +See [[Element-Reference#content-element-control-flow]] for full details, element mapping, and examples. + +--- + ### each Iterates over an array in the data, creating child elements for each item. See [[Template-Expressions]] for details. diff --git a/examples/ImageSharpRenderExample/output.png b/examples/ImageSharpRenderExample/output.png index 5050eb4..7ed704a 100644 --- a/examples/ImageSharpRenderExample/output.png +++ b/examples/ImageSharpRenderExample/output.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:372d689043cf15ebd64138e6f0c864eb154b0df72c94870f39c9468024ba67f7 -size 12058 +oid sha256:2e652c27e505300a8f7fa6a8b6d1b21e606e26f40bcaa34f78b01420ff71318c +size 13056 diff --git a/examples/ImageSharpRenderExample/template.yaml b/examples/ImageSharpRenderExample/template.yaml index 60652c0..0f32d77 100644 --- a/examples/ImageSharpRenderExample/template.yaml +++ b/examples/ImageSharpRenderExample/template.yaml @@ -30,7 +30,7 @@ layout: size: 1.5em color: "#212529" align: center - bold: true + fontWeight: bold # Separator - type: separator @@ -61,7 +61,7 @@ layout: content: "{{price}}" size: 1.3em color: "#198754" - bold: true + fontWeight: bold # QR code - type: qr diff --git a/examples/SkiaRenderExample/output.png b/examples/SkiaRenderExample/output.png index a422c05..fdc5e89 100644 --- a/examples/SkiaRenderExample/output.png +++ b/examples/SkiaRenderExample/output.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dda33b435fa71096fda24d155116daea9fd025f1c229b58d9c0fa20d86c67b24 -size 16921 +oid sha256:ae49946e3a2bb132ada258c3720e07f7320cde938f8be8a79ab5f0780b2ed83d +size 16848 diff --git a/examples/SkiaRenderExample/template.yaml b/examples/SkiaRenderExample/template.yaml index 19f71ba..4dcfafb 100644 --- a/examples/SkiaRenderExample/template.yaml +++ b/examples/SkiaRenderExample/template.yaml @@ -19,7 +19,7 @@ layout: size: 1.5em align: center color: "#1a1a1a" - bold: true + fontWeight: bold # Separator - type: separator @@ -35,12 +35,12 @@ layout: content: "Item" size: 0.85em color: "#666666" - bold: true + fontWeight: bold - type: text content: "Price" size: 0.85em color: "#666666" - bold: true + fontWeight: bold # Item rows - type: each @@ -75,12 +75,12 @@ layout: content: "Total" size: 1.1em color: "#000000" - bold: true + fontWeight: bold - type: text content: "{{total}}" size: 1.1em color: "#000000" - bold: true + fontWeight: bold # Spacer - type: flex diff --git a/examples/SvgRenderExample/output.svg b/examples/SvgRenderExample/output.svg index a7507bb..7152ee8 100644 --- a/examples/SvgRenderExample/output.svg +++ b/examples/SvgRenderExample/output.svg @@ -1 +1 @@ - \ No newline at end of file +\ No newline at end of file diff --git a/examples/SvgRenderExample/template.yaml b/examples/SvgRenderExample/template.yaml index a28b099..9f89e86 100644 --- a/examples/SvgRenderExample/template.yaml +++ b/examples/SvgRenderExample/template.yaml @@ -24,7 +24,7 @@ layout: content: "{{name}}" size: 1.6em color: "#1a1a2e" - bold: true + fontWeight: bold - type: text content: "{{title}}" diff --git a/examples/assets/fonts/Inter-BoldItalic.ttf b/examples/assets/fonts/Inter-BoldItalic.ttf new file mode 100755 index 0000000..a41fd55 --- /dev/null +++ b/examples/assets/fonts/Inter-BoldItalic.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:faef7d798934e94c9d6b0871c54900b270e3d1930c0b096ab690d974dfef806c +size 348184 diff --git a/examples/assets/fonts/Inter-Italic.ttf b/examples/assets/fonts/Inter-Italic.ttf new file mode 100755 index 0000000..c7906ef --- /dev/null +++ b/examples/assets/fonts/Inter-Italic.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1fe26d47c840ef8767635967acaa50ee43a57794fa00a03bf042ee4aab2792c4 +size 346480 diff --git a/examples/expressions-demo.yaml b/examples/expressions-demo.yaml index b1b680d..e60a790 100644 --- a/examples/expressions-demo.yaml +++ b/examples/expressions-demo.yaml @@ -8,8 +8,6 @@ template: fonts: default: "assets/fonts/Inter-Regular.ttf" - bold: "assets/fonts/Inter-Bold.ttf" - semibold: "assets/fonts/Inter-SemiBold.ttf" canvas: fixed: width @@ -25,7 +23,7 @@ layout: # Title - type: text content: "Inline Expressions" - font: bold + fontWeight: bold size: 1.3em color: "#1a1a1a" align: center @@ -45,7 +43,7 @@ layout: children: - type: text content: "Arithmetic Expressions" - font: semibold + fontWeight: semi-bold size: 0.95em color: "#495057" @@ -61,7 +59,7 @@ layout: content: "{{price + tax}}" size: 0.85em color: "#212529" - font: semibold + fontWeight: semi-bold - type: flex direction: row @@ -75,7 +73,7 @@ layout: content: "{{price * quantity}}" size: 0.85em color: "#212529" - font: semibold + fontWeight: semi-bold - type: flex direction: row @@ -89,7 +87,7 @@ layout: content: "{{price * quantity * 0.1}}" size: 0.85em color: "#e74c3c" - font: semibold + fontWeight: semi-bold - type: flex direction: row @@ -103,7 +101,7 @@ layout: content: "{{total / quantity}}" size: 0.85em color: "#212529" - font: semibold + fontWeight: semi-bold - type: flex direction: row @@ -117,7 +115,7 @@ layout: content: "{{-price}}" size: 0.85em color: "#e74c3c" - font: semibold + fontWeight: semi-bold # Section: Null Coalesce - type: flex @@ -129,7 +127,7 @@ layout: children: - type: text content: "Null Coalesce (??)" - font: semibold + fontWeight: semi-bold size: 0.95em color: "#495057" @@ -174,7 +172,7 @@ layout: children: - type: text content: "Expressions in Conditions" - font: semibold + fontWeight: semi-bold size: 0.95em color: "#495057" @@ -221,12 +219,12 @@ layout: content: "Final total:" size: 1em color: "#495057" - font: semibold + fontWeight: semi-bold - type: text content: "${{price * quantity - discount + tax}}" size: 1em color: "#212529" - font: bold + fontWeight: bold # Footer - type: text diff --git a/examples/font-showcase.yaml b/examples/font-showcase.yaml new file mode 100644 index 0000000..bbfb075 --- /dev/null +++ b/examples/font-showcase.yaml @@ -0,0 +1,140 @@ +# Font Showcase +# Demonstrates: fontWeight, fontStyle, fontFamily, list-based font registration + +template: + name: "font-showcase" + version: 1 + +fonts: + - "assets/fonts/Inter-Regular.ttf" + - "assets/fonts/Inter-Bold.ttf" + - "assets/fonts/Inter-SemiBold.ttf" + - "assets/fonts/Inter-Italic.ttf" + - "assets/fonts/Inter-BoldItalic.ttf" + +canvas: + fixed: width + width: 400 + background: "#ffffff" + +layout: + - type: flex + padding: "24 20" + gap: 16 + children: + # Header + - type: text + content: "Font Showcase" + fontWeight: bold + size: 1.8em + color: "#1a1a1a" + + - type: separator + color: "#e0e0e0" + + # fontWeight section + - type: text + content: "fontWeight" + fontWeight: bold + size: 0.85em + color: "#888888" + + - type: flex + gap: 4 + children: + - type: text + content: "Normal (400)" + size: 1.1em + color: "#333333" + + - type: text + content: "Semi-bold (600)" + fontWeight: semi-bold + size: 1.1em + color: "#333333" + + - type: text + content: "Bold (700)" + fontWeight: bold + size: 1.1em + color: "#333333" + + - type: separator + color: "#e0e0e0" + + # fontStyle section + - type: text + content: "fontStyle" + fontWeight: bold + size: 0.85em + color: "#888888" + + - type: flex + gap: 4 + children: + - type: text + content: "Normal style" + size: 1.1em + color: "#333333" + + - type: text + content: "Italic style" + fontStyle: italic + size: 1.1em + color: "#333333" + + - type: text + content: "Bold + Italic" + fontWeight: bold + fontStyle: italic + size: 1.1em + color: "#333333" + + - type: separator + color: "#e0e0e0" + + # fontFamily section + - type: text + content: "fontFamily (system fonts)" + fontWeight: bold + size: 0.85em + color: "#888888" + + - type: flex + gap: 4 + children: + - type: text + content: "Arial — the classic" + fontFamily: "Arial" + size: 1.1em + color: "#333333" + + - type: text + content: "Georgia — a serif font" + fontFamily: "Georgia" + size: 1.1em + color: "#333333" + + - type: text + content: "Courier New — monospace" + fontFamily: "Courier New" + size: 1.1em + color: "#333333" + + - type: text + content: "Arial Bold Italic" + fontFamily: "Arial" + fontWeight: bold + fontStyle: italic + size: 1.1em + color: "#333333" + + - type: separator + color: "#e0e0e0" + + # Footer + - type: text + content: "Inter loaded from files, system fonts via fontFamily" + size: 0.75em + color: "#999999" + align: center diff --git a/examples/html-content-data.json b/examples/html-content-data.json new file mode 100644 index 0000000..41464f1 --- /dev/null +++ b/examples/html-content-data.json @@ -0,0 +1,3 @@ +{ + "body": " Jane Doe Senior Developer jane@example.com +1 (555) 123-4567 example.com Product Details
Introducing the FlexWidget Pro — our most advanced widget yet.
Key features:
- Lightweight — only 50g
- Durable — aircraft-grade aluminum
- Smart — built-in Bluetooth 5.0
\"The best widget I've ever used.\" — Tech ReviewPrice: $29.99
Free shipping on orders over $50.
" +} diff --git a/examples/html-content.yaml b/examples/html-content.yaml new file mode 100644 index 0000000..6e9fe41 --- /dev/null +++ b/examples/html-content.yaml @@ -0,0 +1,39 @@ +template: + name: "html-content-demo" + version: 1 + +canvas: + fixed: width + width: 400 + background: "#ffffff" + +layout: + # Static header + - type: text + content: "HTML Content Demo" + size: "1.5em" + fontWeight: bold + color: "#333333" + padding: "16 16 8 16" + + - type: separator + color: "#e0e0e0" + thickness: 1 + + # Dynamic HTML content from data + - type: content + source: "{{body}}" + format: html + padding: "12 16" + + - type: separator + color: "#e0e0e0" + thickness: 1 + + # Static footer + - type: text + content: "Generated by FlexRender" + size: "0.8em" + color: "#999999" + align: center + padding: "8 16 16 16" diff --git a/examples/label.yaml b/examples/label.yaml index 5217a34..aa09a4b 100644 --- a/examples/label.yaml +++ b/examples/label.yaml @@ -13,7 +13,7 @@ canvas: layout: - type: text content: "{{productName}}" - font: bold + fontWeight: bold size: 1.1em align: center maxLines: 2 @@ -28,7 +28,7 @@ layout: - type: text content: "{{price}} $" - font: bold + fontWeight: bold size: 1.3em color: "#cc0000" align: center diff --git a/examples/markdown-content-data.json b/examples/markdown-content-data.json new file mode 100644 index 0000000..97a354f --- /dev/null +++ b/examples/markdown-content-data.json @@ -0,0 +1,3 @@ +{ + "body": "## Order Summary\n\nThank you for your purchase!\n\n**Order #12345** was placed on *March 5, 2026*.\n\n### Items\n\n- Widget A — $9.99\n- Widget B — $14.50\n- **Gadget C** — $24.99\n\n### Payment\n\n> Payment received via credit card ending in **4242**.\n\nTotal: **$49.48**\n\n---\n\nFor questions, visit `support.example.com`." +} diff --git a/examples/markdown-content.yaml b/examples/markdown-content.yaml new file mode 100644 index 0000000..15ad1e3 --- /dev/null +++ b/examples/markdown-content.yaml @@ -0,0 +1,39 @@ +template: + name: "markdown-content-demo" + version: 1 + +canvas: + fixed: width + width: 400 + background: "#ffffff" + +layout: + # Static header + - type: text + content: "Markdown Content Demo" + size: "1.5em" + fontWeight: bold + color: "#333333" + padding: "16 16 8 16" + + - type: separator + color: "#e0e0e0" + thickness: 1 + + # Dynamic markdown content from data + - type: content + source: "{{body}}" + format: markdown + padding: "12 16" + + - type: separator + color: "#e0e0e0" + thickness: 1 + + # Static footer + - type: text + content: "Generated by FlexRender" + size: "0.8em" + color: "#999999" + align: center + padding: "8 16 16 16" diff --git a/examples/output/font-showcase.png b/examples/output/font-showcase.png new file mode 100644 index 0000000..14f97f9 --- /dev/null +++ b/examples/output/font-showcase.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:886c1b4a53f75db4f1479c6898acda8701caa12a9434ba303c834337e43f55c0 +size 33124 diff --git a/examples/output/html-content.png b/examples/output/html-content.png new file mode 100644 index 0000000..0d67d30 --- /dev/null +++ b/examples/output/html-content.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c10f40ad34351de31bbaed2d362edc19993fe68f4d8edda341a246da4e12c448 +size 30132 diff --git a/examples/output/label.png b/examples/output/label.png index 8d154f6..abdc959 100644 --- a/examples/output/label.png +++ b/examples/output/label.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e9db6ca140546ded0b819e6a646725533db65751b675938266c78c4fca6b41e1 -size 7489 +oid sha256:aa9165353773c1ab3dc61a64196afe107ce8fae80188e1380a73778d58fcf1d9 +size 7346 diff --git a/examples/output/markdown-content.png b/examples/output/markdown-content.png new file mode 100644 index 0000000..7c51fb9 --- /dev/null +++ b/examples/output/markdown-content.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38f6a100b35df4bf0a34606d593400457e5d3355e78239215be74c0cf49f4e85 +size 31497 diff --git a/examples/output/showcase-all-features.png b/examples/output/showcase-all-features.png index 2b58e8f..9ef4fca 100644 --- a/examples/output/showcase-all-features.png +++ b/examples/output/showcase-all-features.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22fa299e8ab8b7f731a60566adce8812b5eb35429f1eb9b0cfe87c4e2fc93aff -size 40047 +oid sha256:b8cee33fc9917a72cc81ed759ebc1f56f02f1c90412d252c6cb5745c47383592 +size 40130 diff --git a/examples/output/showcase-capabilities.png b/examples/output/showcase-capabilities.png new file mode 100644 index 0000000..e69de29 diff --git a/examples/output/showcase.png b/examples/output/showcase.png index fb83649..d9a939e 100644 --- a/examples/output/showcase.png +++ b/examples/output/showcase.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2126e095061f864a68fe5d19189385c56d35c6531b79ab1698fd52f4559d8d40 -size 366048 +oid sha256:f01573b46379f0324a0ff59904948339b144839267ab49d90920a04f35630c0c +size 367050 diff --git a/examples/output/table-invoice.png b/examples/output/table-invoice.png index 2206ea6..9038a88 100644 --- a/examples/output/table-invoice.png +++ b/examples/output/table-invoice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7da710b58218a03730282ce92457eeed81b7a4bbbea36f5f4e52d10c772590bc -size 27652 +oid sha256:9e0309aae03abecc8288a1b7a8b59e33c9e1388ff0bbfc5cb634c2c1c294364b +size 27335 diff --git a/examples/receipt-dynamic.yaml b/examples/receipt-dynamic.yaml index 2df8c76..8baa94c 100644 --- a/examples/receipt-dynamic.yaml +++ b/examples/receipt-dynamic.yaml @@ -9,7 +9,6 @@ template: fonts: default: "assets/fonts/Inter-Regular.ttf" - bold: "assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -29,7 +28,7 @@ layout: children: - type: text content: "{{shopName}}" - font: bold + fontWeight: bold size: 1.5em align: center color: "#1a1a1a" @@ -139,12 +138,12 @@ layout: children: - type: text content: "TOTAL" - font: bold + fontWeight: bold size: 1.2em color: "#1a1a1a" - type: text content: "{{total}} $" - font: bold + fontWeight: bold size: 1.2em color: "#1a1a1a" align: right @@ -179,7 +178,7 @@ layout: children: - type: text content: "PAID" - font: bold + fontWeight: bold size: 1.1em color: "#22c55e" align: center diff --git a/examples/receipt.yaml b/examples/receipt.yaml index dda04c6..f0a7525 100644 --- a/examples/receipt.yaml +++ b/examples/receipt.yaml @@ -8,7 +8,6 @@ template: fonts: default: "assets/fonts/Inter-Regular.ttf" - bold: "assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -29,7 +28,7 @@ layout: children: - type: text content: "{{shopName}}" - font: bold + fontWeight: bold size: 1.5em align: center color: "#1a1a1a" @@ -106,12 +105,12 @@ layout: children: - type: text content: "TOTAL" - font: bold + fontWeight: bold size: 1.2em color: "#1a1a1a" - type: text content: "{{total}} $" - font: bold + fontWeight: bold size: 1.2em color: "#1a1a1a" align: right diff --git a/examples/showcase-all-features.yaml b/examples/showcase-all-features.yaml index 39953d6..a7939b0 100644 --- a/examples/showcase-all-features.yaml +++ b/examples/showcase-all-features.yaml @@ -6,9 +6,11 @@ template: version: 1 fonts: - default: "assets/fonts/Inter-Regular.ttf" - bold: "assets/fonts/Inter-Bold.ttf" - semibold: "assets/fonts/Inter-SemiBold.ttf" + - "assets/fonts/Inter-Regular.ttf" + - "assets/fonts/Inter-Bold.ttf" + - "assets/fonts/Inter-SemiBold.ttf" + - "assets/fonts/Inter-Italic.ttf" + - "assets/fonts/Inter-BoldItalic.ttf" canvas: fixed: width @@ -32,7 +34,7 @@ layout: children: - type: text content: "{{title}}" - font: bold + fontWeight: bold size: 1.3em color: "#ffffff" align: center @@ -52,7 +54,7 @@ layout: children: - type: text content: "Customer" - font: semibold + fontWeight: semi-bold size: 0.9em color: "#6c757d" @@ -64,7 +66,7 @@ layout: content: "{{customerName}}" size: 0.9em color: "#212529" - font: semibold + fontWeight: semi-bold - type: text content: "{{customerEmail}}" size: 0.8em @@ -80,7 +82,7 @@ layout: children: - type: text content: "Order Items" - font: semibold + fontWeight: semi-bold size: 0.9em color: "#6c757d" @@ -92,7 +94,8 @@ layout: color: "#495057" row-gap: "4" column-gap: "6" - header-font: semibold + header-fontWeight: semi-bold + header-fontStyle: italic header-color: "#212529" header-size: 0.8em header-border-bottom: dashed @@ -149,12 +152,12 @@ layout: children: - type: text content: "Total" - font: bold + fontWeight: bold size: 1.1em color: "#212529" - type: text content: "${{total}}" - font: bold + fontWeight: bold size: 1.1em color: "#212529" @@ -173,7 +176,7 @@ layout: content: "{{status}}" size: 0.75em color: "#ffffff" - font: semibold + fontWeight: semi-bold - type: flex padding: "6 14" @@ -185,7 +188,7 @@ layout: content: "{{paymentMethod}}" size: 0.75em color: "#ffffff" - font: semibold + fontWeight: semi-bold # Footer - type: text diff --git a/examples/showcase-capabilities.yaml b/examples/showcase-capabilities.yaml index 274c961..c95c622 100644 --- a/examples/showcase-capabilities.yaml +++ b/examples/showcase-capabilities.yaml @@ -7,8 +7,6 @@ template: fonts: default: "assets/fonts/Inter-Regular.ttf" - bold: "assets/fonts/Inter-Bold.ttf" - semibold: "assets/fonts/Inter-SemiBold.ttf" canvas: fixed: width @@ -35,7 +33,7 @@ layout: children: - type: text content: "{{rendererName}}" - font: bold + fontWeight: bold size: 1.4em color: "#ffffff" align: center @@ -57,7 +55,7 @@ layout: children: - type: text content: "{{rendererName}}" - font: bold + fontWeight: bold size: 1.4em color: "#ffffff" align: center @@ -76,7 +74,7 @@ layout: children: - type: text content: "{{rendererName}}" - font: bold + fontWeight: bold size: 1.4em color: "#ffffff" align: center @@ -96,7 +94,7 @@ layout: children: - type: text content: "Common Features" - font: semibold + fontWeight: semi-bold size: 1em color: "#495057" - type: flex @@ -160,7 +158,7 @@ layout: children: - type: text content: "QR Code + Barcode" - font: semibold + fontWeight: semi-bold size: 1em color: "#495057" - type: flex @@ -209,7 +207,7 @@ layout: children: - type: text content: "Visual Effects (Skia exclusive)" - font: semibold + fontWeight: semi-bold size: 1em color: "#495057" - type: flex @@ -225,7 +223,7 @@ layout: children: - type: text content: "Gradient" - font: semibold + fontWeight: semi-bold size: 0.8em color: "#ffffff" align: center @@ -238,7 +236,7 @@ layout: children: - type: text content: "Shadow" - font: semibold + fontWeight: semi-bold size: 0.8em color: "#ffffff" align: center @@ -250,7 +248,7 @@ layout: children: - type: text content: "Radial" - font: semibold + fontWeight: semi-bold size: 0.8em color: "#7c3aed" align: center @@ -305,7 +303,7 @@ layout: children: - type: text content: "SVG Element Rendering" - font: bold + fontWeight: bold size: 14 color: "#1a1a2e" - type: text @@ -378,7 +376,7 @@ layout: children: - type: text content: "Feature List (dynamic loop)" - font: semibold + fontWeight: semi-bold size: 1em color: "#495057" - type: each @@ -405,7 +403,7 @@ layout: content: "{{@index + 1}}" size: 0.65em color: "#ffffff" - font: semibold + fontWeight: semi-bold align: center - type: flex grow: 1 @@ -413,7 +411,7 @@ layout: children: - type: text content: "{{item.name}}" - font: semibold + fontWeight: semi-bold size: 0.85em color: "#212529" - type: text diff --git a/examples/showcase.yaml b/examples/showcase.yaml index d95969d..86539b8 100644 --- a/examples/showcase.yaml +++ b/examples/showcase.yaml @@ -7,8 +7,6 @@ template: version: 1 fonts: default: "assets/fonts/Inter-Regular.ttf" - bold: "assets/fonts/Inter-Bold.ttf" - semibold: "assets/fonts/Inter-SemiBold.ttf" canvas: fixed: width width: 800 @@ -22,7 +20,7 @@ layout: children: - type: text content: "{{title}}" - font: bold + fontWeight: bold size: 2em color: "#ffffff" align: center @@ -45,7 +43,7 @@ layout: children: - type: text content: "Separator Styles" - font: bold + fontWeight: bold size: 14px color: "#1a1a2e" - type: text @@ -140,7 +138,7 @@ layout: children: - type: text content: "Text Features" - font: bold + fontWeight: bold size: 14px color: "#1a1a2e" - type: text @@ -160,7 +158,7 @@ layout: children: - type: text content: "Left Aligned" - font: semibold + fontWeight: semi-bold size: 12px align: left - type: text @@ -177,7 +175,7 @@ layout: children: - type: text content: "Center Aligned" - font: semibold + fontWeight: semi-bold size: 12px align: center - type: text @@ -194,7 +192,7 @@ layout: children: - type: text content: "Right Aligned" - font: semibold + fontWeight: semi-bold size: 12px align: right - type: text @@ -216,7 +214,7 @@ layout: children: - type: text content: "maxLines: 2, overflow: ellipsis" - font: semibold + fontWeight: semi-bold size: 10px color: "#e65100" - type: text @@ -234,7 +232,7 @@ layout: children: - type: text content: "lineHeight: 2.0" - font: semibold + fontWeight: semi-bold size: 10px color: "#1b5e20" - type: text @@ -257,7 +255,7 @@ layout: children: - type: text content: "Flex Basics -- Justify, Align, Grow" - font: bold + fontWeight: bold size: 14px color: "#1a1a2e" - type: text @@ -356,7 +354,7 @@ layout: children: - type: text content: "Flex Sizing -- Shrink, Basis, Min/Max Constraints" - font: bold + fontWeight: bold size: 14px color: "#1a1a2e" - type: text @@ -486,7 +484,7 @@ layout: children: - type: text content: "Flex Wrapping + AlignContent" - font: bold + fontWeight: bold size: 14px color: "#1a1a2e" - type: text @@ -710,7 +708,7 @@ layout: children: - type: text content: "AlignSelf + Auto Margins" - font: bold + fontWeight: bold size: 14px color: "#1a1a2e" - type: text @@ -845,7 +843,7 @@ layout: children: - type: text content: "Positioning -- Relative + Absolute" - font: bold + fontWeight: bold size: 14px color: "#1a1a2e" - type: text @@ -920,7 +918,7 @@ layout: children: - type: text content: "NEW" - font: bold + fontWeight: bold size: 9px color: "#ffffff" # Bottom-left badge @@ -933,7 +931,7 @@ layout: children: - type: text content: "v1.0" - font: bold + fontWeight: bold size: 9px color: "#ffffff" # Overflows right edge -- clipped by overflow: hidden @@ -962,7 +960,7 @@ layout: children: - type: text content: "Dashboard Card Grid" - font: bold + fontWeight: bold size: 14px color: "#1a1a2e" - type: text @@ -986,12 +984,12 @@ layout: children: - type: text content: "Revenue" - font: bold + fontWeight: bold size: 12px color: "#1a1a2e" - type: text content: "{{dashboard.revenue}}" - font: bold + fontWeight: bold size: 24px color: "#0f3460" - type: text @@ -1010,12 +1008,12 @@ layout: children: - type: text content: "Users" - font: bold + fontWeight: bold size: 12px color: "#1a1a2e" - type: text content: "{{dashboard.users}}" - font: bold + fontWeight: bold size: 24px color: "#0f3460" - type: text @@ -1034,12 +1032,12 @@ layout: children: - type: text content: "Orders" - font: bold + fontWeight: bold size: 12px color: "#1a1a2e" - type: text content: "{{dashboard.orders}}" - font: bold + fontWeight: bold size: 24px color: "#0f3460" - type: text @@ -1058,12 +1056,12 @@ layout: children: - type: text content: "Uptime" - font: bold + fontWeight: bold size: 12px color: "#1a1a2e" - type: text content: "{{dashboard.uptime}}" - font: bold + fontWeight: bold size: 24px color: "#0f3460" - type: text @@ -1082,12 +1080,12 @@ layout: children: - type: text content: "Latency" - font: bold + fontWeight: bold size: 12px color: "#1a1a2e" - type: text content: "{{dashboard.latency}}" - font: bold + fontWeight: bold size: 24px color: "#0f3460" - type: text @@ -1106,12 +1104,12 @@ layout: children: - type: text content: "Errors" - font: bold + fontWeight: bold size: 12px color: "#1a1a2e" - type: text content: "{{dashboard.errors}}" - font: bold + fontWeight: bold size: 24px color: "#e74c3c" - type: text @@ -1132,7 +1130,7 @@ layout: children: - type: text content: "Loop Metadata -- @index, @first, @last" - font: bold + fontWeight: bold size: 14px color: "#1a1a2e" - type: text @@ -1163,13 +1161,13 @@ layout: children: - type: text content: "{{@index}}" - font: bold + fontWeight: bold size: 11px color: "#ffffff" align: center - type: text content: "{{feature.name}}" - font: semibold + fontWeight: semi-bold size: 11px color: "#1a1a2e" - type: text @@ -1201,13 +1199,13 @@ layout: children: - type: text content: "{{@index}}" - font: bold + fontWeight: bold size: 11px color: "#ffffff" align: center - type: text content: "{{feature.name}}" - font: semibold + fontWeight: semi-bold size: 11px color: "#1a1a2e" - type: text @@ -1244,13 +1242,13 @@ layout: children: - type: text content: "{{@index}}" - font: bold + fontWeight: bold size: 11px color: "#ffffff" align: center - type: text content: "{{feature.name}}" - font: semibold + fontWeight: semi-bold size: 11px color: "#1a1a2e" - type: text @@ -1272,7 +1270,7 @@ layout: children: - type: text content: "Conditional Chains -- if / else-if / else" - font: bold + fontWeight: bold size: 14px color: "#1a1a2e" - type: text @@ -1299,7 +1297,7 @@ layout: children: - type: text content: "Active" - font: bold + fontWeight: bold size: 12px color: "#ffffff" - type: text @@ -1321,7 +1319,7 @@ layout: children: - type: text content: "Pending" - font: bold + fontWeight: bold size: 12px color: "#ffffff" - type: text @@ -1340,7 +1338,7 @@ layout: children: - type: text content: "Unknown" - font: bold + fontWeight: bold size: 12px color: "#ffffff" - type: text @@ -1372,7 +1370,7 @@ layout: children: - type: text content: "Active" - font: bold + fontWeight: bold size: 11px color: "#ffffff" - type: flex @@ -1391,7 +1389,7 @@ layout: children: - type: text content: "Pending" - font: bold + fontWeight: bold size: 11px color: "#ffffff" - type: flex @@ -1410,7 +1408,7 @@ layout: children: - type: text content: "Unknown" - font: bold + fontWeight: bold size: 11px color: "#ffffff" @@ -1427,7 +1425,7 @@ layout: children: - type: text content: "QR Code + Barcode Formats" - font: bold + fontWeight: bold size: 14px color: "#1a1a2e" - type: text @@ -1560,7 +1558,7 @@ layout: children: - type: text content: "Image Features and Rotation" - font: bold + fontWeight: bold size: 14px color: "#1a1a2e" - type: text @@ -1619,7 +1617,7 @@ layout: align: center - type: text content: "ROTATED" - font: bold + fontWeight: bold size: 20px color: "#e74c3c" rotate: "15" @@ -1642,7 +1640,7 @@ layout: children: - type: text content: "FlexRender" - font: bold + fontWeight: bold size: 1em color: "#ffffff" - type: text diff --git a/examples/svg-icons.yaml b/examples/svg-icons.yaml index cc14550..6a246a0 100644 --- a/examples/svg-icons.yaml +++ b/examples/svg-icons.yaml @@ -4,7 +4,6 @@ template: fonts: default: "assets/fonts/Inter-Regular.ttf" - bold: "assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -18,7 +17,7 @@ layout: children: - type: text content: "SVG Icons Demo" - font: bold + fontWeight: bold size: 1.3em align: center color: "#1a1a1a" @@ -95,7 +94,7 @@ layout: children: - type: text content: "FlexRender" - font: bold + fontWeight: bold size: 1.1em color: "#1a1a1a" - type: text diff --git a/examples/table-invoice.yaml b/examples/table-invoice.yaml index c80a8c8..2438fc5 100644 --- a/examples/table-invoice.yaml +++ b/examples/table-invoice.yaml @@ -7,9 +7,10 @@ template: version: 1 fonts: - default: "assets/fonts/Inter-Regular.ttf" - bold: "assets/fonts/Inter-Bold.ttf" - semibold: "assets/fonts/Inter-SemiBold.ttf" + - "assets/fonts/Inter-Regular.ttf" + - "assets/fonts/Inter-Bold.ttf" + - "assets/fonts/Inter-SemiBold.ttf" + - "assets/fonts/Inter-Italic.ttf" canvas: fixed: width @@ -29,7 +30,7 @@ layout: children: - type: text content: "{{companyName}}" - font: bold + fontWeight: bold size: 1.4em align: center color: "#1a1a1a" @@ -60,7 +61,8 @@ layout: color: "#333333" row-gap: "2" column-gap: "8" - header-font: semibold + header-fontWeight: semi-bold + header-fontStyle: italic header-color: "#1a1a1a" header-size: 0.85em header-border-bottom: dashed @@ -80,7 +82,7 @@ layout: label: "Total" width: "60" align: right - font: semibold + fontWeight: semi-bold - type: separator style: solid @@ -106,7 +108,7 @@ layout: value: "{{tax}}" - label: "TOTAL" value: "{{total}}" - font: bold + fontWeight: bold color: "#1a1a1a" size: "1.1em" diff --git a/examples/ticket.yaml b/examples/ticket.yaml index 6c5f25e..dcbdf7d 100644 --- a/examples/ticket.yaml +++ b/examples/ticket.yaml @@ -8,8 +8,6 @@ template: fonts: default: "assets/fonts/Inter-Regular.ttf" - bold: "assets/fonts/Inter-Bold.ttf" - semibold: "assets/fonts/Inter-SemiBold.ttf" canvas: fixed: width @@ -25,7 +23,7 @@ layout: children: - type: text content: "{{eventName}}" - font: bold + fontWeight: bold size: 1.5em align: center color: "#ffffff" @@ -61,7 +59,7 @@ layout: color: "#888888" - type: text content: "{{date}}" - font: semibold + fontWeight: semi-bold size: 1.05em color: "#1a1a2e" @@ -77,7 +75,7 @@ layout: color: "#888888" - type: text content: "{{time}}" - font: semibold + fontWeight: semi-bold size: 1.05em color: "#1a1a2e" @@ -100,7 +98,7 @@ layout: align: center - type: text content: "{{section}}" - font: bold + fontWeight: bold size: 1.3em color: "#1a1a2e" align: center @@ -119,7 +117,7 @@ layout: align: center - type: text content: "{{row}}" - font: bold + fontWeight: bold size: 1.3em color: "#1a1a2e" align: center @@ -138,7 +136,7 @@ layout: align: center - type: text content: "{{seatNumber}}" - font: bold + fontWeight: bold size: 1.3em color: "#1a1a2e" align: center diff --git a/examples/visual-docs/align/baseline.yaml b/examples/visual-docs/align/baseline.yaml index 7ad7f7a..a66ba11 100644 --- a/examples/visual-docs/align/baseline.yaml +++ b/examples/visual-docs/align/baseline.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -22,21 +21,21 @@ layout: - type: text content: "Small" color: "#3498db" - font: bold + fontWeight: bold size: 14px background: "#e3f2fd" padding: "4 8" - type: text content: "Large" color: "#e74c3c" - font: bold + fontWeight: bold size: 28px background: "#ffebee" padding: "4 8" - type: text content: "Medium" color: "#2ecc71" - font: bold + fontWeight: bold size: 18px background: "#e8f5e9" padding: "4 8" diff --git a/examples/visual-docs/align/center.yaml b/examples/visual-docs/align/center.yaml index 79bfbf6..3832b98 100644 --- a/examples/visual-docs/align/center.yaml +++ b/examples/visual-docs/align/center.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -30,7 +29,7 @@ layout: - type: text content: "Box 1" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center - type: flex @@ -44,7 +43,7 @@ layout: - type: text content: "Box 2" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center - type: flex @@ -58,6 +57,6 @@ layout: - type: text content: "Box 3" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center diff --git a/examples/visual-docs/align/end.yaml b/examples/visual-docs/align/end.yaml index 17f1d50..bd3d7a5 100644 --- a/examples/visual-docs/align/end.yaml +++ b/examples/visual-docs/align/end.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -30,7 +29,7 @@ layout: - type: text content: "Box 1" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center - type: flex @@ -44,7 +43,7 @@ layout: - type: text content: "Box 2" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center - type: flex @@ -58,6 +57,6 @@ layout: - type: text content: "Box 3" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center diff --git a/examples/visual-docs/align/start.yaml b/examples/visual-docs/align/start.yaml index 53a927f..57e9d92 100644 --- a/examples/visual-docs/align/start.yaml +++ b/examples/visual-docs/align/start.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -30,7 +29,7 @@ layout: - type: text content: "Box 1" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center - type: flex @@ -44,7 +43,7 @@ layout: - type: text content: "Box 2" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center - type: flex @@ -58,6 +57,6 @@ layout: - type: text content: "Box 3" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center diff --git a/examples/visual-docs/align/stretch.yaml b/examples/visual-docs/align/stretch.yaml index a2f8b50..b21d779 100644 --- a/examples/visual-docs/align/stretch.yaml +++ b/examples/visual-docs/align/stretch.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -28,7 +27,7 @@ layout: - type: text content: "Box 1" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center - type: flex @@ -41,7 +40,7 @@ layout: - type: text content: "Box 2" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center - type: flex @@ -54,6 +53,6 @@ layout: - type: text content: "Box 3" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center diff --git a/examples/visual-docs/border/per-side.yaml b/examples/visual-docs/border/per-side.yaml index f8e8b24..72a636b 100644 --- a/examples/visual-docs/border/per-side.yaml +++ b/examples/visual-docs/border/per-side.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -27,7 +26,7 @@ layout: children: - type: text content: "Per-Side Borders" - font: bold + fontWeight: bold size: 16px color: "#333333" align: center diff --git a/examples/visual-docs/border/radius.yaml b/examples/visual-docs/border/radius.yaml index 53adbd7..518ea2c 100644 --- a/examples/visual-docs/border/radius.yaml +++ b/examples/visual-docs/border/radius.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -24,7 +23,7 @@ layout: children: - type: text content: "Rounded Corners" - font: bold + fontWeight: bold size: 16px color: "#3498db" - type: text diff --git a/examples/visual-docs/border/solid.yaml b/examples/visual-docs/border/solid.yaml index fa7f3e1..92d9b68 100644 --- a/examples/visual-docs/border/solid.yaml +++ b/examples/visual-docs/border/solid.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -23,7 +22,7 @@ layout: children: - type: text content: "Solid Border" - font: bold + fontWeight: bold size: 16px color: "#333333" - type: text diff --git a/examples/visual-docs/border/styles.yaml b/examples/visual-docs/border/styles.yaml index 822a2a3..62231a6 100644 --- a/examples/visual-docs/border/styles.yaml +++ b/examples/visual-docs/border/styles.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -26,7 +25,7 @@ layout: children: - type: text content: "Solid" - font: bold + fontWeight: bold size: 14px color: "#3498db" align: center @@ -45,7 +44,7 @@ layout: children: - type: text content: "Dashed" - font: bold + fontWeight: bold size: 14px color: "#e74c3c" align: center @@ -64,7 +63,7 @@ layout: children: - type: text content: "Dotted" - font: bold + fontWeight: bold size: 14px color: "#2ecc71" align: center diff --git a/examples/visual-docs/canvas/background.yaml b/examples/visual-docs/canvas/background.yaml index 6514b83..6c58c6d 100644 --- a/examples/visual-docs/canvas/background.yaml +++ b/examples/visual-docs/canvas/background.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -19,7 +18,7 @@ layout: children: - type: text content: "Dark Background (#1a1a2e)" - font: bold + fontWeight: bold size: 18px color: "#ffffff" diff --git a/examples/visual-docs/canvas/fixed-both.yaml b/examples/visual-docs/canvas/fixed-both.yaml index 4a2e5fa..215238d 100644 --- a/examples/visual-docs/canvas/fixed-both.yaml +++ b/examples/visual-docs/canvas/fixed-both.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: both @@ -20,7 +19,7 @@ layout: children: - type: text content: "fixed: both" - font: bold + fontWeight: bold size: 18px color: "#1a1a2e" @@ -45,7 +44,7 @@ layout: - type: text content: "A" color: "#ffffff" - font: bold + fontWeight: bold size: 20px align: center @@ -59,7 +58,7 @@ layout: - type: text content: "B" color: "#ffffff" - font: bold + fontWeight: bold size: 20px align: center @@ -73,6 +72,6 @@ layout: - type: text content: "C" color: "#ffffff" - font: bold + fontWeight: bold size: 20px align: center diff --git a/examples/visual-docs/canvas/fixed-height.yaml b/examples/visual-docs/canvas/fixed-height.yaml index 9176624..76ed95c 100644 --- a/examples/visual-docs/canvas/fixed-height.yaml +++ b/examples/visual-docs/canvas/fixed-height.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: height @@ -19,7 +18,7 @@ layout: children: - type: text content: "fixed: height" - font: bold + fontWeight: bold size: 18px color: "#1a1a2e" diff --git a/examples/visual-docs/canvas/fixed-none.yaml b/examples/visual-docs/canvas/fixed-none.yaml index 134cfec..a0b14c2 100644 --- a/examples/visual-docs/canvas/fixed-none.yaml +++ b/examples/visual-docs/canvas/fixed-none.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: none @@ -18,7 +17,7 @@ layout: children: - type: text content: "fixed: none" - font: bold + fontWeight: bold size: 18px color: "#1a1a2e" diff --git a/examples/visual-docs/canvas/fixed-width.yaml b/examples/visual-docs/canvas/fixed-width.yaml index 3ff2b4d..7cc4269 100644 --- a/examples/visual-docs/canvas/fixed-width.yaml +++ b/examples/visual-docs/canvas/fixed-width.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -19,7 +18,7 @@ layout: children: - type: text content: "fixed: width" - font: bold + fontWeight: bold size: 18px color: "#1a1a2e" diff --git a/examples/visual-docs/canvas/rotate-flip.yaml b/examples/visual-docs/canvas/rotate-flip.yaml index 2fb579a..9c36c05 100644 --- a/examples/visual-docs/canvas/rotate-flip.yaml +++ b/examples/visual-docs/canvas/rotate-flip.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: both @@ -21,7 +20,7 @@ layout: children: - type: text content: "TOP" - font: bold + fontWeight: bold size: 16px color: "#1a1a2e" align: center @@ -47,7 +46,7 @@ layout: - type: text content: ">" color: "#ffffff" - font: bold + fontWeight: bold size: 28px align: center @@ -58,7 +57,7 @@ layout: - type: text content: "BOTTOM" - font: bold + fontWeight: bold size: 16px color: "#cccccc" align: center diff --git a/examples/visual-docs/canvas/rotate-left.yaml b/examples/visual-docs/canvas/rotate-left.yaml index eab9cda..b9b6e81 100644 --- a/examples/visual-docs/canvas/rotate-left.yaml +++ b/examples/visual-docs/canvas/rotate-left.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: both @@ -21,7 +20,7 @@ layout: children: - type: text content: "TOP" - font: bold + fontWeight: bold size: 16px color: "#1a1a2e" align: center @@ -47,7 +46,7 @@ layout: - type: text content: ">" color: "#ffffff" - font: bold + fontWeight: bold size: 28px align: center @@ -58,7 +57,7 @@ layout: - type: text content: "BOTTOM" - font: bold + fontWeight: bold size: 16px color: "#cccccc" align: center diff --git a/examples/visual-docs/canvas/rotate-right.yaml b/examples/visual-docs/canvas/rotate-right.yaml index dfb3875..9cb943e 100644 --- a/examples/visual-docs/canvas/rotate-right.yaml +++ b/examples/visual-docs/canvas/rotate-right.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: both @@ -21,7 +20,7 @@ layout: children: - type: text content: "TOP" - font: bold + fontWeight: bold size: 16px color: "#1a1a2e" align: center @@ -47,7 +46,7 @@ layout: - type: text content: ">" color: "#ffffff" - font: bold + fontWeight: bold size: 28px align: center @@ -58,7 +57,7 @@ layout: - type: text content: "BOTTOM" - font: bold + fontWeight: bold size: 16px color: "#cccccc" align: center diff --git a/examples/visual-docs/canvas/rotate.yaml b/examples/visual-docs/canvas/rotate.yaml index 5ae3126..3f660e9 100644 --- a/examples/visual-docs/canvas/rotate.yaml +++ b/examples/visual-docs/canvas/rotate.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: both @@ -21,7 +20,7 @@ layout: children: - type: text content: "TOP" - font: bold + fontWeight: bold size: 16px color: "#1a1a2e" align: center @@ -47,7 +46,7 @@ layout: - type: text content: ">" color: "#ffffff" - font: bold + fontWeight: bold size: 28px align: center @@ -58,7 +57,7 @@ layout: - type: text content: "BOTTOM" - font: bold + fontWeight: bold size: 16px color: "#cccccc" align: center diff --git a/examples/visual-docs/direction/column-reverse.yaml b/examples/visual-docs/direction/column-reverse.yaml index 90fe103..a7d656d 100644 --- a/examples/visual-docs/direction/column-reverse.yaml +++ b/examples/visual-docs/direction/column-reverse.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -29,7 +28,7 @@ layout: - type: text content: "1" color: "#ffffff" - font: bold + fontWeight: bold size: 24px align: center - type: flex @@ -43,7 +42,7 @@ layout: - type: text content: "2" color: "#ffffff" - font: bold + fontWeight: bold size: 24px align: center - type: flex @@ -57,6 +56,6 @@ layout: - type: text content: "3" color: "#ffffff" - font: bold + fontWeight: bold size: 24px align: center diff --git a/examples/visual-docs/direction/column.yaml b/examples/visual-docs/direction/column.yaml index 6ced0e0..94f39d0 100644 --- a/examples/visual-docs/direction/column.yaml +++ b/examples/visual-docs/direction/column.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -29,7 +28,7 @@ layout: - type: text content: "1" color: "#ffffff" - font: bold + fontWeight: bold size: 24px align: center - type: flex @@ -43,7 +42,7 @@ layout: - type: text content: "2" color: "#ffffff" - font: bold + fontWeight: bold size: 24px align: center - type: flex @@ -57,6 +56,6 @@ layout: - type: text content: "3" color: "#ffffff" - font: bold + fontWeight: bold size: 24px align: center diff --git a/examples/visual-docs/direction/row-reverse-rtl.yaml b/examples/visual-docs/direction/row-reverse-rtl.yaml index 2687253..a124b69 100644 --- a/examples/visual-docs/direction/row-reverse-rtl.yaml +++ b/examples/visual-docs/direction/row-reverse-rtl.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -29,7 +28,7 @@ layout: - type: text content: "1" color: "#ffffff" - font: bold + fontWeight: bold size: 24px align: center - type: flex @@ -43,7 +42,7 @@ layout: - type: text content: "2" color: "#ffffff" - font: bold + fontWeight: bold size: 24px align: center - type: flex @@ -57,6 +56,6 @@ layout: - type: text content: "3" color: "#ffffff" - font: bold + fontWeight: bold size: 24px align: center diff --git a/examples/visual-docs/direction/row-reverse.yaml b/examples/visual-docs/direction/row-reverse.yaml index ec115b8..3530eb2 100644 --- a/examples/visual-docs/direction/row-reverse.yaml +++ b/examples/visual-docs/direction/row-reverse.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -28,7 +27,7 @@ layout: - type: text content: "1" color: "#ffffff" - font: bold + fontWeight: bold size: 24px align: center - type: flex @@ -42,7 +41,7 @@ layout: - type: text content: "2" color: "#ffffff" - font: bold + fontWeight: bold size: 24px align: center - type: flex @@ -56,6 +55,6 @@ layout: - type: text content: "3" color: "#ffffff" - font: bold + fontWeight: bold size: 24px align: center diff --git a/examples/visual-docs/direction/row-rtl.yaml b/examples/visual-docs/direction/row-rtl.yaml index eedf1a0..174774d 100644 --- a/examples/visual-docs/direction/row-rtl.yaml +++ b/examples/visual-docs/direction/row-rtl.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -29,7 +28,7 @@ layout: - type: text content: "1" color: "#ffffff" - font: bold + fontWeight: bold size: 24px align: center - type: flex @@ -43,7 +42,7 @@ layout: - type: text content: "2" color: "#ffffff" - font: bold + fontWeight: bold size: 24px align: center - type: flex @@ -57,6 +56,6 @@ layout: - type: text content: "3" color: "#ffffff" - font: bold + fontWeight: bold size: 24px align: center diff --git a/examples/visual-docs/direction/row.yaml b/examples/visual-docs/direction/row.yaml index df549ae..12ee284 100644 --- a/examples/visual-docs/direction/row.yaml +++ b/examples/visual-docs/direction/row.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -28,7 +27,7 @@ layout: - type: text content: "1" color: "#ffffff" - font: bold + fontWeight: bold size: 24px align: center - type: flex @@ -42,7 +41,7 @@ layout: - type: text content: "2" color: "#ffffff" - font: bold + fontWeight: bold size: 24px align: center - type: flex @@ -56,6 +55,6 @@ layout: - type: text content: "3" color: "#ffffff" - font: bold + fontWeight: bold size: 24px align: center diff --git a/examples/visual-docs/direction/rtl-mixed.yaml b/examples/visual-docs/direction/rtl-mixed.yaml index 1e1925b..dfe85c2 100644 --- a/examples/visual-docs/direction/rtl-mixed.yaml +++ b/examples/visual-docs/direction/rtl-mixed.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" arabic: "../../assets/fonts/NotoSansArabic-Regular.ttf" canvas: @@ -40,7 +39,7 @@ layout: children: - type: text content: "LTR Section" - font: bold + fontWeight: bold size: 14px color: "#3498db" - type: text diff --git a/examples/visual-docs/direction/rtl-row.yaml b/examples/visual-docs/direction/rtl-row.yaml index 66d2c4f..186274e 100644 --- a/examples/visual-docs/direction/rtl-row.yaml +++ b/examples/visual-docs/direction/rtl-row.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -39,7 +38,7 @@ layout: - type: text content: "First" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center @@ -54,7 +53,7 @@ layout: - type: text content: "Second" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center @@ -69,6 +68,6 @@ layout: - type: text content: "Third" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center diff --git a/examples/visual-docs/justify/center.yaml b/examples/visual-docs/justify/center.yaml index 71d481e..dd061d4 100644 --- a/examples/visual-docs/justify/center.yaml +++ b/examples/visual-docs/justify/center.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -29,7 +28,7 @@ layout: - type: text content: "Box 1" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center - type: flex @@ -43,7 +42,7 @@ layout: - type: text content: "Box 2" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center - type: flex @@ -57,6 +56,6 @@ layout: - type: text content: "Box 3" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center diff --git a/examples/visual-docs/justify/end.yaml b/examples/visual-docs/justify/end.yaml index f3d852c..89a4d36 100644 --- a/examples/visual-docs/justify/end.yaml +++ b/examples/visual-docs/justify/end.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -29,7 +28,7 @@ layout: - type: text content: "Box 1" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center - type: flex @@ -43,7 +42,7 @@ layout: - type: text content: "Box 2" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center - type: flex @@ -57,6 +56,6 @@ layout: - type: text content: "Box 3" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center diff --git a/examples/visual-docs/justify/space-around.yaml b/examples/visual-docs/justify/space-around.yaml index 7784758..4b2e7ed 100644 --- a/examples/visual-docs/justify/space-around.yaml +++ b/examples/visual-docs/justify/space-around.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -29,7 +28,7 @@ layout: - type: text content: "Box 1" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center - type: flex @@ -43,7 +42,7 @@ layout: - type: text content: "Box 2" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center - type: flex @@ -57,6 +56,6 @@ layout: - type: text content: "Box 3" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center diff --git a/examples/visual-docs/justify/space-between.yaml b/examples/visual-docs/justify/space-between.yaml index 30fd25a..a2d35c3 100644 --- a/examples/visual-docs/justify/space-between.yaml +++ b/examples/visual-docs/justify/space-between.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -29,7 +28,7 @@ layout: - type: text content: "Box 1" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center - type: flex @@ -43,7 +42,7 @@ layout: - type: text content: "Box 2" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center - type: flex @@ -57,6 +56,6 @@ layout: - type: text content: "Box 3" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center diff --git a/examples/visual-docs/justify/space-evenly.yaml b/examples/visual-docs/justify/space-evenly.yaml index 0a62c8f..119c16b 100644 --- a/examples/visual-docs/justify/space-evenly.yaml +++ b/examples/visual-docs/justify/space-evenly.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -29,7 +28,7 @@ layout: - type: text content: "Box 1" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center - type: flex @@ -43,7 +42,7 @@ layout: - type: text content: "Box 2" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center - type: flex @@ -57,6 +56,6 @@ layout: - type: text content: "Box 3" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center diff --git a/examples/visual-docs/justify/start.yaml b/examples/visual-docs/justify/start.yaml index 390f24c..aab70ad 100644 --- a/examples/visual-docs/justify/start.yaml +++ b/examples/visual-docs/justify/start.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -29,7 +28,7 @@ layout: - type: text content: "Box 1" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center - type: flex @@ -43,7 +42,7 @@ layout: - type: text content: "Box 2" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center - type: flex @@ -57,6 +56,6 @@ layout: - type: text content: "Box 3" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center diff --git a/examples/visual-docs/order/basic.yaml b/examples/visual-docs/order/basic.yaml index 27bb13e..86ac120 100644 --- a/examples/visual-docs/order/basic.yaml +++ b/examples/visual-docs/order/basic.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -37,7 +36,7 @@ layout: - type: text content: "A" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center - type: text @@ -58,7 +57,7 @@ layout: - type: text content: "B" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center - type: text @@ -79,7 +78,7 @@ layout: - type: text content: "C" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center - type: text diff --git a/examples/visual-docs/order/negative.yaml b/examples/visual-docs/order/negative.yaml index 346c44c..f92110a 100644 --- a/examples/visual-docs/order/negative.yaml +++ b/examples/visual-docs/order/negative.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -37,7 +36,7 @@ layout: - type: text content: "A" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center - type: text @@ -58,7 +57,7 @@ layout: - type: text content: "B" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center - type: text @@ -79,7 +78,7 @@ layout: - type: text content: "C" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center - type: text diff --git a/examples/visual-docs/output/align-baseline.png b/examples/visual-docs/output/align-baseline.png index c1ede01..400dfce 100644 --- a/examples/visual-docs/output/align-baseline.png +++ b/examples/visual-docs/output/align-baseline.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ebb41b5af5dfaeba93fc9f2da744f09fc1bf0d555176354c08f21bb1f718c58f -size 4603 +oid sha256:26cfb16dd2d30c056c11ccc98221564ca9c7dda11c27d637199b05cfa41a236c +size 4540 diff --git a/examples/visual-docs/output/align-center.png b/examples/visual-docs/output/align-center.png index 3b479fe..a358660 100644 --- a/examples/visual-docs/output/align-center.png +++ b/examples/visual-docs/output/align-center.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8b34605854506ed417291f202f6c6ad3c4b2d2923678e7c7b7f28e6985496f0a -size 3472 +oid sha256:d44ea33d6f6a213a5e0396af80b3d3c56d216decd784ab3a7c9dfdb9718edbdc +size 3471 diff --git a/examples/visual-docs/output/align-end.png b/examples/visual-docs/output/align-end.png index bec3090..0d53bee 100644 --- a/examples/visual-docs/output/align-end.png +++ b/examples/visual-docs/output/align-end.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b97d48cc7a4a2b658f9cee832046d577883653b4fb1d304f33e2913da8a11f04 -size 3488 +oid sha256:1c3acf1d7a3691288997c94ac041e6b9e5d28dc0e97c0332571df9f90d5abab7 +size 3499 diff --git a/examples/visual-docs/output/align-start.png b/examples/visual-docs/output/align-start.png index 2320a22..9bbfb52 100644 --- a/examples/visual-docs/output/align-start.png +++ b/examples/visual-docs/output/align-start.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f49fe8f5f260842ceb32cad04cd9cf1d56fdc18ec6d583694ffc998302e21b2a -size 3487 +oid sha256:4478be265fd9ed3b68950c52be58801928674adeb04491f366eefb7321ade562 +size 3499 diff --git a/examples/visual-docs/output/align-stretch.png b/examples/visual-docs/output/align-stretch.png index 327db61..49909b3 100644 --- a/examples/visual-docs/output/align-stretch.png +++ b/examples/visual-docs/output/align-stretch.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ec53a4988c7c03f80def42b9c4206d050088e75649d6e3f256ecaa5985fa9ec8 -size 3079 +oid sha256:10b3181e4c6f866d1e200ef15bc8f33154b0c6e7b7edaf340f1f08f7ce1d1021 +size 3084 diff --git a/examples/visual-docs/output/barcode-colors.png b/examples/visual-docs/output/barcode-colors.png index 73b6b1c..82447b9 100644 --- a/examples/visual-docs/output/barcode-colors.png +++ b/examples/visual-docs/output/barcode-colors.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aa4f49c6c21ffa3f16100af6e61930dc0ff52fc1e930197345e05181473b101a -size 20641 +oid sha256:b19d58eeffb7f2684074867a38fb6b93df4ba09e8759514ee65984629de647e0 +size 20823 diff --git a/examples/visual-docs/output/barcode-showText.png b/examples/visual-docs/output/barcode-showText.png index c04884c..70e026c 100644 --- a/examples/visual-docs/output/barcode-showText.png +++ b/examples/visual-docs/output/barcode-showText.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c36b4a362e3c82a69b22f1da45daf39bbdb18df95d6e81082b01c72103c1fff0 -size 13176 +oid sha256:c5da09a84504db74ef2332ed79c76ee8c3bd5739a2c7e43dfb7be3c90d46d954 +size 13593 diff --git a/examples/visual-docs/output/border-per-side.png b/examples/visual-docs/output/border-per-side.png index 2738f2f..8a99636 100644 --- a/examples/visual-docs/output/border-per-side.png +++ b/examples/visual-docs/output/border-per-side.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:886b713d4fc35f53cbe652df552c50e00f979837d462037a695b8c0b7e33d273 -size 13653 +oid sha256:580afcf64515eb6dcba6d60f06014d1f1e5189e63f82ea27701e2127b2177538 +size 13620 diff --git a/examples/visual-docs/output/border-radius.png b/examples/visual-docs/output/border-radius.png index 060f5b9..fa66651 100644 --- a/examples/visual-docs/output/border-radius.png +++ b/examples/visual-docs/output/border-radius.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f4b674929cf4143dab03322df9932fa32f85c69c538bade856a26b9fa50ad65f -size 15800 +oid sha256:6428b1ae46c99106da9ee8cc524968fe913ee87b9d82651cb266094a93552323 +size 15861 diff --git a/examples/visual-docs/output/border-solid.png b/examples/visual-docs/output/border-solid.png index 915ba02..1608fc9 100644 --- a/examples/visual-docs/output/border-solid.png +++ b/examples/visual-docs/output/border-solid.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7cd2e1a7d80420c69524f290206cc920a76eeb8f2bc03ecd59da567865591d4 -size 8671 +oid sha256:811eb2a205bf896ffd8b92072e5b9f43bd5e364717cd1fa9da3c2b4803f25637 +size 8674 diff --git a/examples/visual-docs/output/border-styles.png b/examples/visual-docs/output/border-styles.png index 1a7c058..382441c 100644 --- a/examples/visual-docs/output/border-styles.png +++ b/examples/visual-docs/output/border-styles.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc566d8cb5fcdb74e89ba98b18ce7486f0b06023e4b48f1c00ce3f2dc9ce91db -size 7049 +oid sha256:c75999588616a7afdedcbb91e0a24b7a8b58d82149b0c6d76e0db2cd048fee9b +size 7179 diff --git a/examples/visual-docs/output/canvas-background.png b/examples/visual-docs/output/canvas-background.png index d02cda9..dff9a2a 100644 --- a/examples/visual-docs/output/canvas-background.png +++ b/examples/visual-docs/output/canvas-background.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da9dff7ff52e46197daf9adfa8d4ffedb2da7542dd734dd266dff0ff85c7cb09 -size 16761 +oid sha256:8b8f1d4678237095ea0993fb20a80517a07eded3fb73388bb07f7dc9abbb24ea +size 16824 diff --git a/examples/visual-docs/output/canvas-fixed-both.png b/examples/visual-docs/output/canvas-fixed-both.png index e518e9c..e5374e6 100644 --- a/examples/visual-docs/output/canvas-fixed-both.png +++ b/examples/visual-docs/output/canvas-fixed-both.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4332ea4257f24339f90962cd4bc5bff1f36350dde25a25ce51f11d4106495b5c -size 12733 +oid sha256:b26988168df6abf618e2e5cb7f28f252e598f8ff9e4392f7d84b1133725a445f +size 12685 diff --git a/examples/visual-docs/output/canvas-fixed-height.png b/examples/visual-docs/output/canvas-fixed-height.png index 0f41616..43b734c 100644 --- a/examples/visual-docs/output/canvas-fixed-height.png +++ b/examples/visual-docs/output/canvas-fixed-height.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f51837b875030d7656040f50ee1e56eebe95045a03599f866fb5455ab118ac2 -size 15082 +oid sha256:76f0e8482c27f339332b0b18d998c728acb78e02a65467cec2e3a25cc0bb7f74 +size 15025 diff --git a/examples/visual-docs/output/canvas-fixed-none.png b/examples/visual-docs/output/canvas-fixed-none.png index 4e597b8..fcfae01 100644 --- a/examples/visual-docs/output/canvas-fixed-none.png +++ b/examples/visual-docs/output/canvas-fixed-none.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:708dfacf8d44feac94d405af04c296e38410135dabaf830e7a1e0e066b7ac6e1 -size 13612 +oid sha256:5be5b4da0352aa3e63862b07612c2791a54c528e0b9bddf3180eaeaa30e45897 +size 13757 diff --git a/examples/visual-docs/output/canvas-fixed-width.png b/examples/visual-docs/output/canvas-fixed-width.png index 9451ab1..973651e 100644 --- a/examples/visual-docs/output/canvas-fixed-width.png +++ b/examples/visual-docs/output/canvas-fixed-width.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:888f1997463ffe454eb10a8987dec5d8ac805e0b8820ccd157e4052c7a2e8126 -size 17984 +oid sha256:c0048250031d4899a9d8355f3e5182ece92e4f18526cbd92bef7c820d237ddcd +size 18030 diff --git a/examples/visual-docs/output/canvas-rotate-flip.png b/examples/visual-docs/output/canvas-rotate-flip.png index afbdc48..47436f8 100644 --- a/examples/visual-docs/output/canvas-rotate-flip.png +++ b/examples/visual-docs/output/canvas-rotate-flip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b216e2578ff4c60b90d4b6548eb469b85fc2ba4829a56a5c63820c7222ce225 -size 4244 +oid sha256:04b957574bb0687cce67250c59da4f79bc783f632c55ff61cf337d1d0970c21b +size 4206 diff --git a/examples/visual-docs/output/canvas-rotate-left.png b/examples/visual-docs/output/canvas-rotate-left.png index 8d33715..86f07df 100644 --- a/examples/visual-docs/output/canvas-rotate-left.png +++ b/examples/visual-docs/output/canvas-rotate-left.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f9e549589054324ace92dd38e065de604648a580d237c6e56bff349c7edb3fff -size 4723 +oid sha256:e416653eb432fdc03a6b02a479e2ff13f15f62281903eaf27828c517ed0f671f +size 4718 diff --git a/examples/visual-docs/output/canvas-rotate-right.png b/examples/visual-docs/output/canvas-rotate-right.png index 8129ed7..82a893d 100644 --- a/examples/visual-docs/output/canvas-rotate-right.png +++ b/examples/visual-docs/output/canvas-rotate-right.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:187f35f036e899cb8df0c5d643908d40bb6be021c0d5571ab8a8ff6746214764 -size 4684 +oid sha256:66ed4d217e2e91bf6723924d55900c732a75953c0c7c01eef2f1f2689baee584 +size 4657 diff --git a/examples/visual-docs/output/canvas-rotate.png b/examples/visual-docs/output/canvas-rotate.png index 5defd99..23003e9 100644 --- a/examples/visual-docs/output/canvas-rotate.png +++ b/examples/visual-docs/output/canvas-rotate.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:263445389388103fe461ad92586e404215e3359b2f37c34731bda8818a0f9c6f -size 4315 +oid sha256:fde107746cde37a531c4363a1100e6068aabace073ec150ed7a29cf8b6cb1698 +size 4289 diff --git a/examples/visual-docs/output/direction-column-reverse.png b/examples/visual-docs/output/direction-column-reverse.png index 3913d04..0b53c25 100644 --- a/examples/visual-docs/output/direction-column-reverse.png +++ b/examples/visual-docs/output/direction-column-reverse.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1b35195568ffe14b49f1deae1ab2d1bf86ccfe413b3abe3b0a115ac292415499 -size 2234 +oid sha256:d4797c881645b82104df393c914aa9608737166eb19c6e79b7e9960588ac7ab0 +size 2237 diff --git a/examples/visual-docs/output/direction-column.png b/examples/visual-docs/output/direction-column.png index 7345f40..06883cb 100644 --- a/examples/visual-docs/output/direction-column.png +++ b/examples/visual-docs/output/direction-column.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e38d70787c7c7e6f619dbf80b563f81d62ec1f916915cbb8bf285f8478644b02 +oid sha256:3a685a8a2b1060e4b2c276889e3f8935f37be1a8b4ca183387070dcd5e3b264f size 2306 diff --git a/examples/visual-docs/output/direction-row-reverse-rtl.png b/examples/visual-docs/output/direction-row-reverse-rtl.png index 789cb16..d1a167a 100644 --- a/examples/visual-docs/output/direction-row-reverse-rtl.png +++ b/examples/visual-docs/output/direction-row-reverse-rtl.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0859dfa2cb5b6504ef7692baadae400dd66520b4ede120cb1a458b4f65410b6e -size 1858 +oid sha256:e46db691449a97198a749da524c01a8665fc1c3df1cb20481903d8a6a4e9bb79 +size 1857 diff --git a/examples/visual-docs/output/direction-row-reverse.png b/examples/visual-docs/output/direction-row-reverse.png index f468711..447bc7c 100644 --- a/examples/visual-docs/output/direction-row-reverse.png +++ b/examples/visual-docs/output/direction-row-reverse.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56f359a56baed3cda3c5df1d4fbadb6527fe4b92926f81228f58c9e183f04707 +oid sha256:8f5e5c7fb84eb85f91e519050a56be5a211259bec2d2688b15ed4a9f08539156 size 1848 diff --git a/examples/visual-docs/output/direction-row-rtl.png b/examples/visual-docs/output/direction-row-rtl.png index f468711..447bc7c 100644 --- a/examples/visual-docs/output/direction-row-rtl.png +++ b/examples/visual-docs/output/direction-row-rtl.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56f359a56baed3cda3c5df1d4fbadb6527fe4b92926f81228f58c9e183f04707 +oid sha256:8f5e5c7fb84eb85f91e519050a56be5a211259bec2d2688b15ed4a9f08539156 size 1848 diff --git a/examples/visual-docs/output/direction-row.png b/examples/visual-docs/output/direction-row.png index 789cb16..d1a167a 100644 --- a/examples/visual-docs/output/direction-row.png +++ b/examples/visual-docs/output/direction-row.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0859dfa2cb5b6504ef7692baadae400dd66520b4ede120cb1a458b4f65410b6e -size 1858 +oid sha256:e46db691449a97198a749da524c01a8665fc1c3df1cb20481903d8a6a4e9bb79 +size 1857 diff --git a/examples/visual-docs/output/direction-rtl-arabic.png b/examples/visual-docs/output/direction-rtl-arabic.png index 3bc1e36..44b963a 100644 --- a/examples/visual-docs/output/direction-rtl-arabic.png +++ b/examples/visual-docs/output/direction-rtl-arabic.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:09df6a545c743618beafd6b35c1414beb741de43a48478118e1a1203f4161afa -size 5245 +oid sha256:0ee57e06e98714f0a089622ee289b97b6068da2bb556b85653b89fab3d5dd336 +size 5265 diff --git a/examples/visual-docs/output/direction-rtl-mixed.png b/examples/visual-docs/output/direction-rtl-mixed.png index eddf8f9..32e4565 100644 --- a/examples/visual-docs/output/direction-rtl-mixed.png +++ b/examples/visual-docs/output/direction-rtl-mixed.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:488c868f72561f6eabe30a2cbdce5b3dab059cc8472dba3dec7d1d0454d9f828 -size 16407 +oid sha256:2af6b7cd5768c84b93cd2e804f843a865f61bc7d482e04d0627724552ecbf0fb +size 16422 diff --git a/examples/visual-docs/output/direction-rtl-row.png b/examples/visual-docs/output/direction-rtl-row.png index 02cfb9e..7542eef 100644 --- a/examples/visual-docs/output/direction-rtl-row.png +++ b/examples/visual-docs/output/direction-rtl-row.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f37e0c556165de5af37d270181f6152a76558b177649915d1fc2b1d895df59a -size 6887 +oid sha256:dcfe2039c55a187749b768f6828fb16efcd8a05d72157b564e789d30d3c61559 +size 6888 diff --git a/examples/visual-docs/output/image-contain.png b/examples/visual-docs/output/image-contain.png index 1736dd2..4e2e83b 100644 --- a/examples/visual-docs/output/image-contain.png +++ b/examples/visual-docs/output/image-contain.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4a92bb7fb4b69e295347dc2b13ee6748fa97b2abe477ca0052d0f744240e404a -size 22685 +oid sha256:ae395bc574d06a899767b0dc3f8a8ccc025887b58078ed8238226a90945fb41b +size 22861 diff --git a/examples/visual-docs/output/image-cover.png b/examples/visual-docs/output/image-cover.png index f15a4ab..2e0d003 100644 --- a/examples/visual-docs/output/image-cover.png +++ b/examples/visual-docs/output/image-cover.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d8ad937be1902c8d1b5818d0b9830288faea4dfdcdf502c1b66590e2e493a986 -size 25578 +oid sha256:cbd118776591253cca3d313e41a2d3e152e05939803cbcddc621cc49f78a1694 +size 31398 diff --git a/examples/visual-docs/output/image-fill.png b/examples/visual-docs/output/image-fill.png index 53df377..d4ca8cb 100644 --- a/examples/visual-docs/output/image-fill.png +++ b/examples/visual-docs/output/image-fill.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:515723a62bd9b8f7ec9d7cf0052d92eaf64a9ba377ee7c1e78176c375eba3fd7 -size 23904 +oid sha256:7ab7d07c261a75e5604ea0e993500bc87ad26a9ec49b0b5399bc325c4e1cd249 +size 29553 diff --git a/examples/visual-docs/output/image-none.png b/examples/visual-docs/output/image-none.png index 90791ad..d5a84fb 100644 --- a/examples/visual-docs/output/image-none.png +++ b/examples/visual-docs/output/image-none.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:15337e2dac275848f248cb128298e3feb2f02daf5fa0ddf17cb53ba90b7124bf -size 21388 +oid sha256:06db1d86d5d882849f531378555f7f36148ee7f2380221a938a71783ea044798 +size 21360 diff --git a/examples/visual-docs/output/justify-center.png b/examples/visual-docs/output/justify-center.png index 6aa1f6d..cd5bb91 100644 --- a/examples/visual-docs/output/justify-center.png +++ b/examples/visual-docs/output/justify-center.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0febafc0bf7cc38f2161c28d73a9bddf3edb4d2c21b22194c1b040d417a927e0 +oid sha256:e95136900c67a19f39194cd84ea257a6a019087188fb89710c8e95200023bc88 size 3220 diff --git a/examples/visual-docs/output/justify-end.png b/examples/visual-docs/output/justify-end.png index ea2dd20..014670b 100644 --- a/examples/visual-docs/output/justify-end.png +++ b/examples/visual-docs/output/justify-end.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b992ccd7e0bd1fcf70407d2715408b471636b36dfdf347b91bec57b783e6517 +oid sha256:3eb1966195501b6a4a692885ccb5003d0913e1222a9f1278a74546a8a78267d9 size 3219 diff --git a/examples/visual-docs/output/justify-space-around.png b/examples/visual-docs/output/justify-space-around.png index 19d7467..377fb36 100644 --- a/examples/visual-docs/output/justify-space-around.png +++ b/examples/visual-docs/output/justify-space-around.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:58af1491a01ca616db20774c0004f6051a56d21714f770b0c1e724516ace3562 +oid sha256:eaa9614674755a0f205752f57fefbf3bc3f752975e58f53ffc08bda7e1d649ba size 3242 diff --git a/examples/visual-docs/output/justify-space-between.png b/examples/visual-docs/output/justify-space-between.png index 631e247..789cb47 100644 --- a/examples/visual-docs/output/justify-space-between.png +++ b/examples/visual-docs/output/justify-space-between.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3c95e480720572263f7b344943f5440483f19a4c510ac2e71b7e1ebf87d137bc +oid sha256:ccee84a3c8446881ba848dbec159587db117d9d7903be01f310dccb4e2dc6d7a size 3245 diff --git a/examples/visual-docs/output/justify-space-evenly.png b/examples/visual-docs/output/justify-space-evenly.png index 5eb8474..aee7ad4 100644 --- a/examples/visual-docs/output/justify-space-evenly.png +++ b/examples/visual-docs/output/justify-space-evenly.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:812a3fe15bbfeac532e01d6b71bd47e070a0cc69b75e2bba6a49ecdc6ae557ba +oid sha256:e8aa87309adc7e2a61dd20d1995b32d38c82c7f77d86913a99d0dec25a46d663 size 3243 diff --git a/examples/visual-docs/output/justify-start.png b/examples/visual-docs/output/justify-start.png index e8baa54..332df97 100644 --- a/examples/visual-docs/output/justify-start.png +++ b/examples/visual-docs/output/justify-start.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d468de63ffd90f36d5a59a8b2fa7e8d99b82b1e618f83f08d70a6fb0732f9a6 +oid sha256:ab70494de86be764ea5dd965f4d19573258aebcd64d9709b73ef4af41ef23a01 size 3219 diff --git a/examples/visual-docs/output/order-basic.png b/examples/visual-docs/output/order-basic.png index 3f03b1b..40fcd16 100644 --- a/examples/visual-docs/output/order-basic.png +++ b/examples/visual-docs/output/order-basic.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a04d3f95e33ad8c17650abcf6779ef924f35be1ddc33e54dfbd4b2ef0e867c62 -size 9876 +oid sha256:a82559fb21409d2161a1d7dce83f176222554e13e4faa0facabd9af2a71c9454 +size 9884 diff --git a/examples/visual-docs/output/order-negative.png b/examples/visual-docs/output/order-negative.png index 8985b9a..a674bf2 100644 --- a/examples/visual-docs/output/order-negative.png +++ b/examples/visual-docs/output/order-negative.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2c0e45472abf6e41ff464fb5d2dece372aa02414f7359981d329d233cd66df4 -size 10049 +oid sha256:ab39e57118b13b4b042db38d03053e5870c408e3601b070d9b787e8ff92821bd +size 10056 diff --git a/examples/visual-docs/output/position-absolute-bottom-right.png b/examples/visual-docs/output/position-absolute-bottom-right.png index 1c03cfa..959f945 100644 --- a/examples/visual-docs/output/position-absolute-bottom-right.png +++ b/examples/visual-docs/output/position-absolute-bottom-right.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4eaddfd6e583f4229b3e499c946846749057492d9d632a83b5825d5c2693bb40 -size 13575 +oid sha256:f5670c1bfd705255a6dafa961b4223eba1a1ea3ebfd814975a83a2d9deb62f87 +size 13266 diff --git a/examples/visual-docs/output/position-absolute-center.png b/examples/visual-docs/output/position-absolute-center.png index 71af407..4f479be 100644 --- a/examples/visual-docs/output/position-absolute-center.png +++ b/examples/visual-docs/output/position-absolute-center.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:25627dc401b69b4809f93c6f5ecce9199d9b7607f637113028655aa4d7a5e95a -size 12522 +oid sha256:9f5e8bb198daf82cb91f5ddcea8e3db5f280b1d4562b7c8a1a08e3da8216db6f +size 12540 diff --git a/examples/visual-docs/output/position-absolute-top-left.png b/examples/visual-docs/output/position-absolute-top-left.png index 6c5d38b..f1743d1 100644 --- a/examples/visual-docs/output/position-absolute-top-left.png +++ b/examples/visual-docs/output/position-absolute-top-left.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:44b9c94d60db723c2162be9b18658f95eb6319edf666b481618fe4637c6ab5d4 -size 12143 +oid sha256:82f9fa8cfae95dff6d028d4f36abf36e91be2aa2a4f6c06e199807f4da08e8fd +size 12009 diff --git a/examples/visual-docs/output/position-badge.png b/examples/visual-docs/output/position-badge.png index 57fc107..65411f6 100644 --- a/examples/visual-docs/output/position-badge.png +++ b/examples/visual-docs/output/position-badge.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d768d5a7e5ce29eda664c08de61a574264cf307f812fcb1794f839f7a0e9e17 -size 39051 +oid sha256:df61deb42d67cf73c5e34498da41ab04e6210e47686690f488941f9698c3abf8 +size 39072 diff --git a/examples/visual-docs/output/position-floating-label.png b/examples/visual-docs/output/position-floating-label.png index 281a705..eef4f97 100644 --- a/examples/visual-docs/output/position-floating-label.png +++ b/examples/visual-docs/output/position-floating-label.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d0026899cf62f3d4f07fd2b3aff460a8710f7500902a30f0f5f314e0d3aa6191 -size 11866 +oid sha256:63a18b24af2f63623b5a89fcf1be3a8f4d56b9cc9b56b4aaf850dd2a7dfae153 +size 11976 diff --git a/examples/visual-docs/output/position-flow-exclusion.png b/examples/visual-docs/output/position-flow-exclusion.png index 223752b..100ebab 100644 --- a/examples/visual-docs/output/position-flow-exclusion.png +++ b/examples/visual-docs/output/position-flow-exclusion.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:db6773119442b68a857467507bcc53c54cb20bc92d6ae41c73c9a1686fe3bd8b -size 16115 +oid sha256:e50c3c9bcd1bff62309925b94e8799b0f97cc906fe38563d7c06cd1a367621d2 +size 16126 diff --git a/examples/visual-docs/output/position-inset-sizing.png b/examples/visual-docs/output/position-inset-sizing.png index db90e85..5216576 100644 --- a/examples/visual-docs/output/position-inset-sizing.png +++ b/examples/visual-docs/output/position-inset-sizing.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b4f948cbc086deee2b20dc4105f85b4ceb4dc9e9a9b1023728181d6d9d6e3883 -size 23349 +oid sha256:39d18b89c075ca804e0da17c3fed3bc088d772f8ffa033681a3b53cce87e4b12 +size 23390 diff --git a/examples/visual-docs/output/position-overlay.png b/examples/visual-docs/output/position-overlay.png index 4f28f16..a3c7715 100644 --- a/examples/visual-docs/output/position-overlay.png +++ b/examples/visual-docs/output/position-overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0368299441a6c5415706f54c2c93829e5e39bd8a00d93c7b04c6ffcc8b58a692 -size 38853 +oid sha256:9bed0f22f2647c994f098d826cbf4c6eaac2ba78081ba08a9bef5a124614728c +size 38788 diff --git a/examples/visual-docs/output/position-relative-offset.png b/examples/visual-docs/output/position-relative-offset.png index d9dfd4a..ed508fe 100644 --- a/examples/visual-docs/output/position-relative-offset.png +++ b/examples/visual-docs/output/position-relative-offset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20bb122b2d34bd6e927e9cbe0ceac4301435a4e1d5af4febc91ef67388084900 -size 10111 +oid sha256:b443f6727ff2263949276a04a4d6609eab5974a1c21d1bfb108078f3fe25e352 +size 10117 diff --git a/examples/visual-docs/output/qr-colors.png b/examples/visual-docs/output/qr-colors.png index 3faa8d6..a8e246f 100644 --- a/examples/visual-docs/output/qr-colors.png +++ b/examples/visual-docs/output/qr-colors.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f8e1d57dadd93dba156a9fdf9022f9b8f05f62bb8a3cbf4647f88ffa3bef436c -size 5610 +oid sha256:c4a38b0136ac65aea26b30583270aa6ce6ca15755ce6d23886b403b29e5ac105 +size 5700 diff --git a/examples/visual-docs/output/qr-errorCorrection.png b/examples/visual-docs/output/qr-errorCorrection.png index 4b9f3ee..249c664 100644 --- a/examples/visual-docs/output/qr-errorCorrection.png +++ b/examples/visual-docs/output/qr-errorCorrection.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0e39b729f420c6d516e5e2e7d84ee8d80420766bddda08c68aa9f9759f63ede2 -size 7702 +oid sha256:9f54d3cb132434f48e0cd7ba02323da4eb038cd413241f0a261571c90fdac506 +size 7620 diff --git a/examples/visual-docs/output/separator-orientation.png b/examples/visual-docs/output/separator-orientation.png index 99dcd5e..855493b 100644 --- a/examples/visual-docs/output/separator-orientation.png +++ b/examples/visual-docs/output/separator-orientation.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:75b66a14763701de74ca9b4b182443ebf3038f5b6bfcb66f1b34aeb55e7ef21f -size 5715 +oid sha256:162e00dc67927e817fd115403a60f0dcdfb61c3454129d89ea36050ea503f8cb +size 5682 diff --git a/examples/visual-docs/output/separator-styles.png b/examples/visual-docs/output/separator-styles.png index f1551df..bd88461 100644 --- a/examples/visual-docs/output/separator-styles.png +++ b/examples/visual-docs/output/separator-styles.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22b9a2e076722d40c413a0c83d826e12497e1acfd77e779d3fa1953682106164 -size 8561 +oid sha256:39a292094a122a35307cdfbb0d5f673b1ee3bc63413d9ed556dc6c9d16ecae7b +size 8515 diff --git a/examples/visual-docs/output/separator-thickness.png b/examples/visual-docs/output/separator-thickness.png index 5e2d2b3..d53ce19 100644 --- a/examples/visual-docs/output/separator-thickness.png +++ b/examples/visual-docs/output/separator-thickness.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8703118b633bda681fec81c5251ef0722fc51d8aa90c6015219f34ad2425372 -size 6039 +oid sha256:3c1460f556ec8de3e0f461b92e7c202355d086b4346efda11b21ad183fe32c7b +size 6031 diff --git a/examples/visual-docs/output/text-align-start-rtl.png b/examples/visual-docs/output/text-align-start-rtl.png index 8daa5b8..853efb4 100644 --- a/examples/visual-docs/output/text-align-start-rtl.png +++ b/examples/visual-docs/output/text-align-start-rtl.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:44d69f692c4e6c8e815dcaea64ac338893e5dd584fa040c0fd589f6b9050294c -size 15292 +oid sha256:6a497a4f34aea82959b31a99c95bf3bd8cd7bbaa3fc1e970fd078e3ba5159bef +size 15298 diff --git a/examples/visual-docs/output/text-align.png b/examples/visual-docs/output/text-align.png index 93b2184..675058d 100644 --- a/examples/visual-docs/output/text-align.png +++ b/examples/visual-docs/output/text-align.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7a7a2aec3b1ad3983dd43210f5cc6d216e3f16298fa9f3943dffb40aa7c0a363 -size 13099 +oid sha256:a534c722420658e726fedb2b2195ebfc901fba801deafedb073f7da7cb830a1c +size 13111 diff --git a/examples/visual-docs/output/text-lineHeight.png b/examples/visual-docs/output/text-lineHeight.png index f7f9d5f..5f1196c 100644 --- a/examples/visual-docs/output/text-lineHeight.png +++ b/examples/visual-docs/output/text-lineHeight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:39aa9228c1dc3735e9ff24d11fc02671ecae2ed1f1ad7adb9eaf36a36393a2bc -size 22539 +oid sha256:865fc4869624087b5c47a69dded1a93148af87c13afec7d62f58f191c98dabfd +size 22666 diff --git a/examples/visual-docs/output/text-maxLines.png b/examples/visual-docs/output/text-maxLines.png index cbb09d8..e5f681a 100644 --- a/examples/visual-docs/output/text-maxLines.png +++ b/examples/visual-docs/output/text-maxLines.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f6457e164a22145ff8ad5019bd20767c3d99276c75b492b5b9b579847bc2b638 -size 22087 +oid sha256:fb809750c98c94595da9e73c3b30d91982ec5e89cbbfaf6efd9e0927d8914565 +size 22356 diff --git a/examples/visual-docs/output/text-wrap.png b/examples/visual-docs/output/text-wrap.png index 4bf1acd..d9bced7 100644 --- a/examples/visual-docs/output/text-wrap.png +++ b/examples/visual-docs/output/text-wrap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b53e0ac55934b46247a1f60fbe02d5aa67255978be71b46adaaf2ae528bc1c7a -size 17673 +oid sha256:37d90bd421f6aa893c72b21fadf42e05fd05fb3dfc4cf8b7e8b675b68c41a123 +size 19281 diff --git a/examples/visual-docs/output/wrap-nowrap.png b/examples/visual-docs/output/wrap-nowrap.png index 2a87a42..6602335 100644 --- a/examples/visual-docs/output/wrap-nowrap.png +++ b/examples/visual-docs/output/wrap-nowrap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c1f29a35906d66317dc4a29f1285c7448e680fa09b1864469016d52b3706a53 -size 2908 +oid sha256:c9fa9ce984498e04b3bb4759c835f44101238f17f42c4d137845e401836a95eb +size 2909 diff --git a/examples/visual-docs/output/wrap-wrap-reverse.png b/examples/visual-docs/output/wrap-wrap-reverse.png index 1a10676..c2a69a1 100644 --- a/examples/visual-docs/output/wrap-wrap-reverse.png +++ b/examples/visual-docs/output/wrap-wrap-reverse.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:05fb50d34f1b3a1467c7d3e09676e87cd34b325d64060b2be33d432d27147105 -size 3195 +oid sha256:e33db913225bff8f53e3b8ee936bbc472bf6c60505434f86125cb5f30cb845ab +size 3194 diff --git a/examples/visual-docs/output/wrap-wrap.png b/examples/visual-docs/output/wrap-wrap.png index dffecba..2f87105 100644 --- a/examples/visual-docs/output/wrap-wrap.png +++ b/examples/visual-docs/output/wrap-wrap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:46aa67f7c866a25b47f4ae6bbbdd0c5ee319a2e32ece633879c1cb10f4535b63 -size 3199 +oid sha256:7490bcd3d1a749ce1c7653a33a46d99d1be525967e364166f8ea2bf9f998d08e +size 3198 diff --git a/examples/visual-docs/position/absolute-bottom-right.yaml b/examples/visual-docs/position/absolute-bottom-right.yaml index ba2f4ef..ae53c56 100644 --- a/examples/visual-docs/position/absolute-bottom-right.yaml +++ b/examples/visual-docs/position/absolute-bottom-right.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -20,7 +19,7 @@ layout: # Title - type: text content: "position: absolute (bottom/right)" - font: bold + fontWeight: bold size: 16px color: "#333333" @@ -49,7 +48,7 @@ layout: - type: text content: "Normal flow" color: "#ffffff" - font: bold + fontWeight: bold size: 14px - type: flex @@ -63,7 +62,7 @@ layout: - type: text content: "Normal flow" color: "#ffffff" - font: bold + fontWeight: bold size: 14px # Absolute positioned box @@ -81,6 +80,6 @@ layout: - type: text content: "Absolute" color: "#ffffff" - font: bold + fontWeight: bold size: 11px align: center diff --git a/examples/visual-docs/position/absolute-center.yaml b/examples/visual-docs/position/absolute-center.yaml index f61fc2c..6fb99b8 100644 --- a/examples/visual-docs/position/absolute-center.yaml +++ b/examples/visual-docs/position/absolute-center.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -20,7 +19,7 @@ layout: # Title - type: text content: "position: absolute (centered)" - font: bold + fontWeight: bold size: 16px color: "#333333" @@ -70,6 +69,6 @@ layout: - type: text content: "Centered" color: "#ffffff" - font: bold + fontWeight: bold size: 16px align: center diff --git a/examples/visual-docs/position/absolute-top-left.yaml b/examples/visual-docs/position/absolute-top-left.yaml index be6806f..253f52a 100644 --- a/examples/visual-docs/position/absolute-top-left.yaml +++ b/examples/visual-docs/position/absolute-top-left.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -20,7 +19,7 @@ layout: # Title - type: text content: "position: absolute (top/left)" - font: bold + fontWeight: bold size: 16px color: "#333333" @@ -49,7 +48,7 @@ layout: - type: text content: "Normal flow" color: "#ffffff" - font: bold + fontWeight: bold size: 14px - type: flex @@ -63,7 +62,7 @@ layout: - type: text content: "Normal flow" color: "#ffffff" - font: bold + fontWeight: bold size: 14px # Absolute positioned box @@ -81,6 +80,6 @@ layout: - type: text content: "Absolute" color: "#ffffff" - font: bold + fontWeight: bold size: 11px align: center diff --git a/examples/visual-docs/position/badge.yaml b/examples/visual-docs/position/badge.yaml index a3000ba..e1cfbff 100644 --- a/examples/visual-docs/position/badge.yaml +++ b/examples/visual-docs/position/badge.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -20,7 +19,7 @@ layout: # Title - type: text content: "Badge Pattern" - font: bold + fontWeight: bold size: 16px color: "#333333" @@ -47,14 +46,14 @@ layout: # Product name - type: text content: "Premium Widget" - font: bold + fontWeight: bold size: 16px color: "#333333" # Price - type: text content: "$49.99" - font: bold + fontWeight: bold size: 18px color: "#2ecc71" diff --git a/examples/visual-docs/position/floating-label.yaml b/examples/visual-docs/position/floating-label.yaml index c776280..15a9a0e 100644 --- a/examples/visual-docs/position/floating-label.yaml +++ b/examples/visual-docs/position/floating-label.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -20,7 +19,7 @@ layout: # Title - type: text content: "Floating Label Pattern" - font: bold + fontWeight: bold size: 16px color: "#333333" diff --git a/examples/visual-docs/position/flow-exclusion.yaml b/examples/visual-docs/position/flow-exclusion.yaml index afe9ea6..89a4120 100644 --- a/examples/visual-docs/position/flow-exclusion.yaml +++ b/examples/visual-docs/position/flow-exclusion.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -20,7 +19,7 @@ layout: # Title - type: text content: "Flow Exclusion" - font: bold + fontWeight: bold size: 16px color: "#333333" @@ -53,7 +52,7 @@ layout: - type: text content: "A (static)" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center @@ -70,7 +69,7 @@ layout: - type: text content: "B (absolute)" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center @@ -85,7 +84,7 @@ layout: - type: text content: "C (static)" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center @@ -100,6 +99,6 @@ layout: - type: text content: "D (static)" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center diff --git a/examples/visual-docs/position/inset-sizing.yaml b/examples/visual-docs/position/inset-sizing.yaml index 7365b55..ff18a00 100644 --- a/examples/visual-docs/position/inset-sizing.yaml +++ b/examples/visual-docs/position/inset-sizing.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -20,7 +19,7 @@ layout: # Title - type: text content: "Inset-Based Sizing" - font: bold + fontWeight: bold size: 16px color: "#333333" @@ -53,7 +52,7 @@ layout: - type: text content: "Stretched by insets" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center @@ -81,6 +80,6 @@ layout: - type: text content: "Horizontal stretch" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center diff --git a/examples/visual-docs/position/overlay.yaml b/examples/visual-docs/position/overlay.yaml index f8aec69..8fc5728 100644 --- a/examples/visual-docs/position/overlay.yaml +++ b/examples/visual-docs/position/overlay.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -20,7 +19,7 @@ layout: # Title - type: text content: "Text Overlay Pattern" - font: bold + fontWeight: bold size: 16px color: "#333333" @@ -54,7 +53,7 @@ layout: # Article title - type: text content: "Article Title" - font: bold + fontWeight: bold size: 16px color: "#ffffff" diff --git a/examples/visual-docs/position/relative-offset.yaml b/examples/visual-docs/position/relative-offset.yaml index bfafa7c..ab4bba1 100644 --- a/examples/visual-docs/position/relative-offset.yaml +++ b/examples/visual-docs/position/relative-offset.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -20,7 +19,7 @@ layout: # Title - type: text content: "position: relative" - font: bold + fontWeight: bold size: 16px color: "#333333" @@ -49,7 +48,7 @@ layout: - type: text content: "Box 1" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center @@ -67,7 +66,7 @@ layout: - type: text content: "Box 2" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center @@ -82,6 +81,6 @@ layout: - type: text content: "Box 3" color: "#ffffff" - font: bold + fontWeight: bold size: 14px align: center diff --git a/examples/visual-docs/text/align-start-rtl.yaml b/examples/visual-docs/text/align-start-rtl.yaml index bf54586..cb946fc 100644 --- a/examples/visual-docs/text/align-start-rtl.yaml +++ b/examples/visual-docs/text/align-start-rtl.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -17,7 +16,7 @@ layout: content: "align: start (RTL = right-aligned)" align: start padding: "12" - font: bold + fontWeight: bold size: 14px color: "#333333" - type: separator @@ -27,7 +26,7 @@ layout: content: "align: end (RTL = left-aligned)" align: end padding: "12" - font: bold + fontWeight: bold size: 14px color: "#333333" - type: separator diff --git a/examples/visual-docs/wrap/nowrap.yaml b/examples/visual-docs/wrap/nowrap.yaml index 79d8abb..31cc27e 100644 --- a/examples/visual-docs/wrap/nowrap.yaml +++ b/examples/visual-docs/wrap/nowrap.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -29,7 +28,7 @@ layout: - type: text content: "1" color: "#ffffff" - font: bold + fontWeight: bold size: 20px align: center - type: flex @@ -43,7 +42,7 @@ layout: - type: text content: "2" color: "#ffffff" - font: bold + fontWeight: bold size: 20px align: center - type: flex @@ -57,7 +56,7 @@ layout: - type: text content: "3" color: "#ffffff" - font: bold + fontWeight: bold size: 20px align: center - type: flex @@ -71,7 +70,7 @@ layout: - type: text content: "4" color: "#ffffff" - font: bold + fontWeight: bold size: 20px align: center - type: flex @@ -85,7 +84,7 @@ layout: - type: text content: "5" color: "#ffffff" - font: bold + fontWeight: bold size: 20px align: center - type: flex @@ -99,6 +98,6 @@ layout: - type: text content: "6" color: "#ffffff" - font: bold + fontWeight: bold size: 20px align: center diff --git a/examples/visual-docs/wrap/wrap-reverse.yaml b/examples/visual-docs/wrap/wrap-reverse.yaml index fce3751..d002880 100644 --- a/examples/visual-docs/wrap/wrap-reverse.yaml +++ b/examples/visual-docs/wrap/wrap-reverse.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -29,7 +28,7 @@ layout: - type: text content: "1" color: "#ffffff" - font: bold + fontWeight: bold size: 20px align: center - type: flex @@ -43,7 +42,7 @@ layout: - type: text content: "2" color: "#ffffff" - font: bold + fontWeight: bold size: 20px align: center - type: flex @@ -57,7 +56,7 @@ layout: - type: text content: "3" color: "#ffffff" - font: bold + fontWeight: bold size: 20px align: center - type: flex @@ -71,7 +70,7 @@ layout: - type: text content: "4" color: "#ffffff" - font: bold + fontWeight: bold size: 20px align: center - type: flex @@ -85,7 +84,7 @@ layout: - type: text content: "5" color: "#ffffff" - font: bold + fontWeight: bold size: 20px align: center - type: flex @@ -99,6 +98,6 @@ layout: - type: text content: "6" color: "#ffffff" - font: bold + fontWeight: bold size: 20px align: center diff --git a/examples/visual-docs/wrap/wrap.yaml b/examples/visual-docs/wrap/wrap.yaml index 5f50460..8f344c7 100644 --- a/examples/visual-docs/wrap/wrap.yaml +++ b/examples/visual-docs/wrap/wrap.yaml @@ -4,7 +4,6 @@ template: fonts: default: "../../assets/fonts/Inter-Regular.ttf" - bold: "../../assets/fonts/Inter-Bold.ttf" canvas: fixed: width @@ -29,7 +28,7 @@ layout: - type: text content: "1" color: "#ffffff" - font: bold + fontWeight: bold size: 20px align: center - type: flex @@ -43,7 +42,7 @@ layout: - type: text content: "2" color: "#ffffff" - font: bold + fontWeight: bold size: 20px align: center - type: flex @@ -57,7 +56,7 @@ layout: - type: text content: "3" color: "#ffffff" - font: bold + fontWeight: bold size: 20px align: center - type: flex @@ -71,7 +70,7 @@ layout: - type: text content: "4" color: "#ffffff" - font: bold + fontWeight: bold size: 20px align: center - type: flex @@ -85,7 +84,7 @@ layout: - type: text content: "5" color: "#ffffff" - font: bold + fontWeight: bold size: 20px align: center - type: flex @@ -99,6 +98,6 @@ layout: - type: text content: "6" color: "#ffffff" - font: bold + fontWeight: bold size: 20px align: center diff --git a/examples/visual-effects.yaml b/examples/visual-effects.yaml index 336cefe..5f04539 100644 --- a/examples/visual-effects.yaml +++ b/examples/visual-effects.yaml @@ -7,8 +7,6 @@ template: fonts: default: "assets/fonts/Inter-Regular.ttf" - bold: "assets/fonts/Inter-Bold.ttf" - semibold: "assets/fonts/Inter-SemiBold.ttf" canvas: fixed: width @@ -24,7 +22,7 @@ layout: # Title - type: text content: "Visual Effects" - font: bold + fontWeight: bold size: 1.3em color: "#1a1a1a" align: center @@ -39,7 +37,7 @@ layout: children: - type: text content: "Box Shadow" - font: semibold + fontWeight: semi-bold size: 1em color: "#333333" - type: text @@ -57,7 +55,7 @@ layout: children: - type: text content: "Deeper Shadow" - font: semibold + fontWeight: semi-bold size: 1em color: "#333333" - type: text @@ -74,7 +72,7 @@ layout: children: - type: text content: "Linear Gradient" - font: bold + fontWeight: bold size: 1.1em color: "#ffffff" - type: text @@ -91,7 +89,7 @@ layout: children: - type: text content: "Radial Gradient" - font: bold + fontWeight: bold size: 1.1em color: "#7c3aed" - type: text @@ -105,7 +103,7 @@ layout: children: - type: text content: "Opacity Levels" - font: semibold + fontWeight: semi-bold size: 0.95em color: "#333333" @@ -125,7 +123,7 @@ layout: content: "100%" size: 0.8em color: "#ffffff" - font: semibold + fontWeight: semi-bold align: center - type: flex @@ -140,7 +138,7 @@ layout: content: "70%" size: 0.8em color: "#ffffff" - font: semibold + fontWeight: semi-bold align: center - type: flex @@ -155,7 +153,7 @@ layout: content: "40%" size: 0.8em color: "#ffffff" - font: semibold + fontWeight: semi-bold align: center - type: flex @@ -170,7 +168,7 @@ layout: content: "15%" size: 0.8em color: "#ffffff" - font: semibold + fontWeight: semi-bold align: center # Combined: gradient + shadow + opacity @@ -184,7 +182,7 @@ layout: children: - type: text content: "Combined Effects" - font: bold + fontWeight: bold size: 1em color: "#ffffff" - type: text diff --git a/llms-full.txt b/llms-full.txt index 84847b2..f12f1d1 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -77,6 +77,9 @@ src/FlexRender.Svg.Render/ # SVG output renderer (-> Core) SvgBuilder.cs # Builder for SVG renderer configuration src/FlexRender.Svg/ # SVG backend meta-package (renderer + providers) +src/FlexRender.Content.Markdown/ # Markdown content parser (-> Core + Markdig) +src/FlexRender.Content.Html/ # HTML content parser (-> Core + HtmlAgilityPack) + src/FlexRender.DependencyInjection/ # Microsoft.Extensions.DI integration ServiceCollectionExtensions.cs # AddFlexRender() extension method @@ -135,6 +138,8 @@ FlexRender.Yaml FlexRender.Http FlexRender.Skia.Render FlexRender.ImageSharp. | FlexRender.SvgElement | All renderers | -- | | FlexRender.Svg.Render | Core | (none) | | FlexRender.Svg | Svg.Render + providers | (none) | +| FlexRender.Content.Markdown | Core | Markdig 1.1.1 | +| FlexRender.Content.Html | Core | HtmlAgilityPack 1.12.4 | | FlexRender.MetaPackage | All | -- | | flexrender-cli | All | System.CommandLine | @@ -182,9 +187,9 @@ template: # Required: template metadata version: 1 # Template version (int) # culture: "ru-RU" # Optional: culture for number/date formatting -fonts: # Optional: font definitions (key = reference name, value = path) - default: "assets/fonts/Inter-Regular.ttf" # File path, base64, embedded://, or http:// - bold: "assets/fonts/Inter-Bold.ttf" +fonts: # Optional: list format (recommended) or dictionary format + - "assets/fonts/Inter-Regular.ttf" # First unnamed = default/main + - "assets/fonts/Inter-Bold.ttf" # File path, embedded://, or http:// canvas: # Required: canvas configuration fixed: width # Which dimension is fixed (width|height|both|none) @@ -422,6 +427,194 @@ The `order` property controls the visual display order of flex items. Items are - Arabic font support: use an Arabic-capable font (e.g., Noto Sans Arabic) in the `fonts` section for Arabic text rendering - HarfBuzz text shaping: optional `FlexRender.HarfBuzz` package provides proper Arabic/Hebrew glyph shaping via `.WithHarfBuzz()` on the Skia builder +## Fonts + +Fonts are registered in the `fonts:` section of the YAML template. Two formats are supported. Supported file types: `.ttf` and `.otf`. Font sources can be local file paths, `embedded://` resources, or `http://` URLs. System fonts are used when no custom font is registered. + +### Font Registration -- Dictionary Format (Legacy) + +Key-value pairs where the key is a reference name used in `font:` properties: + +```yaml +fonts: + default: "assets/fonts/Inter-Regular.ttf" + heading: "assets/fonts/Roboto-Regular.ttf" + icon: "embedded://MyApp.Fonts.icons.ttf" + remote: "https://example.com/font.ttf" +``` + +### Font Registration -- List Format (Recommended) + +An array of font entries. Simple strings and objects with `path`/`name`/`fallback` can be mixed: + +```yaml +fonts: + # Simple strings -- first unnamed font automatically becomes "default" (and "main") + - "assets/fonts/Inter-Regular.ttf" + - "assets/fonts/Inter-Bold.ttf" + - "assets/fonts/Inter-Italic.ttf" + + # With optional name and fallback + - path: "assets/fonts/Roboto-Regular.ttf" + name: heading + fallback: "Arial" +``` + +**Rules:** +- The first unnamed font automatically becomes `default` (and `main`) +- Fonts can be mixed: simple strings and objects with `path`/`name`/`fallback` +- Named fonts are referenced via `font:` on elements (e.g., `font: heading`) + +### Font Properties on Text Elements + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `font` | string | `main` | Reference to a registered font name | +| `fontFamily` | string | (empty) | CSS-like font family name -- searches registered fonts by FamilyName metadata, then system fonts | +| `fontWeight` | string/number | `normal` | Font weight (see table below), or numeric 100-900 | +| `fontStyle` | string | `normal` | Font style: `normal`, `italic`, `oblique` | + +### Font Resolution Priority + +``` +font (registered name) > fontFamily (family name) > fallback (default) +``` + +- If `font` is set to a non-default value, resolve by registered name +- If `fontFamily` is set, search registered fonts by FamilyName metadata, then system fonts +- Otherwise, use the default font (`main`) + +For weight/style variants: **automatic sibling discovery** scans the same directory as the base font file for `.ttf`/`.otf` files with matching family name and weight/style. + +### Automatic Sibling Font Discovery + +Only the regular/default font file needs to be registered per family. When a text element uses `fontWeight` or `fontStyle`, FlexRender automatically scans the same directory as the registered font for sibling files with a matching family name and weight/style (within +/-100 units for weight, case-insensitive). + +**Convention:** Place all weight/style variants of a font family in the same directory: + +``` +assets/fonts/ + Inter-Regular.ttf # weight 400, upright + Inter-Bold.ttf # weight 700, upright + Inter-SemiBold.ttf # weight 600, upright + Inter-Italic.ttf # weight 400, italic + Inter-BoldItalic.ttf # weight 700, italic +``` + +Then in templates: + +```yaml +- type: text + content: "Bold text" + fontWeight: bold # uses Inter-Bold.ttf automatically + +- type: text + content: "Light italic" + fontWeight: light + fontStyle: italic # discovers Inter-LightItalic.ttf if present +``` + +For multiple font families, register each family's regular font and use `font:` to select: + +```yaml +fonts: + - "assets/fonts/Inter-Regular.ttf" + - path: "assets/fonts/Roboto-Regular.ttf" + name: heading +``` + +```yaml +- type: text + font: heading + fontWeight: bold # discovers Roboto-Bold.ttf in the same directory + content: "Bold Heading" +``` + +### Table Header Font Properties + +Tables support font properties on the header row: + +| Property | Aliases | Description | +|----------|---------|-------------| +| `headerFont` | `header-font` | Font name for header cells | +| `headerFontWeight` | `header-fontWeight` | Font weight for headers | +| `headerFontStyle` | `header-fontStyle` | Font style for headers (normal, italic, oblique) | +| `headerFontFamily` | `header-fontFamily` | CSS-like font family for headers | + +### fontWeight Values + +| Name | Numeric | +|------|---------| +| `thin` | 100 | +| `extra-light` | 200 | +| `light` | 300 | +| `normal` (default) | 400 | +| `medium` | 500 | +| `semi-bold` | 600 | +| `bold` | 700 | +| `extra-bold` | 800 | +| `black` | 900 | + +Numeric values (100-900) are also accepted: `fontWeight: 600`. + +### fontStyle Values + +`normal` (default), `italic`, `oblique`. + +### Examples + +**Minimal (system fonts only, no registration):** + +```yaml +layout: + - type: text + content: "Hello" + fontFamily: "Arial" + fontWeight: bold +``` + +**List registration with fontWeight/fontStyle:** + +```yaml +fonts: + - "assets/fonts/Inter-Regular.ttf" + - "assets/fonts/Inter-Bold.ttf" + - "assets/fonts/Inter-Italic.ttf" + +layout: + - type: text + content: "Bold text" + fontWeight: bold + - type: text + content: "Italic text" + fontStyle: italic +``` + +**Mixed: named + unnamed, font + fontFamily:** + +```yaml +fonts: + - "assets/fonts/Inter-Regular.ttf" + - path: "assets/fonts/NotoSansArabic-Regular.ttf" + name: arabic + +layout: + - type: text + content: "Inter bold" + fontWeight: bold + - type: text + content: "System Georgia" + fontFamily: "Georgia" + - type: text + content: "Arabic" + font: arabic +``` + +### Limitations + +- **Variable fonts are NOT supported** -- SkiaSharp 3.x does not expose an API for font variation axes. Use separate static font files per weight/style instead. +- Sibling discovery relies on font file metadata (family name, weight, slant). If files use non-standard naming, register them explicitly in the `fonts:` section. + ## Flex-Item Properties All leaf elements (text, image, qr, barcode, separator) and flex containers (when nested) have these flex-item properties: @@ -442,6 +635,9 @@ All leaf elements (text, image, qr, barcode, separator) and flex containers (whe |----------|------|---------|-------------| | content | string | "" | Text content, may contain `{{variable}}` expressions | | font | string | "main" | Font reference name from `fonts` section | +| fontFamily | string | "" | CSS-like font family name -- searches registered fonts by FamilyName, then system fonts | +| fontWeight | string? | null | Font weight: `thin` (100), `extra-light` (200), `light` (300), `normal` (400), `medium` (500), `semi-bold` (600), `bold` (700), `extra-bold` (800), `black` (900), or numeric 100-900 | +| fontStyle | string? | null | Font style: `normal`, `italic`, `oblique` | | size | string | "1em" | Font size (px, em, %) | | color | string | "#000000" | Text color in hex format | | align | TextAlign | Left | Text alignment: `left`, `center`, `right`, `start` (logical), `end` (logical) | @@ -546,6 +742,9 @@ Tabular data with configurable columns, optional header, and support for dynamic | columns | TableColumn[] | -- | Column definitions (required, at least one) | | rows | TableRow[] | [] | Static rows (alternative to array) | | headerFont | string? | null | Font for header row | +| headerFontWeight | string? | null | Font weight for header row | +| headerFontStyle | string? | null | Font style for header row (normal, italic, oblique) | +| headerFontFamily | string? | null | CSS-like font family for header row | | headerColor | string? | null | Text color for header row | | headerSize | string? | null | Font size for header row | | headerBackground | string? | null | Background color for header row | @@ -647,6 +846,24 @@ Renders SVG vector graphics. Supports external files (`src`) or inline markup (` height: 48 ``` +## Element Type: content (ContentElement) + +Embeds formatted text (Markdown or HTML) that is parsed into an AST subtree at render time. Requires a content parser to be registered. + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| source | string | "" | Formatted text content, may contain `{{variable}}` expressions | +| format | string | "markdown" | Content format: `markdown` or `html` -- must match a registered parser | + +```yaml +- type: content + source: "{{body}}" # text with formatting (from data) + format: markdown # or "html" -- must match registered parser +``` + +Requires content parser registration: `.WithMarkdown()` (Markdig) or `.WithHtml()` (HtmlAgilityPack). +Converts formatted text into FlexRender AST elements at render time (bold -> FontWeight.Bold, italic -> FontStyle.Italic, headings, lists, etc.). + ## Color Format Colors are specified in hex format: @@ -874,6 +1091,17 @@ Conditional rendering based on data values. Supports 13 operators. content: "Unknown" ``` +### Content Embedding + +```yaml +- type: content + source: "{{body}}" # text with formatting (from data) + format: markdown # or "html" -- must match registered parser +``` + +Requires content parser registration: `.WithMarkdown()` (Markdig) or `.WithHtml()` (HtmlAgilityPack). +Converts formatted text into FlexRender AST elements at render time (bold -> FontWeight.Bold, italic -> FontStyle.Italic, headings, lists, etc.). + ### Processing Layers 1. **AST-Level** (`TemplateExpander`): Expands `type: each` and `type: if` elements into concrete elements based on data. Enables template caching — parse once, expand many times with different data. @@ -914,6 +1142,8 @@ var render = new FlexRenderBuilder() .WithEmbeddedLoader(typeof(Program).Assembly) // Embedded resources .WithBasePath("./templates") // Base path for files .WithLimits(limits => limits.MaxRenderDepth = 200) + .WithMarkdown() // optional: Markdown content parsing + .WithHtml() // optional: HTML content parsing .WithSkia(skia => skia .WithQr() // QR code support .WithBarcode()) // Barcode support @@ -989,6 +1219,8 @@ byte[] png = await render.Render(_templates["receipt"], data); | `WithEmbeddedLoader(Assembly)` | Add embedded resource loader | | `WithHttpLoader(HttpClient?)` | Add HTTP resource loader | | `WithFilter(ITemplateFilter)` | Register a custom template filter for inline expressions (works alongside built-in filters) | +| `WithMarkdown()` | Enable Markdown content parsing (requires FlexRender.Content.Markdown) | +| `WithHtml()` | Enable HTML content parsing (requires FlexRender.Content.Html) | | `WithoutDefaultLoaders()` | Remove File and Base64 loaders | | `WithoutDefaultFilters()` | Remove all 8 built-in filters (enabled by default), leaving only custom-registered filters | @@ -1269,6 +1501,8 @@ Reference these YAML files for real-world template patterns. Each file is a comp | `examples/showcase.yaml` | Full feature showcase | All element types, 14 sections, HTTP images | | `examples/showcase-capabilities.yaml` | Renderer comparison | Conditional rendering per backend (`type: if` with `renderer`) | | `examples/showcase-all-features.yaml` | All features showcase | Every supported feature in one template | +| `examples/markdown-content.yaml` | Markdown content demo | `type: content` with `format: markdown`, dynamic body | +| `examples/html-content.yaml` | HTML content demo | `type: content` with `format: html`, inline styles | ### Per-Feature Examples (examples/visual-docs/) diff --git a/llms.txt b/llms.txt index fa0607e..f445665 100644 --- a/llms.txt +++ b/llms.txt @@ -43,6 +43,8 @@ src/FlexRender.ImageSharp.Render/ # ImageSharp renderer (-> Core + SixLabors.Ima src/FlexRender.ImageSharp/ # ImageSharp backend meta-package (renderer + providers) src/FlexRender.Svg.Render/ # SVG output renderer (-> Core) src/FlexRender.Svg/ # SVG backend meta-package (renderer + providers) +src/FlexRender.Content.Markdown/ # Markdown content parser (-> Core + Markdig) +src/FlexRender.Content.Html/ # HTML content parser (-> Core + HtmlAgilityPack) src/FlexRender.DependencyInjection/ # Microsoft.Extensions.DI integration src/FlexRender.MetaPackage/ # Meta-package (core + all backends + DI) @@ -72,16 +74,17 @@ Ten element types, each a sealed class extending `TemplateElement`: | Type | Key Properties | |------|---------------| -| **text** | content, font, size, color, align (left/center/right/start/end), wrap, overflow (ellipsis/clip/visible), maxLines, lineHeight | +| **text** | content, font, fontFamily, fontWeight (thin/light/normal/medium/semi-bold/bold/extra-bold/black or 100-900), fontStyle (normal/italic/oblique), size, color, align (left/center/right/start/end), wrap, overflow (ellipsis/clip/visible), maxLines, lineHeight | | **flex** | direction (row/column), wrap, gap, rowGap, columnGap, justify, align, alignContent, overflow, children | | **image** | src, width, height, fit (fill/contain/cover/none) | | **qr** | data, size, errorCorrection (L/M/Q/H), foreground -- requires `FlexRender.QrCode.*` (Skia/Svg/ImageSharp) | | **barcode** | data, format (code128/code39/ean13/ean8/upc), width, height, showText, foreground -- requires `FlexRender.Barcode.*` (Skia/Svg/ImageSharp) | | **svg** | src, content, width, height, fit (fill/contain/cover/none) -- requires `FlexRender.SvgElement.*` (Skia/Svg) | | **separator** | orientation (horizontal/vertical), style (dotted/dashed/solid), thickness, color | -| **table** | array, as, columns (key/label/width/grow/align/format), rows, headerFont/headerColor/headerSize/headerBackground -- expands to flex tree | +| **table** | array, as, columns (key/label/width/grow/align/format), rows, headerFont/headerFontWeight/headerFontStyle/headerFontFamily/headerColor/headerSize/headerBackground -- expands to flex tree | | **each** | array, as, children -- iteration over data arrays | | **if** | condition, equals/notEquals, then, elseIf, else -- conditional rendering | +| **content** | source, format -- embeds formatted text (Markdown/HTML) parsed into AST subtree at render time. Requires content parser: `.WithMarkdown()` or `.WithHtml()` | All elements share common properties from `TemplateElement`: padding, margin, background, opacity, box-shadow, rotate, display (flex/none), position, top, right, bottom, left, aspectRatio, minWidth, maxWidth, minHeight, maxHeight, text-direction, border, border-top, border-right, border-bottom, border-left, border-width, border-color, border-style, border-radius. @@ -136,6 +139,31 @@ All elements (except flex containers) have flex-item properties: grow, shrink, b - HarfBuzz text shaping: optional `FlexRender.HarfBuzz` package provides proper Arabic/Hebrew glyph shaping via `.WithHarfBuzz()` on the Skia builder - Arabic font support: use an Arabic-capable font (e.g., Noto Sans Arabic) in the `fonts` section for Arabic text rendering. Combine with `text-direction: rtl` and HarfBuzz for full Arabic support +## Fonts + +**Two registration formats** in the `fonts:` YAML section: + +- **Dictionary format** (legacy): `fonts: { default: "path.ttf", heading: "path.ttf" }` +- **List format** (recommended): `fonts: [ "path.ttf", { path: "path.ttf", name: heading, fallback: "Arial" } ]` + - First unnamed font automatically becomes `default`/`main` + - Simple strings and objects with `path`/`name`/`fallback` can be mixed + +**Text element font properties:** + +- `font`: registered font name (default: `main`) +- `fontFamily`: CSS-like family name -- searches registered fonts by FamilyName, then system fonts +- `fontWeight`: `thin`(100), `extra-light`(200), `light`(300), `normal`(400), `medium`(500), `semi-bold`(600), `bold`(700), `extra-bold`(800), `black`(900), or numeric 100-900 +- `fontStyle`: `normal`, `italic`, `oblique` + +**Resolution priority:** `font` (registered name) > `fontFamily` (family name) > fallback (default) + +**Automatic sibling discovery:** when `fontWeight`/`fontStyle` is used, scans the font file's directory for `.ttf`/`.otf` siblings with matching family name and weight/style (within +/-100 units) + +**Table header font properties:** `headerFont`, `headerFontWeight`, `headerFontStyle`, `headerFontFamily` (aliases: `header-font`, `header-fontWeight`, `header-fontStyle`, `header-fontFamily`) + +- Variable fonts are NOT supported (SkiaSharp 3.x limitation) -- use separate static font files per weight +- System fonts are used when no custom font is registered + ## Non-Uniform Padding Padding and margin support CSS-like shorthand with 1 to 4 space-separated values: @@ -235,6 +263,16 @@ Operators: truthy (no key), `equals`, `notEquals`, `in`, `notIn`, `contains`, `g else: [...] ``` +### Content Embedding +```yaml +- type: content + source: "{{body}}" # text with formatting (from data) + format: markdown # or "html" -- must match registered parser +``` + +Requires content parser registration: `.WithMarkdown()` (Markdig) or `.WithHtml()` (HtmlAgilityPack). +Converts formatted text into FlexRender AST elements at render time (bold → FontWeight.Bold, italic → FontStyle.Italic, headings, lists, etc.). + ## Supported Units - `px` -- pixels (default when no unit specified) @@ -250,9 +288,9 @@ template: version: 1 culture: "ru-RU" # optional: culture for number/date formatting -fonts: - default: "assets/fonts/Inter-Regular.ttf" - bold: "assets/fonts/Inter-Bold.ttf" +fonts: # list format (recommended) or dictionary format + - "assets/fonts/Inter-Regular.ttf" # first unnamed = default/main + - "assets/fonts/Inter-Bold.ttf" canvas: fixed: width # width | height | both | none @@ -313,6 +351,8 @@ var render = new FlexRenderBuilder() }) .WithBasePath("./templates") .WithLimits(limits => limits.MaxRenderDepth = 200) + .WithMarkdown() // optional: Markdown content parsing + .WithHtml() // optional: HTML content parsing .WithSkia(skia => skia .WithQr() .WithBarcode() @@ -440,6 +480,8 @@ Reference these YAML files for real-world template patterns. Each file is a comp | `examples/showcase.yaml` | Full feature showcase | All element types, 14 sections, HTTP images | | `examples/showcase-capabilities.yaml` | Renderer comparison | Conditional rendering per backend (`type: if` with `renderer`) | | `examples/showcase-all-features.yaml` | All features showcase | Every supported feature in one template | +| `examples/markdown-content.yaml` | Markdown content demo | `type: content` with `format: markdown`, dynamic body | +| `examples/html-content.yaml` | HTML content demo | `type: content` with `format: html`, inline styles | ### Per-Feature Examples (examples/visual-docs/) @@ -515,6 +557,8 @@ Minimal, focused templates -- one feature per file: | FlexRender.Svg | Svg.Render + providers | (none) | | FlexRender.DependencyInjection | Core | Microsoft.Extensions.DI | | FlexRender.MetaPackage | All | -- | +| FlexRender.Content.Markdown | Core | Markdig 1.1.1 | +| FlexRender.Content.Html | Core | HtmlAgilityPack 1.12.4 | | flexrender-cli | All | System.CommandLine | **Linux/Docker:** SkiaSharp requires native libraries. Add `SkiaSharp.NativeAssets.Linux` (or `.NoDependencies` for minimal containers) to your executable project to avoid `DllNotFoundException: libSkiaSharp`. When using `FlexRender.HarfBuzz`, also add `HarfBuzzSharp.NativeAssets.Linux` to avoid `DllNotFoundException: libHarfBuzzSharp`. diff --git a/src/FlexRender.Barcode.ImageSharp.Render/Providers/BarcodeImageSharpProvider.cs b/src/FlexRender.Barcode.ImageSharp.Render/Providers/BarcodeImageSharpProvider.cs index 2bd1ec5..552d333 100644 --- a/src/FlexRender.Barcode.ImageSharp.Render/Providers/BarcodeImageSharpProvider.cs +++ b/src/FlexRender.Barcode.ImageSharp.Render/Providers/BarcodeImageSharpProvider.cs @@ -3,6 +3,7 @@ using FlexRender.Parsing.Ast; using FlexRender.Providers; using SixLabors.Fonts; +using SixLaborsFontStyle = SixLabors.Fonts.FontStyle; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.PixelFormats; @@ -100,14 +101,14 @@ private static ImageGenerateCode128(BarcodeElement element, int targetW private static Font ResolveFont(float size) { if (SystemFonts.TryGet("Arial", out var arialFamily)) - return arialFamily.CreateFont(size, FontStyle.Regular); + return arialFamily.CreateFont(size, SixLaborsFontStyle.Regular); if (SystemFonts.TryGet("Liberation Sans", out var liberationFamily)) - return liberationFamily.CreateFont(size, FontStyle.Regular); + return liberationFamily.CreateFont(size, SixLaborsFontStyle.Regular); foreach (var family in SystemFonts.Families) { - return family.CreateFont(size, FontStyle.Regular); + return family.CreateFont(size, SixLaborsFontStyle.Regular); } throw new InvalidOperationException("No system fonts are available for barcode text rendering."); diff --git a/src/FlexRender.Cli/Commands/DebugLayoutCommand.cs b/src/FlexRender.Cli/Commands/DebugLayoutCommand.cs index 3e74b5d..bd72ae4 100644 --- a/src/FlexRender.Cli/Commands/DebugLayoutCommand.cs +++ b/src/FlexRender.Cli/Commands/DebugLayoutCommand.cs @@ -118,7 +118,7 @@ private static Task Execute( var templateData = data ?? new ObjectValue(); // Create renderer (has TextMeasurer configured) - using var renderer = new SkiaRenderer(); + using var renderer = Program.CreateSkiaRenderer(); // Register extra fonts from --fonts dir if (fontsDir is not null) diff --git a/src/FlexRender.Cli/FlexRender.Cli.csproj b/src/FlexRender.Cli/FlexRender.Cli.csproj index dc51706..42eafb2 100644 --- a/src/FlexRender.Cli/FlexRender.Cli.csproj +++ b/src/FlexRender.Cli/FlexRender.Cli.csproj @@ -25,6 +25,8 @@ + + diff --git a/src/FlexRender.Cli/Program.cs b/src/FlexRender.Cli/Program.cs index db1789e..c4edfc7 100644 --- a/src/FlexRender.Cli/Program.cs +++ b/src/FlexRender.Cli/Program.cs @@ -6,7 +6,11 @@ using FlexRender.Http; using FlexRender.QrCode; using FlexRender.QrCode.ImageSharp; +using FlexRender.Content.Html; +using FlexRender.Content.Markdown; +using FlexRender.Rendering; using FlexRender.SvgElement; +using FlexRender.TemplateEngine; namespace FlexRender.Cli; @@ -71,7 +75,9 @@ public static FlexRenderBuilder CreateRenderBuilder( string rasterBackend = "skia") { var builder = new FlexRenderBuilder() - .WithHttpLoader(); + .WithHttpLoader() + .WithMarkdown() + .WithHtml(); switch (backend) { @@ -113,6 +119,24 @@ public static FlexRenderBuilder CreateRenderBuilder( return builder; } + /// + /// Creates a configured + ///with content parsers registered. + /// Used by commands that need direct Skia API access (e.g., debug-layout). + /// A configured + internal static SkiaRenderer CreateSkiaRenderer() + { + var registry = new ContentParserRegistry(); + registry.Register(new MarkdownContentParser()); + registry.Register(new HtmlContentParser()); + return new SkiaRenderer( + new ResourceLimits(), + qrProvider: null, + barcodeProvider: null, + imageLoader: null, + contentParserRegistry: registry); + } + ///instance. The caller is responsible for disposing it. /// Creates and configures the root command with all subcommands. /// diff --git a/src/FlexRender.Content.Html/FlexRender.Content.Html.csproj b/src/FlexRender.Content.Html/FlexRender.Content.Html.csproj new file mode 100644 index 0000000..401b365 --- /dev/null +++ b/src/FlexRender.Content.Html/FlexRender.Content.Html.csproj @@ -0,0 +1,17 @@ ++ + diff --git a/src/FlexRender.Content.Html/FlexRenderBuilderExtensions.cs b/src/FlexRender.Content.Html/FlexRenderBuilderExtensions.cs new file mode 100644 index 0000000..8f5c4bc --- /dev/null +++ b/src/FlexRender.Content.Html/FlexRenderBuilderExtensions.cs @@ -0,0 +1,21 @@ +using FlexRender.Configuration; + +namespace FlexRender.Content.Html; + +///+ + +FlexRender.Content.Html +HTML content parser for FlexRender. Converts HTML text into FlexRender template elements. +true ++ + ++ + + ++ +/// Extension methods for configuring HTML content parsing in FlexRender. +/// +public static class FlexRenderBuilderExtensions +{ + ///+ /// Adds HTML content parsing support. Enables + /// The builder to configure. + ///type: content elements withformat: html . + ///The same builder instance for method chaining. + ///Thrown when + public static FlexRenderBuilder WithHtml(this FlexRenderBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); + return builder.WithContentParser(new HtmlContentParser()); + } +} diff --git a/src/FlexRender.Content.Html/HtmlContentParser.cs b/src/FlexRender.Content.Html/HtmlContentParser.cs new file mode 100644 index 0000000..77803d8 --- /dev/null +++ b/src/FlexRender.Content.Html/HtmlContentParser.cs @@ -0,0 +1,600 @@ +using System.Globalization; +using FlexRender.Abstractions; +using FlexRender.Layout; +using FlexRender.Parsing.Ast; +using HtmlAgilityPack; + +namespace FlexRender.Content.Html; + +///is null. +/// Parses HTML-formatted text into FlexRender template elements using HtmlAgilityPack. +/// +public sealed class HtmlContentParser : IContentParser +{ + ///+ /// Maximum allowed recursion depth for nested node processing to prevent + /// + private const int MaxDepth = 64; + + private static readonly HashSeton deeply nested input. + /// IgnoredTags = new(StringComparer.OrdinalIgnoreCase) + { + "script", "style", "head", "meta", "link", "title" + }; + + private static readonly HashSet PassthroughTags = new(StringComparer.OrdinalIgnoreCase) + { + "html", "body" + }; + + private static readonly Dictionary HeadingSizes = new(StringComparer.OrdinalIgnoreCase) + { + ["h1"] = "2em", + ["h2"] = "1.5em", + ["h3"] = "1.2em", + ["h4"] = "1em", + ["h5"] = "0.9em", + ["h6"] = "0.8em" + }; + + /// + public string FormatName => "html"; + + /// + public IReadOnlyList Parse(string text) + { + ArgumentNullException.ThrowIfNull(text); + if (string.IsNullOrWhiteSpace(text)) return []; + + var doc = new HtmlDocument(); + doc.LoadHtml(text); + + var context = new InlineContext(); + var elements = new List (); + ProcessNodes(doc.DocumentNode.ChildNodes, elements, context, depth: 0); + return elements; + } + + private static void ProcessNodes( + HtmlNodeCollection nodes, + List results, + InlineContext context, + int depth) + { + if (depth > MaxDepth) return; + + foreach (var node in nodes) + { + ProcessNode(node, results, context, depth); + } + } + + private static void ProcessNode( + HtmlNode node, + List results, + InlineContext context, + int depth) + { + switch (node.NodeType) + { + case HtmlNodeType.Text: + ProcessTextNode(node, results, context); + break; + + case HtmlNodeType.Element: + ProcessElementNode(node, results, context, depth); + break; + } + } + + private static void ProcessTextNode( + HtmlNode node, + List results, + InlineContext context) + { + var text = HtmlEntity.DeEntitize(node.InnerText); + if (string.IsNullOrWhiteSpace(text)) return; + + results.Add(CreateTextElement(text, context)); + } + + private static void ProcessElementNode( + HtmlNode node, + List results, + InlineContext context, + int depth) + { + var tag = node.Name.ToLowerInvariant(); + + if (IgnoredTags.Contains(tag)) return; + + if (PassthroughTags.Contains(tag)) + { + ProcessNodes(node.ChildNodes, results, context, depth); + return; + } + + switch (tag) + { + case "br": + results.Add(new TextElement { Content = "\n" }); + break; + + case "hr": + results.Add(new SeparatorElement()); + break; + + case "img": + ProcessImage(node, results); + break; + + case "b" or "strong": + ProcessInlineFormatting(node, results, context with { Weight = FontWeight.Bold }, depth); + break; + + case "i" or "em": + ProcessInlineFormatting(node, results, context with { Style = Parsing.Ast.FontStyle.Italic }, depth); + break; + + case "code" when !IsInsidePre(node): + ProcessInlineFormatting(node, results, context with { Background = "#f0f0f0" }, depth); + break; + + case "span": + var spanContext = ApplyInlineStyles(node, context); + ProcessInlineFormatting(node, results, spanContext, depth); + break; + + case "h1" or "h2" or "h3" or "h4" or "h5" or "h6": + ProcessHeading(node, results, context, tag, depth); + break; + + case "p": + ProcessParagraph(node, results, context, depth); + break; + + case "ul": + ProcessUnorderedList(node, results, context, depth); + break; + + case "ol": + ProcessOrderedList(node, results, context, depth); + break; + + case "li": + // li outside a list context: treat as paragraph + ProcessParagraph(node, results, context, depth); + break; + + case "blockquote": + ProcessBlockquote(node, results, context, depth); + break; + + case "pre": + ProcessPreformatted(node, results, context); + break; + + case "code" when IsInsidePre(node): + // code inside pre: just process children as-is + ProcessNodes(node.ChildNodes, results, context, depth + 1); + break; + + case "div" or "section" or "article" or "nav" or "header" or "footer" or "main" or "aside": + ProcessContainer(node, results, context, depth); + break; + + case "a": + // Treat links as styled inline text + ProcessInlineFormatting(node, results, context with { Color = "#0066cc" }, depth); + break; + + default: + // Unknown tags: process children + ProcessNodes(node.ChildNodes, results, context, depth + 1); + break; + } + } + + private static void ProcessImage(HtmlNode node, List results) + { + var src = node.GetAttributeValue("src", ""); + if (string.IsNullOrWhiteSpace(src)) return; + + var img = new ImageElement { Src = src }; + + var widthAttr = node.GetAttributeValue("width", ""); + if (int.TryParse(widthAttr, NumberStyles.Integer, CultureInfo.InvariantCulture, out var w)) + img.ImageWidth = w; + + var heightAttr = node.GetAttributeValue("height", ""); + if (int.TryParse(heightAttr, NumberStyles.Integer, CultureInfo.InvariantCulture, out var h)) + img.ImageHeight = h; + + results.Add(img); + } + + private static void ProcessInlineFormatting( + HtmlNode node, + List results, + InlineContext context, + int depth) + { + context = ApplyInlineStyles(node, context); + ProcessNodes(node.ChildNodes, results, context, depth + 1); + } + + private static void ProcessHeading( + HtmlNode node, + List results, + InlineContext context, + string tag, + int depth) + { + var size = HeadingSizes.GetValueOrDefault(tag, "1em"); + var headingContext = ApplyInlineStyles(node, context with { Weight = FontWeight.Bold, Size = size }); + + var children = CollectInlineChildren(node, headingContext, depth); + if (children.Count == 1) + { + results.Add(children[0]); + } + else if (children.Count > 1) + { + var container = new FlexElement { Direction = FlexDirection.Row }; + foreach (var child in children) + container.AddChild(child); + results.Add(container); + } + } + + private static void ProcessParagraph( + HtmlNode node, + List results, + InlineContext context, + int depth) + { + context = ApplyInlineStyles(node, context); + var children = CollectInlineChildren(node, context, depth); + + if (children.Count == 1) + { + results.Add(children[0]); + } + else if (children.Count > 1) + { + var container = new FlexElement { Direction = FlexDirection.Row }; + foreach (var child in children) + container.AddChild(child); + results.Add(container); + } + } + + private static void ProcessUnorderedList( + HtmlNode node, + List results, + InlineContext context, + int depth) + { + context = ApplyInlineStyles(node, context); + var list = new FlexElement { Direction = FlexDirection.Column, Gap = "4" }; + + foreach (var child in node.ChildNodes) + { + if (child.NodeType != HtmlNodeType.Element) continue; + if (!child.Name.Equals("li", StringComparison.OrdinalIgnoreCase)) continue; + + var itemElements = CollectInlineChildren(child, context, depth); + if (itemElements.Count == 0) continue; + + // Prepend bullet to the first text element + if (itemElements[0] is TextElement firstText) + { + firstText.Content = "\u2022 " + firstText.Content.Value; + if (itemElements.Count == 1) + { + list.AddChild(firstText); + } + else + { + var row = new FlexElement { Direction = FlexDirection.Row }; + foreach (var item in itemElements) + row.AddChild(item); + list.AddChild(row); + } + } + else + { + var row = new FlexElement { Direction = FlexDirection.Row }; + row.AddChild(new TextElement { Content = "\u2022 " }); + foreach (var item in itemElements) + row.AddChild(item); + list.AddChild(row); + } + } + + if (list.Children.Count > 0) + results.Add(list); + } + + private static void ProcessOrderedList( + HtmlNode node, + List results, + InlineContext context, + int depth) + { + context = ApplyInlineStyles(node, context); + var list = new FlexElement { Direction = FlexDirection.Column, Gap = "4" }; + var index = 1; + + foreach (var child in node.ChildNodes) + { + if (child.NodeType != HtmlNodeType.Element) continue; + if (!child.Name.Equals("li", StringComparison.OrdinalIgnoreCase)) continue; + + var prefix = $"{index}. "; + var itemElements = CollectInlineChildren(child, context, depth); + if (itemElements.Count == 0) { index++; continue; } + + if (itemElements[0] is TextElement firstText) + { + firstText.Content = prefix + firstText.Content.Value; + if (itemElements.Count == 1) + { + list.AddChild(firstText); + } + else + { + var row = new FlexElement { Direction = FlexDirection.Row }; + foreach (var item in itemElements) + row.AddChild(item); + list.AddChild(row); + } + } + else + { + var row = new FlexElement { Direction = FlexDirection.Row }; + row.AddChild(new TextElement { Content = prefix }); + foreach (var item in itemElements) + row.AddChild(item); + list.AddChild(row); + } + + index++; + } + + if (list.Children.Count > 0) + results.Add(list); + } + + private static void ProcessBlockquote( + HtmlNode node, + List results, + InlineContext context, + int depth) + { + context = ApplyInlineStyles(node, context); + var container = new FlexElement + { + Padding = "0 0 0 12", + Background = "#f5f5f5", + Direction = FlexDirection.Column + }; + + var children = new List (); + ProcessNodes(node.ChildNodes, children, context, depth + 1); + foreach (var child in children) + container.AddChild(child); + + if (container.Children.Count > 0) + results.Add(container); + } + + private static void ProcessPreformatted( + HtmlNode node, + List results, + InlineContext context) + { + var container = new FlexElement + { + Background = "#f0f0f0", + Padding = "8", + Direction = FlexDirection.Column + }; + + // For , get the raw inner text preserving whitespace + var text = HtmlEntity.DeEntitize(node.InnerText); + if (!string.IsNullOrEmpty(text)) + { + container.AddChild(CreateTextElement(text, context)); + } + + results.Add(container); + } + + private static void ProcessContainer( + HtmlNode node, + Listresults, + InlineContext context, + int depth) + { + context = ApplyInlineStyles(node, context); + var styleProps = ParseStyleAttribute(node.GetAttributeValue("style", "")); + + var container = new FlexElement { Direction = FlexDirection.Column }; + + if (styleProps.TryGetValue("padding", out var padding)) + container.Padding = padding; + if (styleProps.TryGetValue("background-color", out var bg)) + container.Background = bg; + else if (styleProps.TryGetValue("background", out bg)) + container.Background = bg; + + var children = new List (); + ProcessNodes(node.ChildNodes, children, context, depth + 1); + foreach (var child in children) + container.AddChild(child); + + if (container.Children.Count > 0) + results.Add(container); + } + + private static List CollectInlineChildren(HtmlNode node, InlineContext context, int depth) + { + var elements = new List (); + ProcessNodes(node.ChildNodes, elements, context, depth + 1); + return elements; + } + + private static TextElement CreateTextElement(string text, InlineContext context) + { + var element = new TextElement { Content = text }; + + if (context.Weight != FontWeight.Normal) + element.FontWeight = context.Weight; + if (context.Style != Parsing.Ast.FontStyle.Normal) + element.FontStyle = context.Style; + if (context.Size is not null) + element.Size = context.Size; + if (context.Color is not null) + element.Color = context.Color; + if (context.Background is not null) + element.Background = context.Background; + if (context.Align is not null) + element.Align = context.Align.Value; + + return element; + } + + private static InlineContext ApplyInlineStyles(HtmlNode node, InlineContext context) + { + var style = node.GetAttributeValue("style", ""); + if (string.IsNullOrWhiteSpace(style)) return context; + + var props = ParseStyleAttribute(style); + + if (props.TryGetValue("color", out var color)) + context = context with { Color = color }; + + if (props.TryGetValue("font-size", out var fontSize)) + context = context with { Size = fontSize }; + + if (props.TryGetValue("background-color", out var bgColor)) + context = context with { Background = bgColor }; + else if (props.TryGetValue("background", out var bg)) + context = context with { Background = bg }; + + if (props.TryGetValue("font-weight", out var fontWeight)) + context = context with { Weight = ParseFontWeight(fontWeight) }; + + if (props.TryGetValue("font-style", out var fontStyle)) + context = context with { Style = ParseFontStyle(fontStyle) }; + + if (props.TryGetValue("text-align", out var textAlign)) + context = context with { Align = ParseTextAlign(textAlign) }; + + return context; + } + + private static Dictionary ParseStyleAttribute(string style) + { + var result = new Dictionary (StringComparer.OrdinalIgnoreCase); + if (string.IsNullOrWhiteSpace(style)) return result; + + foreach (var declaration in style.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + var colonIndex = declaration.IndexOf(':'); + if (colonIndex <= 0 || colonIndex >= declaration.Length - 1) continue; + + var property = declaration[..colonIndex].Trim(); + var value = declaration[(colonIndex + 1)..].Trim(); + if (property.Length > 0 && value.Length > 0) + result[property] = value; + } + + return result; + } + + private static FontWeight ParseFontWeight(string value) + { + if (value.Equals("bold", StringComparison.OrdinalIgnoreCase)) + return FontWeight.Bold; + if (value.Equals("normal", StringComparison.OrdinalIgnoreCase)) + return FontWeight.Normal; + + if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var numeric)) + { + // Map to closest enum value + return numeric switch + { + <= 100 => FontWeight.Thin, + <= 200 => FontWeight.ExtraLight, + <= 300 => FontWeight.Light, + <= 400 => FontWeight.Normal, + <= 500 => FontWeight.Medium, + <= 600 => FontWeight.SemiBold, + <= 700 => FontWeight.Bold, + <= 800 => FontWeight.ExtraBold, + _ => FontWeight.Black + }; + } + + return FontWeight.Normal; + } + + private static Parsing.Ast.FontStyle ParseFontStyle(string value) + { + if (value.Equals("italic", StringComparison.OrdinalIgnoreCase)) + return Parsing.Ast.FontStyle.Italic; + if (value.Equals("oblique", StringComparison.OrdinalIgnoreCase)) + return Parsing.Ast.FontStyle.Oblique; + return Parsing.Ast.FontStyle.Normal; + } + + private static TextAlign ParseTextAlign(string value) + { + return value.ToLowerInvariant() switch + { + "center" => TextAlign.Center, + "right" => TextAlign.Right, + "left" => TextAlign.Left, + _ => TextAlign.Left + }; + } + + private static bool IsInsidePre(HtmlNode node) + { + var parent = node.ParentNode; + while (parent is not null) + { + if (parent.Name.Equals("pre", StringComparison.OrdinalIgnoreCase)) + return true; + parent = parent.ParentNode; + } + + return false; + } + + /// + /// Tracks inherited inline formatting state during DOM traversal. + /// + private sealed record InlineContext + { + ///Gets the current font weight. + public FontWeight Weight { get; init; } = FontWeight.Normal; + + ///Gets the current font style. + public Parsing.Ast.FontStyle Style { get; init; } = Parsing.Ast.FontStyle.Normal; + + ///Gets the current font size override, or null for default. + public string? Size { get; init; } + + ///Gets the current text color override, or null for default. + public string? Color { get; init; } + + ///Gets the current background color override, or null for default. + public string? Background { get; init; } + + ///Gets the current text alignment override, or null for default. + public TextAlign? Align { get; init; } + } +} diff --git a/src/FlexRender.Content.Markdown/FlexRender.Content.Markdown.csproj b/src/FlexRender.Content.Markdown/FlexRender.Content.Markdown.csproj new file mode 100644 index 0000000..0fd9fc7 --- /dev/null +++ b/src/FlexRender.Content.Markdown/FlexRender.Content.Markdown.csproj @@ -0,0 +1,17 @@ ++ + diff --git a/src/FlexRender.Content.Markdown/FlexRenderBuilderExtensions.cs b/src/FlexRender.Content.Markdown/FlexRenderBuilderExtensions.cs new file mode 100644 index 0000000..09605b4 --- /dev/null +++ b/src/FlexRender.Content.Markdown/FlexRenderBuilderExtensions.cs @@ -0,0 +1,21 @@ +using FlexRender.Configuration; + +namespace FlexRender.Content.Markdown; + +///+ + +FlexRender.Content.Markdown +Markdown content parser for FlexRender. Converts Markdown text into FlexRender template elements. +true ++ + ++ + + ++ +/// Extension methods for configuring Markdown content parsing in FlexRender. +/// +public static class FlexRenderBuilderExtensions +{ + ///+ /// Adds Markdown content parsing support. Enables + /// The builder to configure. + ///type: content elements withformat: markdown . + ///The same builder instance for method chaining. + ///Thrown when + public static FlexRenderBuilder WithMarkdown(this FlexRenderBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); + return builder.WithContentParser(new MarkdownContentParser()); + } +} diff --git a/src/FlexRender.Content.Markdown/MarkdownContentParser.cs b/src/FlexRender.Content.Markdown/MarkdownContentParser.cs new file mode 100644 index 0000000..41d00bf --- /dev/null +++ b/src/FlexRender.Content.Markdown/MarkdownContentParser.cs @@ -0,0 +1,374 @@ +using FlexRender.Abstractions; +using FlexRender.Layout; +using FlexRender.Parsing.Ast; +using Markdig; +using Markdig.Syntax; +using Markdig.Syntax.Inlines; + +namespace FlexRender.Content.Markdown; + +///is null. +/// Parses Markdown-formatted text into FlexRender template elements using the Markdig library. +/// +public sealed class MarkdownContentParser : IContentParser +{ + ///+ /// Maximum allowed recursion depth for nested block/inline conversion to prevent + /// + private const int MaxDepth = 64; + + private static readonly MarkdownPipeline Pipeline = new MarkdownPipelineBuilder() + .UseAdvancedExtensions() + .Build(); + + ///on deeply nested input. + /// + public string FormatName => "markdown"; + + /// + public IReadOnlyList Parse(string text) + { + ArgumentNullException.ThrowIfNull(text); + if (string.IsNullOrWhiteSpace(text)) return []; + + var document = Markdig.Markdown.Parse(text, Pipeline); + var elements = new List (); + + foreach (var block in document) + { + var converted = ConvertBlock(block, depth: 0); + if (converted is not null) + { + elements.Add(converted); + } + } + + return elements; + } + + private static TemplateElement? ConvertBlock(Block block, int depth = 0) + { + if (depth > MaxDepth) return null; + + return block switch + { + HeadingBlock heading => ConvertHeading(heading, depth), + ParagraphBlock paragraph => ConvertParagraph(paragraph, depth), + ThematicBreakBlock => new SeparatorElement(), + ListBlock list => ConvertList(list, depth), + QuoteBlock quote => ConvertBlockquote(quote, depth), + FencedCodeBlock fencedCode => ConvertCodeBlock(ExtractCodeBlockText(fencedCode)), + CodeBlock code => ConvertCodeBlock(ExtractCodeBlockText(code)), + _ => null + }; + } + + private static TemplateElement ConvertHeading(HeadingBlock heading, int depth) + { + var size = heading.Level switch + { + 1 => "2em", + 2 => "1.5em", + 3 => "1.2em", + 4 => "1em", + _ => "0.9em" + }; + + // A heading may contain mixed inline formatting (bold, italic, etc.) + // but the entire heading is bold by default. + var inlineElements = CollectInlines(heading.Inline, isBold: true, isItalic: false, depth: depth + 1); + + if (inlineElements.Count == 1 && inlineElements[0] is TextElement singleText) + { + singleText.Size = size; + return singleText; + } + + // Multiple inline segments: wrap in a flex row so they appear on the same line + var container = new FlexElement { Direction = FlexDirection.Row }; + foreach (var element in inlineElements) + { + if (element is TextElement te) + { + te.Size = size; + } + + container.AddChild(element); + } + + return container; + } + + private static TemplateElement ConvertParagraph(ParagraphBlock paragraph, int depth) + { + var inlineElements = CollectInlines(paragraph.Inline, isBold: false, isItalic: false, depth: depth + 1); + + if (inlineElements.Count == 1) + { + return inlineElements[0]; + } + + // Multiple inline segments with different formatting: wrap in a flex row + var container = new FlexElement { Direction = FlexDirection.Row }; + foreach (var element in inlineElements) + { + container.AddChild(element); + } + + return container; + } + + private static FlexElement ConvertList(ListBlock list, int depth) + { + var container = new FlexElement + { + Direction = FlexDirection.Column, + Gap = "4" + }; + + var index = 1; + foreach (var item in list) + { + if (item is not ListItemBlock listItem) continue; + + var prefix = list.IsOrdered ? $"{index}. " : "\u2022 "; + var itemElement = ConvertListItem(listItem, prefix, depth); + container.AddChild(itemElement); + index++; + } + + return container; + } + + private static TemplateElement ConvertListItem(ListItemBlock listItem, string prefix, int depth) + { + // Collect all inline content from the list item's paragraphs + var allInlines = new List (); + foreach (var subBlock in listItem) + { + if (subBlock is ParagraphBlock paragraph) + { + var inlines = CollectInlines(paragraph.Inline, isBold: false, isItalic: false, depth: depth + 1); + allInlines.AddRange(inlines); + } + else + { + var converted = ConvertBlock(subBlock, depth + 1); + if (converted is not null) + { + allInlines.Add(converted); + } + } + } + + // Prepend the bullet/number prefix to the first text element + if (allInlines.Count > 0 && allInlines[0] is TextElement firstText) + { + firstText.Content = prefix + firstText.Content.Value; + } + else + { + allInlines.Insert(0, new TextElement { Content = prefix }); + } + + if (allInlines.Count == 1) + { + return allInlines[0]; + } + + var row = new FlexElement { Direction = FlexDirection.Row }; + foreach (var element in allInlines) + { + row.AddChild(element); + } + + return row; + } + + private static FlexElement ConvertBlockquote(QuoteBlock quote, int depth) + { + var container = new FlexElement + { + Padding = "0 0 0 12", + Background = "#f5f5f5", + Direction = FlexDirection.Column + }; + + foreach (var subBlock in quote) + { + var converted = ConvertBlock(subBlock, depth + 1); + if (converted is not null) + { + container.AddChild(converted); + } + } + + return container; + } + + private static FlexElement ConvertCodeBlock(string codeText) + { + var container = new FlexElement + { + Background = "#f0f0f0", + Padding = "8" + }; + + container.AddChild(new TextElement { Content = codeText }); + + return container; + } + + private static string ExtractCodeBlockText(LeafBlock codeBlock) + { + var lines = codeBlock.Lines; + var builder = new System.Text.StringBuilder(); + + for (var i = 0; i < lines.Count; i++) + { + if (i > 0) + { + builder.Append('\n'); + } + + var line = lines.Lines[i]; + builder.Append(line.Slice.AsSpan()); + } + + return builder.ToString(); + } + + /// + /// Walks the inline tree and produces a list of template elements, accumulating + /// contiguous text runs with the same formatting into single TextElements. + /// + /// The container inline to walk. + /// Whether text is currently bold. + /// Whether text is currently italic. + /// Current recursion depth for stack overflow protection. + ///A list of template elements representing the inline content. + private static ListCollectInlines( + ContainerInline? container, + bool isBold, + bool isItalic, + int depth = 0) + { + var results = new List (); + if (container is null || depth > MaxDepth) return results; + + // Track current formatting state for text accumulation + var currentText = new System.Text.StringBuilder(); + var currentBold = isBold; + var currentItalic = isItalic; + var currentIsCode = false; + + void FlushText() + { + if (currentText.Length == 0) return; + + var element = new TextElement { Content = currentText.ToString() }; + + if (currentBold) + element.FontWeight = Parsing.Ast.FontWeight.Bold; + if (currentItalic) + element.FontStyle = Parsing.Ast.FontStyle.Italic; + if (currentIsCode) + element.Background = "#f0f0f0"; + + results.Add(element); + currentText.Clear(); + } + + foreach (var inline in container) + { + switch (inline) + { + case LiteralInline literal: + { + // Check if formatting state matches current accumulation + if (isBold != currentBold || isItalic != currentItalic || currentIsCode) + { + FlushText(); + currentBold = isBold; + currentItalic = isItalic; + currentIsCode = false; + } + + currentText.Append(literal.Content.AsSpan()); + break; + } + + case EmphasisInline emphasis: + { + FlushText(); + + var childBold = emphasis.DelimiterCount >= 2 ? true : isBold; + var childItalic = emphasis.DelimiterCount == 1 || emphasis.DelimiterCount == 3 ? true : isItalic; + + var childElements = CollectInlines(emphasis, childBold, childItalic, depth + 1); + results.AddRange(childElements); + + // Reset state after emphasis children + currentBold = isBold; + currentItalic = isItalic; + currentIsCode = false; + break; + } + + case CodeInline code: + { + FlushText(); + currentBold = isBold; + currentItalic = isItalic; + currentIsCode = true; + + currentText.Append(code.Content); + FlushText(); + currentIsCode = false; + break; + } + + case LinkInline link: + { + FlushText(); + + if (link.IsImage) + { + results.Add(new ImageElement { Src = link.Url ?? "" }); + } + else + { + // For regular links, render the link text as plain text + var linkInlines = CollectInlines(link, isBold, isItalic, depth + 1); + results.AddRange(linkInlines); + } + + currentBold = isBold; + currentItalic = isItalic; + currentIsCode = false; + break; + } + + case LineBreakInline: + { + currentText.Append('\n'); + break; + } + + case ContainerInline nestedContainer: + { + FlushText(); + var nested = CollectInlines(nestedContainer, isBold, isItalic, depth + 1); + results.AddRange(nested); + currentBold = isBold; + currentItalic = isItalic; + currentIsCode = false; + break; + } + } + } + + FlushText(); + return results; + } +} diff --git a/src/FlexRender.Core/Abstractions/IContentParser.cs b/src/FlexRender.Core/Abstractions/IContentParser.cs new file mode 100644 index 0000000..c1d156d --- /dev/null +++ b/src/FlexRender.Core/Abstractions/IContentParser.cs @@ -0,0 +1,26 @@ +using FlexRender.Parsing.Ast; + +namespace FlexRender.Abstractions; + +/// +/// Parses formatted text content into a subtree of template elements. +/// +public interface IContentParser +{ + ///+ /// Gets the format name this parser handles (e.g., "markdown", "escpos", "xml"). + /// + string FormatName { get; } + + ///+ /// Parses the formatted text into template elements. + /// + /// The formatted text to parse. + ///+ /// A list of renderable template elements (e.g., + ///, , + /// ). Must not contain control-flow elements + /// ( EachElement ,IfElement ,ContentElement ) as they will not be expanded. + ///Thrown when + IReadOnlyListis null. Parse(string text); +} diff --git a/src/FlexRender.Core/Configuration/FlexRenderBuilder.cs b/src/FlexRender.Core/Configuration/FlexRenderBuilder.cs index b53d66b..276f7f1 100644 --- a/src/FlexRender.Core/Configuration/FlexRenderBuilder.cs +++ b/src/FlexRender.Core/Configuration/FlexRenderBuilder.cs @@ -51,6 +51,7 @@ public sealed class FlexRenderBuilder private bool _defaultLoadersAdded; private bool _built; private FilterRegistry? _filterRegistry; + private ContentParserRegistry? _contentParserRegistry; private bool _useDefaultFilters = true; /// @@ -81,6 +82,11 @@ public sealed class FlexRenderBuilder /// internal FilterRegistry? FilterRegistry => _filterRegistry; + ///+ /// Gets the configured content parser registry, or + internal ContentParserRegistry? ContentParserRegistry => _contentParserRegistry; + ///null if no content parsers have been registered. + ////// Sets the renderer factory function that creates the @@ -199,6 +205,21 @@ public FlexRenderBuilder WithFilter(ITemplateFilter filter) return this; } + ///implementation. /// + /// Registers a content parser for expanding + /// The content parser to register. + ///type: content elements. + ///This builder instance for method chaining. + ///Thrown when + ///is null. Thrown when a parser for this format is already registered. + public FlexRenderBuilder WithContentParser(IContentParser parser) + { + ArgumentNullException.ThrowIfNull(parser); + _contentParserRegistry ??= new ContentParserRegistry(); + _contentParserRegistry.Register(parser); + return this; + } + ////// Clears all built-in filters, allowing only explicitly registered custom filters. /// diff --git a/src/FlexRender.Core/Parsing/Ast/ContentElement.cs b/src/FlexRender.Core/Parsing/Ast/ContentElement.cs new file mode 100644 index 0000000..fba6d2b --- /dev/null +++ b/src/FlexRender.Core/Parsing/Ast/ContentElement.cs @@ -0,0 +1,38 @@ +namespace FlexRender.Parsing.Ast; + +///+/// A content element that receives formatted text and expands it into a subtree +/// of template elements via a registered +public sealed class ContentElement : TemplateElement +{ + ///. +/// + public override ElementType Type => ElementType.Content; + + /// + /// Gets or sets the source text to parse. Supports + public ExprValue{{expressions}} . + ///Source { get; set; } = ""; + + /// + /// Gets or sets the format name (e.g., "markdown", "escpos", "xml"). + /// Supports + public ExprValue{{expressions}} . Must match a registered. + /// Format { get; set; } = ""; + + /// + public override void ResolveExpressions(Func resolver, ObjectValue data) + { + base.ResolveExpressions(resolver, data); + Source = Source.Resolve(resolver, data); + Format = Format.Resolve(resolver, data); + } + + /// + public override void Materialize() + { + base.Materialize(); + Source = Source.Materialize(nameof(Source)); + Format = Format.Materialize(nameof(Format)); + } +} diff --git a/src/FlexRender.Core/Parsing/Ast/TableElement.cs b/src/FlexRender.Core/Parsing/Ast/TableElement.cs index 405f834..9deff3b 100644 --- a/src/FlexRender.Core/Parsing/Ast/TableElement.cs +++ b/src/FlexRender.Core/Parsing/Ast/TableElement.cs @@ -66,6 +66,24 @@ public sealed class TableElement : TemplateElement /// public string? HeaderFont { get; set; } + /// + /// Gets or sets the font weight for header cells (e.g., bold, semi-bold). + /// When set, overrides the default font weight for all header cells. + /// + public FontWeight? HeaderFontWeight { get; set; } + + ///+ /// Gets or sets the font style for header cells (e.g., italic, oblique). + /// When set, overrides the default font style for all header cells. + /// + public FontStyle? HeaderFontStyle { get; set; } + + ///+ /// Gets or sets the CSS-like font family name for header cells. + /// When set, overrides the default font family for all header cells. + /// + public string? HeaderFontFamily { get; set; } + ////// Gets or sets the text color for header cells. /// diff --git a/src/FlexRender.Core/Parsing/Ast/TemplateElement.cs b/src/FlexRender.Core/Parsing/Ast/TemplateElement.cs index 51fb708..a4b9c2e 100644 --- a/src/FlexRender.Core/Parsing/Ast/TemplateElement.cs +++ b/src/FlexRender.Core/Parsing/Ast/TemplateElement.cs @@ -56,7 +56,12 @@ public enum ElementType ////// An SVG element for rendering vector graphics content. /// - Svg + Svg, + + ///+ /// A content element that expands formatted text into a subtree. + /// + Content } ///diff --git a/src/FlexRender.Core/Parsing/Ast/TextElement.cs b/src/FlexRender.Core/Parsing/Ast/TextElement.cs index 1f0cdd3..13f6b2c 100644 --- a/src/FlexRender.Core/Parsing/Ast/TextElement.cs +++ b/src/FlexRender.Core/Parsing/Ast/TextElement.cs @@ -54,6 +54,44 @@ public enum TextOverflow Visible } +/// public ExprValue+/// Font weight values following CSS font-weight specification. +/// +public enum FontWeight +{ + ///Thin (100). + Thin = 100, + ///Extra-light (200). + ExtraLight = 200, + ///Light (300). + Light = 300, + ///Normal/Regular (400). Default. + Normal = 400, + ///Medium (500). + Medium = 500, + ///Semi-bold (600). + SemiBold = 600, + ///Bold (700). + Bold = 700, + ///Extra-bold (800). + ExtraBold = 800, + ///Black (900). + Black = 900 +} + +///+/// Font style values following CSS font-style specification. +/// +public enum FontStyle +{ + ///Normal upright text. Default. + Normal, + ///Italic text using a dedicated italic typeface. + Italic, + ///Oblique text (mechanically slanted). + Oblique +} + ////// A text element in the template. /// @@ -72,6 +110,22 @@ public sealed class TextElement : TemplateElement ///Font { get; set; } = "main"; + /// + /// CSS-like font family name. Searched in registered fonts by FamilyName, then system fonts. + /// When set and + public ExprValueis at its default value, this takes precedence. + /// FontFamily { get; set; } = ""; + + /// + /// Font weight (100-900 or named: thin, light, normal, bold, black). Default: normal (400). + /// + public ExprValueFontWeight { get; set; } = Ast.FontWeight.Normal; + + /// + /// Font style (normal, italic, oblique). Default: normal. + /// + public ExprValueFontStyle { get; set; } = Ast.FontStyle.Normal; + /// /// Font size (pixels, em, or percentage). /// @@ -116,6 +170,9 @@ public override void ResolveExpressions(Funcresolv base.ResolveExpressions(resolver, data); Content = Content.Resolve(resolver, data); Font = Font.Resolve(resolver, data); + FontFamily = FontFamily.Resolve(resolver, data); + FontWeight = FontWeight.Resolve(resolver, data); + FontStyle = FontStyle.Resolve(resolver, data); Size = Size.Resolve(resolver, data); Color = Color.Resolve(resolver, data); Align = Align.Resolve(resolver, data); @@ -131,6 +188,9 @@ public override void Materialize() base.Materialize(); Content = Content.Materialize(nameof(Content)); Font = Font.Materialize(nameof(Font)); + FontFamily = FontFamily.Materialize(nameof(FontFamily)); + FontWeight = FontWeight.Materialize(nameof(FontWeight)); + FontStyle = FontStyle.Materialize(nameof(FontStyle)); Size = Size.Materialize(nameof(Size), ValueKind.Size); Color = Color.Materialize(nameof(Color), ValueKind.Color); Align = Align.Materialize(nameof(Align)); diff --git a/src/FlexRender.Core/TemplateEngine/ContentParserRegistry.cs b/src/FlexRender.Core/TemplateEngine/ContentParserRegistry.cs new file mode 100644 index 0000000..61eac31 --- /dev/null +++ b/src/FlexRender.Core/TemplateEngine/ContentParserRegistry.cs @@ -0,0 +1,43 @@ +using FlexRender.Abstractions; + +namespace FlexRender.TemplateEngine; + +/// +/// Registry for content parsers, mapping format names to +public sealed class ContentParserRegistry +{ + private readonly Dictionaryimplementations. +/// _parsers = new(StringComparer.OrdinalIgnoreCase); + + /// + /// Registers a content parser for its format name. + /// + /// The content parser to register. + ///Thrown when + ///is null. Thrown when a parser for the same format is already registered. + public void Register(IContentParser parser) + { + ArgumentNullException.ThrowIfNull(parser); + ArgumentException.ThrowIfNullOrWhiteSpace(parser.FormatName, nameof(parser)); + if (!_parsers.TryAdd(parser.FormatName, parser)) + { + throw new InvalidOperationException( + $"A content parser for format '{parser.FormatName}' is already registered."); + } + } + + ///+ /// Gets the content parser for the specified format name. + /// + /// The format name to look up. + ///The content parser if found; otherwise, + public IContentParser? GetParser(string formatName) + { + return _parsers.GetValueOrDefault(formatName); + } + + ///null .+ /// Gets whether any content parsers are registered. + /// + internal bool HasParsers => _parsers.Count > 0; +} diff --git a/src/FlexRender.Core/TemplateEngine/TemplateExpander.cs b/src/FlexRender.Core/TemplateEngine/TemplateExpander.cs index 5bb2f41..dcbd55e 100644 --- a/src/FlexRender.Core/TemplateEngine/TemplateExpander.cs +++ b/src/FlexRender.Core/TemplateEngine/TemplateExpander.cs @@ -12,6 +12,7 @@ public sealed class TemplateExpander { private readonly ResourceLimits _limits; private readonly InlineExpressionEvaluator? _expressionEvaluator; + private readonly ContentParserRegistry? _contentParserRegistry; ////// Creates a new TemplateExpander with default resource limits. @@ -24,12 +25,14 @@ public TemplateExpander() : this(new ResourceLimits()) /// Creates a new TemplateExpander with the specified resource limits. /// /// Resource limits for expansion depth protection. + /// Optional content parser registry for ContentElement expansion. ///Thrown when limits is null. - public TemplateExpander(ResourceLimits limits) + public TemplateExpander(ResourceLimits limits, ContentParserRegistry? contentParserRegistry = null) { ArgumentNullException.ThrowIfNull(limits); _limits = limits; _expressionEvaluator = new InlineExpressionEvaluator(); + _contentParserRegistry = contentParserRegistry; } ///@@ -37,9 +40,11 @@ public TemplateExpander(ResourceLimits limits) /// /// Resource limits for expansion depth protection. /// The filter registry for expression evaluation. + /// Optional content parser registry for ContentElement expansion. ///Thrown when limits or filterRegistry is null. - public TemplateExpander(ResourceLimits limits, FilterRegistry filterRegistry) - : this(limits, filterRegistry, CultureInfo.InvariantCulture) + public TemplateExpander(ResourceLimits limits, FilterRegistry filterRegistry, + ContentParserRegistry? contentParserRegistry = null) + : this(limits, filterRegistry, CultureInfo.InvariantCulture, contentParserRegistry) { } @@ -49,14 +54,17 @@ public TemplateExpander(ResourceLimits limits, FilterRegistry filterRegistry) /// Resource limits for expansion depth protection. /// The filter registry for expression evaluation. /// The culture to use for culture-aware filter formatting. + /// Optional content parser registry for ContentElement expansion. ///Thrown when any parameter is null. - public TemplateExpander(ResourceLimits limits, FilterRegistry filterRegistry, CultureInfo culture) + public TemplateExpander(ResourceLimits limits, FilterRegistry filterRegistry, CultureInfo culture, + ContentParserRegistry? contentParserRegistry = null) { ArgumentNullException.ThrowIfNull(limits); ArgumentNullException.ThrowIfNull(filterRegistry); ArgumentNullException.ThrowIfNull(culture); _limits = limits; _expressionEvaluator = new InlineExpressionEvaluator(filterRegistry, culture); + _contentParserRegistry = contentParserRegistry; } ///@@ -119,6 +127,7 @@ private IEnumerable internal sealed class ImageSharpFontManager : IDisposable { private readonly ConcurrentDictionaryExpandElement(TemplateElement element, Temp IfElement ifEl => ExpandIf(ifEl, context, depth), TableElement table => [ExpandTable(table, context, depth)], FlexElement flex => [ExpandFlex(flex, context, depth)], + ContentElement content => ExpandContent(content, context, depth), _ => [CloneWithVariableSubstitution(element, context)] }; } @@ -597,6 +606,9 @@ private FlexElement BuildHeaderRow(TableElement table, TemplateContext context) { Content = SubstituteVariables(col.Label ?? "", context), Font = table.HeaderFont ?? col.Font ?? table.Font, + FontWeight = table.HeaderFontWeight ?? FontWeight.Normal, + FontStyle = table.HeaderFontStyle ?? FontStyle.Normal, + FontFamily = table.HeaderFontFamily ?? "", Color = table.HeaderColor ?? col.Color ?? table.Color, Size = table.HeaderSize ?? col.Size ?? table.Size, Align = col.Align @@ -745,6 +757,32 @@ private FlexElement ExpandFlex(FlexElement flex, TemplateContext context, int de return clone; } + private IEnumerable ExpandContent(ContentElement content, TemplateContext context, int depth) + { + var childDepth = depth + 1; + if (childDepth > _limits.MaxRenderDepth) + { + throw new TemplateEngineException($"Maximum expansion depth ({_limits.MaxRenderDepth}) exceeded"); + } + + var resolvedSource = SubstituteVariables(content.Source.RawValue ?? content.Source.Value, context); + var resolvedFormat = SubstituteVariables(content.Format.RawValue ?? content.Format.Value, context); + + if (_contentParserRegistry is null) + { + throw new TemplateEngineException( + $"ContentElement with format '{resolvedFormat}' cannot be expanded: no content parsers are registered. " + + "Register a parser via FlexRenderBuilder.WithContentParser()."); + } + + var parser = _contentParserRegistry.GetParser(resolvedFormat) + ?? throw new TemplateEngineException( + $"No content parser registered for format '{resolvedFormat}'. " + + "Register a parser via FlexRenderBuilder.WithContentParser()."); + + return parser.Parse(resolvedSource ?? string.Empty); + } + /// /// Clones an element and substitutes variables in its string properties. /// @@ -762,6 +800,7 @@ private TemplateElement CloneWithVariableSubstitution(TemplateElement element, T BarcodeElement barcode => CloneBarcodeElement(barcode, context), SvgElement svg => CloneSvgElement(svg, context), SeparatorElement sep => CloneSeparatorElement(sep, context), + ContentElement content => CloneContentElement(content, context), FlexElement => throw new InvalidOperationException("FlexElement should be handled by ExpandFlex"), EachElement => throw new InvalidOperationException("EachElement should be expanded, not cloned"), IfElement => throw new InvalidOperationException("IfElement should be expanded, not cloned"), @@ -847,6 +886,8 @@ private TextElement CloneTextElement(TextElement text, TemplateContext context) { Content = SubstituteVariables(text.Content.Value, context), Font = text.Font, + FontWeight = text.FontWeight, + FontStyle = text.FontStyle, Size = text.Size, Color = text.Color, Align = text.Align, @@ -956,4 +997,20 @@ private SeparatorElement CloneSeparatorElement(SeparatorElement sep, TemplateCon TemplateElement.CopyBaseProperties(sep, clone); return clone; } + + private ContentElement CloneContentElement(ContentElement content, TemplateContext context) + { + var clone = new ContentElement + { + Source = SubstituteVariables(content.Source.RawValue ?? content.Source.Value, context), + Format = SubstituteVariables(content.Format.RawValue ?? content.Format.Value, context), + Rotate = content.Rotate, + Background = SubstituteVariables(content.Background.RawValue ?? content.Background.Value, context), + Padding = content.Padding, + Margin = content.Margin + }; + + TemplateElement.CopyBaseProperties(content, clone); + return clone; + } } diff --git a/src/FlexRender.ImageSharp.Render/FlexRenderBuilderExtensions.cs b/src/FlexRender.ImageSharp.Render/FlexRenderBuilderExtensions.cs index f9dac4e..91f8f04 100644 --- a/src/FlexRender.ImageSharp.Render/FlexRenderBuilderExtensions.cs +++ b/src/FlexRender.ImageSharp.Render/FlexRenderBuilderExtensions.cs @@ -54,7 +54,8 @@ public static FlexRenderBuilder WithImageSharp( b.Options, b.ResourceLoaders, imageSharpBuilder, - b.FilterRegistry)); + b.FilterRegistry, + b.ContentParserRegistry)); return builder; } @@ -89,6 +90,7 @@ public static FuncCreateRendererFactory( b.Options, b.ResourceLoaders, imageSharpBuilder, - b.FilterRegistry); + b.FilterRegistry, + b.ContentParserRegistry); } } diff --git a/src/FlexRender.ImageSharp.Render/ImageSharpRender.cs b/src/FlexRender.ImageSharp.Render/ImageSharpRender.cs index 7632cbf..a48f32b 100644 --- a/src/FlexRender.ImageSharp.Render/ImageSharpRender.cs +++ b/src/FlexRender.ImageSharp.Render/ImageSharpRender.cs @@ -37,6 +37,7 @@ public sealed class ImageSharpRender : IFlexRender private readonly ImageSharpFontManager _fontManager; private readonly IReadOnlyList _resourceLoaders; private readonly FilterRegistry? _filterRegistry; + private readonly ContentParserRegistry? _contentParserRegistry; private readonly ResourceLimits _limits; private readonly FlexRenderOptions _options; private readonly RenderOptions _defaultRenderOptions; @@ -50,6 +51,7 @@ public sealed class ImageSharpRender : IFlexRender /// Collection of resource loaders for assets. /// ImageSharp-specific configuration. /// Optional filter registry for expression filter evaluation. + /// Optional content parser registry for custom content type parsing. /// /// Thrown when , , /// , or is null. @@ -59,7 +61,8 @@ internal ImageSharpRender( FlexRenderOptions options, IReadOnlyList resourceLoaders, ImageSharpBuilder builder, - FilterRegistry? filterRegistry = null) + FilterRegistry? filterRegistry = null, + ContentParserRegistry? contentParserRegistry = null) { ArgumentNullException.ThrowIfNull(limits); ArgumentNullException.ThrowIfNull(options); @@ -71,6 +74,7 @@ internal ImageSharpRender( _defaultRenderOptions = options.DefaultRenderOptions; _resourceLoaders = resourceLoaders; _filterRegistry = filterRegistry; + _contentParserRegistry = contentParserRegistry; _fontManager = new ImageSharpFontManager(); var textRenderer = new ImageSharpTextRenderer(_fontManager); @@ -186,7 +190,7 @@ public async Task RenderToPng( try { using var image = _engine.RenderToImage( - layoutTemplate, effectiveData, _filterRegistry, imageCache, processedTemplate); + layoutTemplate, effectiveData, _filterRegistry, imageCache, processedTemplate, _contentParserRegistry); var encoder = new PngEncoder(); await image.SaveAsync(output, encoder, cancellationToken).ConfigureAwait(false); @@ -237,7 +241,7 @@ public async Task RenderToJpeg( try { using var image = _engine.RenderToImage( - layoutTemplate, effectiveData, _filterRegistry, imageCache, processedTemplate); + layoutTemplate, effectiveData, _filterRegistry, imageCache, processedTemplate, _contentParserRegistry); var encoder = new JpegEncoder { Quality = effectiveOptions.Quality }; await image.SaveAsync(output, encoder, cancellationToken).ConfigureAwait(false); @@ -287,7 +291,7 @@ public async Task RenderToBmp( try { using var image = _engine.RenderToImage( - layoutTemplate, effectiveData, _filterRegistry, imageCache, processedTemplate); + layoutTemplate, effectiveData, _filterRegistry, imageCache, processedTemplate, _contentParserRegistry); var encoder = new BmpEncoder { BitsPerPixel = BmpBitsPerPixel.Pixel32 }; await image.SaveAsync(output, encoder, cancellationToken).ConfigureAwait(false); @@ -335,7 +339,7 @@ public async Task RenderToRaw( try { using var image = _engine.RenderToImage( - layoutTemplate, effectiveData, _filterRegistry, imageCache, processedTemplate); + layoutTemplate, effectiveData, _filterRegistry, imageCache, processedTemplate, _contentParserRegistry); // Write raw RGBA pixel data var pixelCount = checked(image.Width * image.Height); @@ -377,8 +381,8 @@ public async Task RenderToRaw( // Expand, resolve, and materialize template to resolve expressions in image src attributes var expander = _filterRegistry is not null - ? new TemplateExpander(_limits, _filterRegistry) - : new TemplateExpander(_limits); + ? new TemplateExpander(_limits, _filterRegistry, _contentParserRegistry) + : new TemplateExpander(_limits, _contentParserRegistry); var templateProcessor = _filterRegistry is not null ? new TemplateProcessor(_limits, _filterRegistry) : new TemplateProcessor(_limits); diff --git a/src/FlexRender.ImageSharp.Render/Rendering/ImageSharpFontManager.cs b/src/FlexRender.ImageSharp.Render/Rendering/ImageSharpFontManager.cs index 2b98a45..5a25a96 100644 --- a/src/FlexRender.ImageSharp.Render/Rendering/ImageSharpFontManager.cs +++ b/src/FlexRender.ImageSharp.Render/Rendering/ImageSharpFontManager.cs @@ -1,6 +1,8 @@ using System.Collections.Concurrent; using System.Globalization; using SixLabors.Fonts; +using AstFontWeight = FlexRender.Parsing.Ast.FontWeight; +using AstFontStyle = FlexRender.Parsing.Ast.FontStyle; namespace FlexRender.ImageSharp.Rendering; @@ -10,12 +12,23 @@ namespace FlexRender.ImageSharp.Rendering; /// Each font file is loaded into an isolated FontCollection to prevent /// family name grouping (e.g. Inter-Regular and Inter-Bold both declare /// family "Inter" but must remain separate for correct weight selection). +/// When a specific font weight or style is requested, sibling font files +/// in the same directory are scanned and loaded into a shared FontCollection +/// so that SixLabors.Fonts can resolve the correct variant. /// _families = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary _fontStyles = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary _fontPaths = new(StringComparer.OrdinalIgnoreCase); + + /// + /// Cache of shared FontCollections keyed by directory path. + /// Each shared collection contains ALL .ttf/.otf files from that directory, + /// enabling SixLabors.Fonts to resolve font family variants (Bold, Italic, etc.). + /// + private readonly ConcurrentDictionary_sharedCollections = new(StringComparer.OrdinalIgnoreCase); + private int _disposed; /// @@ -79,6 +92,118 @@ public Font GetFont(string fontName, float size, FontStyle style = FontStyle.Reg return family.CreateFont(size, style); } + /// + /// Gets a font with the specified size, weight, and style for the given logical font name. + /// Maps AST font weight and style enums to the closest SixLabors.Fonts.FontStyle value. + /// When the requested weight or style differs from Normal, scans sibling font files in the + /// same directory as the registered font to find the correct variant (e.g. Inter-Bold.ttf + /// for + /// The logical font name. + /// The font size in pixels. + /// The AST font weight (100-900). + /// The AST font style (Normal, Italic, Oblique). + ///). Falls back to the isolated collection if no + /// sibling match is found. + /// A configured Font instance. Never returns null. + public Font GetFont(string fontName, float size, AstFontWeight weight, AstFontStyle style) + { + ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) == 1, this); + + if (weight == AstFontWeight.Normal && style == AstFontStyle.Normal) + return GetFont(fontName, size); + + var sixLaborsStyle = (weight >= AstFontWeight.Bold, style) switch + { + (true, AstFontStyle.Italic or AstFontStyle.Oblique) => FontStyle.BoldItalic, + (true, _) => FontStyle.Bold, + (_, AstFontStyle.Italic or AstFontStyle.Oblique) => FontStyle.Italic, + _ => FontStyle.Regular + }; + + // Try to find the variant in a shared collection built from sibling font files + var familyFont = GetFamilyFont(fontName, size, sixLaborsStyle); + if (familyFont is not null) + return familyFont; + + // Fall back to the isolated collection approach + var family = GetFontFamily(fontName); + return family.CreateFont(size, sixLaborsStyle); + } + + ///+ /// Attempts to find a font variant by scanning sibling font files in the same directory + /// as the registered font. Loads all + /// The logical font name. + /// The font size in pixels. + /// The desired SixLabors font style. + ///.ttf and.otf files from that directory + /// into a shared, then looks for a + /// matching the base font's family name that supports the requested style. + /// A + private Font? GetFamilyFont(string fontName, float size, FontStyle requestedStyle) + { + if (!_fontPaths.TryGetValue(fontName, out var basePath)) + return null; + + var directory = Path.GetDirectoryName(basePath); + if (string.IsNullOrEmpty(directory) || !Directory.Exists(directory)) + return null; + + // Get or create a shared collection for this directory + var sharedCollection = _sharedCollections.GetOrAdd(directory, LoadDirectoryFonts); + + // Determine the family name from the base font's isolated collection + if (!_families.TryGetValue(fontName, out var isolatedFamily)) + return null; + + var familyName = isolatedFamily.Name; + + // Look for the family in the shared collection + if (!sharedCollection.TryGet(familyName, out var sharedFamily)) + return null; + + // Check if the requested style is available + var availableStyles = sharedFamily.GetAvailableStyles(); + if (!availableStyles.Contains(requestedStyle)) + return null; + + return sharedFamily.CreateFont(size, requestedStyle); + } + + ///if the variant was found; otherwise, null .+ /// Loads all + /// The directory path to scan for font files. + ///.ttf and.otf font files from the specified directory + /// into a new. Files that fail to load (corrupt or + /// unsupported format) are silently skipped. + /// A + private static FontCollection LoadDirectoryFonts(string directory) + { + var collection = new FontCollection(); + + var fontFiles = Directory.EnumerateFiles(directory, "*.*") + .Where(static f => + { + var ext = Path.GetExtension(f); + return ext.Equals(".ttf", StringComparison.OrdinalIgnoreCase) + || ext.Equals(".otf", StringComparison.OrdinalIgnoreCase); + }); + + foreach (var filePath in fontFiles) + { + try + { + collection.Add(filePath, CultureInfo.InvariantCulture); + } + catch + { + // Corrupted or unreadable font file; skip it + } + } + + return collection; + } + ///containing all loadable fonts from the directory. /// Gets the FontFamily for the given logical font name. /// Falls back to "default", then "main", then system Arial. diff --git a/src/FlexRender.ImageSharp.Render/Rendering/ImageSharpRenderingEngine.cs b/src/FlexRender.ImageSharp.Render/Rendering/ImageSharpRenderingEngine.cs index 43bc088..e1042ab 100644 --- a/src/FlexRender.ImageSharp.Render/Rendering/ImageSharpRenderingEngine.cs +++ b/src/FlexRender.ImageSharp.Render/Rendering/ImageSharpRenderingEngine.cs @@ -76,13 +76,15 @@ internal ImageSharpRenderingEngine( /// Optional pre-processed template from image preloading. When provided, the /// expand+preprocess steps are skipped to avoid redundant work. /// + /// Optional content parser registry for custom content type parsing. /// A new image containing the rendered template. Caller owns disposal. internal ImageRenderToImage( Template template, ObjectValue data, FilterRegistry? filterRegistry = null, IReadOnlyDictionary >? imageCache = null, - Template? preprocessedTemplate = null) + Template? preprocessedTemplate = null, + ContentParserRegistry? contentParserRegistry = null) { ArgumentNullException.ThrowIfNull(template); ArgumentNullException.ThrowIfNull(data); @@ -97,8 +99,8 @@ internal Image RenderToImage( { // Expand, resolve, and materialize template via the Core pipeline var expander = filterRegistry is not null - ? new TemplateExpander(_limits, filterRegistry) - : new TemplateExpander(_limits); + ? new TemplateExpander(_limits, filterRegistry, contentParserRegistry) + : new TemplateExpander(_limits, contentParserRegistry); var templateProcessor = filterRegistry is not null ? new TemplateProcessor(_limits, filterRegistry) : new TemplateProcessor(_limits); diff --git a/src/FlexRender.ImageSharp.Render/Rendering/ImageSharpTextRenderer.cs b/src/FlexRender.ImageSharp.Render/Rendering/ImageSharpTextRenderer.cs index fb3d4e5..af2e202 100644 --- a/src/FlexRender.ImageSharp.Render/Rendering/ImageSharpTextRenderer.cs +++ b/src/FlexRender.ImageSharp.Render/Rendering/ImageSharpTextRenderer.cs @@ -111,7 +111,7 @@ public void DrawText( private Font CreateFont(TextElement element, float baseFontSize) { var fontSize = FontSizeResolver.Resolve(element.Size.Value, baseFontSize); - return _fontManager.GetFont(element.Font.Value, fontSize); + return _fontManager.GetFont(element.Font.Value, fontSize, element.FontWeight.Value, element.FontStyle.Value); } private static float ResolveLineHeight(string? lineHeight, Font font) diff --git a/src/FlexRender.ImageSharp.Render/Rendering/ImageSharpTextShaper.cs b/src/FlexRender.ImageSharp.Render/Rendering/ImageSharpTextShaper.cs index 3f6716c..6e89af2 100644 --- a/src/FlexRender.ImageSharp.Render/Rendering/ImageSharpTextShaper.cs +++ b/src/FlexRender.ImageSharp.Render/Rendering/ImageSharpTextShaper.cs @@ -76,7 +76,7 @@ public TextShapingResult ShapeText(TextElement element, float fontSize, float ma private Font CreateFont(TextElement element, float fontSize) { - return _fontManager.GetFont(element.Font.Value, fontSize); + return _fontManager.GetFont(element.Font.Value, fontSize, element.FontWeight.Value, element.FontStyle.Value); } private static float ResolveLineHeight(string? lineHeight, Font font) diff --git a/src/FlexRender.Skia.Render/Abstractions/IFontManager.cs b/src/FlexRender.Skia.Render/Abstractions/IFontManager.cs index 8b802e4..aec4cb9 100644 --- a/src/FlexRender.Skia.Render/Abstractions/IFontManager.cs +++ b/src/FlexRender.Skia.Render/Abstractions/IFontManager.cs @@ -1,3 +1,4 @@ +using FlexRender.Parsing.Ast; using SkiaSharp; namespace FlexRender.Abstractions; @@ -19,6 +20,39 @@ public interface IFontManager /// Thrown when SKTypeface GetTypeface(string fontName); + ///is null or empty. + /// Gets a typeface by font name with specific weight and style, using fallback if necessary. + /// When both + /// The font family name. + /// The desired font weight (100-900). + /// The desired font style (normal, italic, oblique). + ///and are their default values + /// ( and ), this behaves + /// identically to . + /// The requested typeface, or a fallback if not found. Never returns null. + SKTypeface GetTypeface(string fontName, FontWeight weight, FontStyle style); + + ///+ /// Gets a typeface by registered font name and optional CSS font family name with specific weight and style. + /// Priority: registered font name (if not default) > font family name > fallback. + /// + /// The registered font name. + /// CSS-like font family name to search registered fonts and system fonts. + /// The desired font weight (100-900). + /// The desired font style (normal, italic, oblique). + ///The requested typeface, or a fallback if not found. Never returns null. + SKTypeface GetTypeface(string fontName, string fontFamily, FontWeight weight, FontStyle style); + + ///+ /// Gets a typeface by font family name with specific weight and style. + /// Searches registered fonts by FamilyName metadata, then system fonts. + /// + /// The font family name (e.g., "Inter 18pt", "Arial"). + /// The desired font weight (100-900). + /// The desired font style (normal, italic, oblique). + ///The requested typeface, or a fallback if not found. Never returns null. + SKTypeface GetTypefaceByFamily(string familyName, FontWeight weight, FontStyle style); + ////// Registers a font with a file path and optional fallback. /// diff --git a/src/FlexRender.Skia.Render/FlexRenderBuilderExtensions.cs b/src/FlexRender.Skia.Render/FlexRenderBuilderExtensions.cs index 3011a03..8e6af28 100644 --- a/src/FlexRender.Skia.Render/FlexRenderBuilderExtensions.cs +++ b/src/FlexRender.Skia.Render/FlexRenderBuilderExtensions.cs @@ -63,7 +63,8 @@ public static FlexRenderBuilder WithSkia( b.Options, b.ResourceLoaders, skiaBuilder, - b.FilterRegistry)); + b.FilterRegistry, + b.ContentParserRegistry)); return builder; } diff --git a/src/FlexRender.Skia.Render/Rendering/FontManager.cs b/src/FlexRender.Skia.Render/Rendering/FontManager.cs index 86a2568..6bb62c4 100644 --- a/src/FlexRender.Skia.Render/Rendering/FontManager.cs +++ b/src/FlexRender.Skia.Render/Rendering/FontManager.cs @@ -2,6 +2,7 @@ using System.Globalization; using FlexRender.Abstractions; using FlexRender.Layout; +using FlexRender.Parsing.Ast; using SkiaSharp; namespace FlexRender.Rendering; @@ -13,6 +14,7 @@ namespace FlexRender.Rendering; public sealed class FontManager : IFontManager, IDisposable { private readonly ConcurrentDictionary_typefaces = new(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary _variantTypefaces = new(); private readonly ConcurrentDictionary _fontPaths = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary _fontFallbacks = new(StringComparer.OrdinalIgnoreCase); private string _defaultFallback = "Arial"; @@ -31,6 +33,79 @@ public SKTypeface GetTypeface(string fontName) return _typefaces.GetOrAdd(fontName, LoadTypeface); } + /// + /// Gets a typeface by font name and optional font family with specific weight and style. + /// Priority: if + /// The registered font name. + /// CSS-like font family name to search registered fonts and system fonts. + /// The desired font weight (100-900). + /// The desired font style (normal, italic, oblique). + ///is not "main" and not empty, resolves by registered name. + /// Otherwise, if is not empty, resolves by family name. + /// Otherwise, falls back to the default font. + /// The typeface (never null - falls back to system font). + public SKTypeface GetTypeface(string fontName, string fontFamily, FontWeight weight, FontStyle style) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + // If fontName is explicitly set (not the default "main"), use the registered name lookup + if (!string.Equals(fontName, "main", StringComparison.OrdinalIgnoreCase) + && !string.IsNullOrEmpty(fontName)) + { + return GetTypeface(fontName, weight, style); + } + + // If fontFamily is specified, search by family name + if (!string.IsNullOrEmpty(fontFamily)) + { + return GetTypefaceByFamily(fontFamily, weight, style); + } + + // Fall back to default + return GetTypeface(fontName, weight, style); + } + + ///+ /// Gets a typeface by font family name with specific weight and style. + /// Searches registered fonts by FamilyName metadata, then system fonts. + /// + /// The font family name (e.g., "Inter 18pt", "Arial"). + /// The desired font weight (100-900). + /// The desired font style (normal, italic, oblique). + ///The typeface (never null - falls back to system font). + public SKTypeface GetTypefaceByFamily(string familyName, FontWeight weight, FontStyle style) + { + ObjectDisposedException.ThrowIf(_disposed, this); + ArgumentNullException.ThrowIfNull(familyName); + + var key = new TypefaceVariantKey($"__family__{familyName}", weight, style); + return _variantTypefaces.GetOrAdd(key, _ => LoadTypefaceByFamily(familyName, weight, style)); + } + + ///+ /// Gets a typeface by font name with specific weight and style. + /// When both weight and style are their default values, delegates to + /// The font name. + /// The desired font weight (100-900). + /// The desired font style (normal, italic, oblique). + ///. + /// Otherwise, resolves the base font family name and uses to + /// match a typeface variant with the requested weight and slant. + /// The typeface (never null - falls back to system font). + public SKTypeface GetTypeface(string fontName, FontWeight weight, FontStyle style) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + // Fast path: default weight+style, use existing cache + if (weight == FontWeight.Normal && style == FontStyle.Normal) + { + return GetTypeface(fontName); + } + + var key = new TypefaceVariantKey(fontName, weight, style); + return _variantTypefaces.GetOrAdd(key, LoadTypefaceVariant); + } + ////// Factory method to load a typeface for the given font name. /// Called by GetOrAdd when the typeface is not in the cache. @@ -63,6 +138,217 @@ private SKTypeface LoadTypeface(string fontName) return SKTypeface.FromFamilyName(_defaultFallback) ?? SKTypeface.Default; } + /// + /// Loads a typeface by searching registered fonts' FamilyName metadata, then system fonts. + /// + /// The font family name to search for. + /// The desired font weight. + /// The desired font style. + ///The best matching typeface, or a fallback. + private SKTypeface LoadTypefaceByFamily(string familyName, FontWeight weight, FontStyle style) + { + var skFontStyle = ToSkFontStyle(weight, style); + var targetWeight = (int)weight; + + // 1. Search registered fonts by loading each and checking FamilyName + SKTypeface? bestRegisteredMatch = null; + var bestRegisteredWeightDiff = int.MaxValue; + + foreach (var fontPath in _fontPaths) + { + var typeface = GetTypeface(fontPath.Key); + if (!string.Equals(typeface.FamilyName, familyName, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + // Found a registered font with matching family name; now try sibling discovery for weight/style + if (weight == FontWeight.Normal && style == FontStyle.Normal) + { + return typeface; + } + + // Try to find the exact variant via the existing variant logic + var variantKey = new TypefaceVariantKey(fontPath.Key, weight, style); + var variant = _variantTypefaces.GetOrAdd(variantKey, LoadTypefaceVariant); + var weightDiff = Math.Abs((int)variant.FontStyle.Weight - targetWeight); + + if (weightDiff < bestRegisteredWeightDiff) + { + bestRegisteredMatch = variant; + bestRegisteredWeightDiff = weightDiff; + } + } + + if (bestRegisteredMatch is not null && bestRegisteredWeightDiff <= 100) + { + return bestRegisteredMatch; + } + + // 2. Try system fonts via SKFontManager + var systemMatch = SKFontManager.Default.MatchFamily(familyName, skFontStyle); + if (systemMatch is not null) + { + var weightDiff = Math.Abs((int)systemMatch.FontStyle.Weight - targetWeight); + if (string.Equals(systemMatch.FamilyName, familyName, StringComparison.OrdinalIgnoreCase) + && weightDiff <= 100) + { + return systemMatch; + } + + // System returned an unrelated font; dispose it + systemMatch.Dispose(); + } + + // 3. Return registered match even if weight is off, or fall back to default + return bestRegisteredMatch ?? GetTypeface("main"); + } + + ///+ /// Factory method to load a typeface variant with specific weight and style. + /// Resolves the base font family name from the registered font, then attempts to find + /// a matching variant through the system font manager first. If the system match returns + /// an unrelated font (different family or distant weight), scans sibling font files in + /// the same directory as the base font for a better match. + /// + /// The variant key containing font name, weight, and style. + ///The loaded typeface variant or a fallback to the base typeface. + private SKTypeface LoadTypefaceVariant(TypefaceVariantKey key) + { + var skFontStyle = ToSkFontStyle(key.Weight, key.Style); + var targetWeight = (int)key.Weight; + + // Resolve the family name from the base typeface so that + // named fonts (e.g. "main" mapped to a file) resolve correctly. + var baseTypeface = GetTypeface(key.FontName); + var familyName = baseTypeface.FamilyName; + + // 1. Try system font manager, but verify the result actually matches + var systemMatch = SKFontManager.Default.MatchFamily(familyName, skFontStyle); + if (systemMatch is not null) + { + var weightDiff = Math.Abs((int)systemMatch.FontStyle.Weight - targetWeight); + if (string.Equals(systemMatch.FamilyName, familyName, StringComparison.OrdinalIgnoreCase) + && weightDiff <= 100) + { + return systemMatch; + } + + // System returned an unrelated font; dispose and try sibling scan + systemMatch.Dispose(); + } + + // 2. Scan sibling font files in the same directory as the base font + var siblingMatch = FindSiblingTypeface(key.FontName, familyName, targetWeight, skFontStyle.Slant); + if (siblingMatch is not null) + { + return siblingMatch; + } + + // 3. Fall back to base typeface + return baseTypeface; + } + + ///+ /// Scans the directory containing the registered font file for sibling + /// The registered font name used to look up the file path. + /// The expected font family name (case-insensitive match). + /// The desired font weight (100-900). + /// The desired font slant. + ///.ttf and.otf + /// files that belong to the same font family. Returns the best weight match within 100 units + /// of the target weight with matching slant, ornull if no suitable sibling is found. + /// Rejected typefaces are disposed immediately to prevent memory leaks. + ///The best matching sibling typeface, or + private SKTypeface? FindSiblingTypeface(string fontName, string familyName, int targetWeight, SKFontStyleSlant targetSlant) + { + if (!_fontPaths.TryGetValue(fontName, out var basePath)) + { + return null; + } + + var directory = Path.GetDirectoryName(basePath); + if (string.IsNullOrEmpty(directory) || !Directory.Exists(directory)) + { + return null; + } + + SKTypeface? bestMatch = null; + var bestWeightDiff = int.MaxValue; + + var fontFiles = Directory.EnumerateFiles(directory, "*.*") + .Where(static f => + { + var ext = Path.GetExtension(f); + return ext.Equals(".ttf", StringComparison.OrdinalIgnoreCase) + || ext.Equals(".otf", StringComparison.OrdinalIgnoreCase); + }); + + foreach (var filePath in fontFiles) + { + SKTypeface? candidate = null; + try + { + candidate = SKTypeface.FromFile(filePath); + if (candidate is null) + { + continue; + } + + // Must match family name (case-insensitive) and slant + if (!string.Equals(candidate.FamilyName, familyName, StringComparison.OrdinalIgnoreCase) + || candidate.FontStyle.Slant != targetSlant) + { + candidate.Dispose(); + continue; + } + + var weightDiff = Math.Abs((int)candidate.FontStyle.Weight - targetWeight); + if (weightDiff > 100) + { + candidate.Dispose(); + continue; + } + + if (weightDiff < bestWeightDiff) + { + bestMatch?.Dispose(); + bestMatch = candidate; + bestWeightDiff = weightDiff; + } + else + { + candidate.Dispose(); + } + } + catch + { + // Corrupted or unreadable font file; skip it + candidate?.Dispose(); + } + } + + return bestMatch; + } + + ///null if none found.+ /// Converts + /// The font weight (100-900). + /// The font style (normal, italic, oblique). + ///and to an . + /// The corresponding + internal static SKFontStyle ToSkFontStyle(FontWeight weight, FontStyle style) + { + var slant = style switch + { + FontStyle.Italic => SKFontStyleSlant.Italic, + FontStyle.Oblique => SKFontStyleSlant.Oblique, + _ => SKFontStyleSlant.Upright + }; + + return new SKFontStyle((SKFontStyleWeight)(int)weight, SKFontStyleWidth.Normal, slant); + } + ///. /// Registers a font with a file path and optional fallback. /// This method is thread-safe. @@ -83,6 +369,16 @@ public bool RegisterFont(string name, string path, string? fallback = null) _typefaces.TryRemove(name, out var removedTypeface); removedTypeface?.Dispose(); + // Clear matching variant typefaces for re-registered font + foreach (var variantKey in _variantTypefaces.Keys) + { + if (string.Equals(variantKey.FontName, name, StringComparison.OrdinalIgnoreCase) + && _variantTypefaces.TryRemove(variantKey, out var variantTypeface)) + { + variantTypeface.Dispose(); + } + } + return File.Exists(path); } @@ -126,7 +422,7 @@ public float ParseFontSize(string? sizeStr, float baseFontSize, float parentSize } /// - /// Disposes all loaded typefaces. + /// Disposes all loaded typefaces (both base and variant caches). /// This method is thread-safe but should only be called once. /// public void Dispose() @@ -136,7 +432,7 @@ public void Dispose() _disposed = true; - // Dispose and remove each typeface atomically + // Dispose and remove each base typeface atomically foreach (var key in _typefaces.Keys) { if (_typefaces.TryRemove(key, out var typeface)) @@ -144,5 +440,58 @@ public void Dispose() typeface.Dispose(); } } + + // Dispose and remove each variant typeface atomically + foreach (var key in _variantTypefaces.Keys) + { + if (_variantTypefaces.TryRemove(key, out var typeface)) + { + typeface.Dispose(); + } + } + } + + ///+ /// Cache key for typeface variants identified by font name, weight, and style. + /// Uses case-insensitive font name comparison. + /// + private readonly record struct TypefaceVariantKey + { + ///The registered font name. + public string FontName { get; } + + ///The font weight (100-900). + public FontWeight Weight { get; } + + ///The font style (normal, italic, oblique). + public FontStyle Style { get; } + + ///+ /// Creates a new + /// The registered font name. + /// The font weight. + /// The font style. + public TypefaceVariantKey(string fontName, FontWeight weight, FontStyle style) + { + ArgumentNullException.ThrowIfNull(fontName); + FontName = fontName; + Weight = weight; + Style = style; + } + + ///. + /// + /// Case-insensitive equality for font names. + /// + public bool Equals(TypefaceVariantKey other) => + string.Equals(FontName, other.FontName, StringComparison.OrdinalIgnoreCase) + && Weight == other.Weight + && Style == other.Style; + + ///+ /// Case-insensitive hash code for font names. + /// + public override int GetHashCode() => + HashCode.Combine(StringComparer.OrdinalIgnoreCase.GetHashCode(FontName), Weight, Style); } } diff --git a/src/FlexRender.Skia.Render/Rendering/RenderingEngine.cs b/src/FlexRender.Skia.Render/Rendering/RenderingEngine.cs index 7205db7..ae01b2a 100644 --- a/src/FlexRender.Skia.Render/Rendering/RenderingEngine.cs +++ b/src/FlexRender.Skia.Render/Rendering/RenderingEngine.cs @@ -27,6 +27,7 @@ internal sealed class RenderingEngine private readonly ResourceLimits _limits; private readonly float _baseFontSize; private readonly FilterRegistry? _filterRegistry; + private readonly ContentParserRegistry? _contentParserRegistry; private readonly FontManager? _fontManager; private readonly FlexRenderOptions? _renderingOptions; @@ -46,6 +47,7 @@ internal sealed class RenderingEngine /// Optional filter registry for per-call culture resolution. /// Optional font manager for creating culture-aware pipelines. /// Optional rendering options for creating culture-aware pipelines. + /// Optional content parser registry for custom content type parsing. internal RenderingEngine( TextRenderer textRenderer, IContentProvider? qrProvider, @@ -59,7 +61,8 @@ internal RenderingEngine( float baseFontSize, FilterRegistry? filterRegistry = null, FontManager? fontManager = null, - FlexRenderOptions? renderingOptions = null) + FlexRenderOptions? renderingOptions = null, + ContentParserRegistry? contentParserRegistry = null) { ArgumentNullException.ThrowIfNull(textRenderer); ArgumentNullException.ThrowIfNull(pipeline); @@ -77,6 +80,7 @@ internal RenderingEngine( _limits = limits; _baseFontSize = baseFontSize; _filterRegistry = filterRegistry; + _contentParserRegistry = contentParserRegistry; _fontManager = fontManager; _renderingOptions = renderingOptions; } @@ -863,7 +867,7 @@ private TemplatePipeline ResolvePipeline( return _pipeline; } - var expander = new TemplateExpander(_limits, _filterRegistry, effectiveCulture); + var expander = new TemplateExpander(_limits, _filterRegistry, effectiveCulture, _contentParserRegistry); var processor = new TemplateProcessor(_limits, _filterRegistry, effectiveCulture); return new TemplatePipeline(expander, processor); diff --git a/src/FlexRender.Skia.Render/Rendering/SkiaRenderer.cs b/src/FlexRender.Skia.Render/Rendering/SkiaRenderer.cs index 988b736..349cc65 100644 --- a/src/FlexRender.Skia.Render/Rendering/SkiaRenderer.cs +++ b/src/FlexRender.Skia.Render/Rendering/SkiaRenderer.cs @@ -86,6 +86,7 @@ public SkiaRenderer( /// Optional configuration options for path resolution and other settings. /// Optional SVG content provider for rendering SVG elements. /// Optional filter registry for expression filter evaluation. + /// Optional content parser registry for ContentElement expansion. /// Thrown when public SkiaRenderer( ResourceLimits limits, @@ -95,7 +96,8 @@ public SkiaRenderer( bool deterministicRendering = false, FlexRenderOptions? options = null, IContentProvideris null. ? svgProvider = null, - FilterRegistry? filterRegistry = null) + FilterRegistry? filterRegistry = null, + ContentParserRegistry? contentParserRegistry = null) { ArgumentNullException.ThrowIfNull(limits); _limits = limits; @@ -104,8 +106,8 @@ public SkiaRenderer( ? new TemplateProcessor(limits, filterRegistry) : new TemplateProcessor(limits); var expander = filterRegistry is not null - ? new TemplateExpander(limits, filterRegistry) - : new TemplateExpander(limits); + ? new TemplateExpander(limits, filterRegistry, contentParserRegistry) + : new TemplateExpander(limits, contentParserRegistry); _fontManager = new FontManager(); _defaultRenderOptions = deterministicRendering ? RenderOptions.Deterministic : RenderOptions.Default; _textRenderer = new TextRenderer(_fontManager); @@ -129,7 +131,8 @@ public SkiaRenderer( BaseFontSize, filterRegistry, _fontManager, - options); + options, + contentParserRegistry); } /// diff --git a/src/FlexRender.Skia.Render/Rendering/SkiaTextShaper.cs b/src/FlexRender.Skia.Render/Rendering/SkiaTextShaper.cs index b0e0c70..1bb833d 100644 --- a/src/FlexRender.Skia.Render/Rendering/SkiaTextShaper.cs +++ b/src/FlexRender.Skia.Render/Rendering/SkiaTextShaper.cs @@ -85,7 +85,7 @@ public TextShapingResult ShapeText(TextElement element, float fontSize, float ma /// private SKFont CreateFont(TextElement element, float fontSize) { - var typeface = _fontManager.GetTypeface(element.Font.Value); + var typeface = _fontManager.GetTypeface(element.Font.Value, element.FontFamily.Value, element.FontWeight.Value, element.FontStyle.Value); var font = new SKFont(typeface, fontSize) { Subpixel = _defaultRenderOptions.SubpixelText, diff --git a/src/FlexRender.Skia.Render/Rendering/TextRenderer.cs b/src/FlexRender.Skia.Render/Rendering/TextRenderer.cs index b9ad2ee..7a1a536 100644 --- a/src/FlexRender.Skia.Render/Rendering/TextRenderer.cs +++ b/src/FlexRender.Skia.Render/Rendering/TextRenderer.cs @@ -172,7 +172,7 @@ public void DrawText( ///A configured private SKFont CreateFont(TextElement element, float baseFontSize, RenderOptions renderOptions) { - var typeface = _fontManager.GetTypeface(element.Font.Value); + var typeface = _fontManager.GetTypeface(element.Font.Value, element.FontFamily.Value, element.FontWeight.Value, element.FontStyle.Value); var fontSize = FontSizeResolver.Resolve(element.Size.Value, baseFontSize); var font = new SKFont(typeface, fontSize) diff --git a/src/FlexRender.Skia.Render/SkiaRender.cs b/src/FlexRender.Skia.Render/SkiaRender.cs index 823ac7e..2800565 100644 --- a/src/FlexRender.Skia.Render/SkiaRender.cs +++ b/src/FlexRender.Skia.Render/SkiaRender.cs @@ -61,6 +61,7 @@ public sealed class SkiaRender : IFlexRender /// Collection of resource loaders for images and other assets. /// Skia-specific configuration including content providers. /// Optional filter registry for expression filter evaluation. + /// Optional content parser registry for custom content type parsing. ///instance. Caller must dispose. /// Thrown when , , /// , or is null. @@ -70,7 +71,8 @@ internal SkiaRender( FlexRenderOptions options, IReadOnlyList resourceLoaders, SkiaBuilder skiaBuilder, - FilterRegistry? filterRegistry = null) + FilterRegistry? filterRegistry = null, + ContentParserRegistry? contentParserRegistry = null) { ArgumentNullException.ThrowIfNull(limits); ArgumentNullException.ThrowIfNull(options); @@ -104,7 +106,8 @@ internal SkiaRender( _legacyDeterministicRendering, options, svgProvider, - filterRegistry); + filterRegistry, + contentParserRegistry); _renderer.BaseFontSize = options.BaseFontSize; } diff --git a/src/FlexRender.Skia.Render/SvgBuilderSkiaExtensions.cs b/src/FlexRender.Skia.Render/SvgBuilderSkiaExtensions.cs index bcee391..d9d7df1 100644 --- a/src/FlexRender.Skia.Render/SvgBuilderSkiaExtensions.cs +++ b/src/FlexRender.Skia.Render/SvgBuilderSkiaExtensions.cs @@ -45,6 +45,8 @@ public static SvgBuilder WithSkia(this SvgBuilder svgBuilder, Action "italic", + FontStyle.Oblique => "oblique", + _ => "normal" + }).Append('"'); + } + sb.Append(" fill=\"").Append(EscapeXml(text.Color.Value)).Append('"'); if (anchor != "start") diff --git a/src/FlexRender.Svg.Render/SvgBuilderExtensions.cs b/src/FlexRender.Svg.Render/SvgBuilderExtensions.cs index e38940e..c1648e4 100644 --- a/src/FlexRender.Svg.Render/SvgBuilderExtensions.cs +++ b/src/FlexRender.Svg.Render/SvgBuilderExtensions.cs @@ -90,7 +90,8 @@ public static FlexRenderBuilder WithSvg( barcodeProvider, svgBuilder.QrSvgProvider, svgBuilder.BarcodeSvgProvider, - svgBuilder.SvgElementSvgProvider); + svgBuilder.SvgElementSvgProvider, + b.ContentParserRegistry); }); return builder; diff --git a/src/FlexRender.Svg.Render/SvgRender.cs b/src/FlexRender.Svg.Render/SvgRender.cs index 64e72f2..8e821f3 100644 --- a/src/FlexRender.Svg.Render/SvgRender.cs +++ b/src/FlexRender.Svg.Render/SvgRender.cs @@ -44,6 +44,7 @@ public sealed class SvgRender : IFlexRender /// Optional SVG-native QR code provider for vector QR code embedding. /// Optional SVG-native barcode provider for vector barcode embedding. /// Optional SVG-native SVG element provider. + /// Optional content parser registry for custom content type parsing. internal SvgRender( ResourceLimits limits, FlexRenderOptions options, @@ -52,7 +53,8 @@ internal SvgRender( IContentProvider ? barcodeProvider = null, ISvgContentProvider ? qrSvgProvider = null, ISvgContentProvider ? barcodeSvgProvider = null, - ISvgContentProvider ? svgElementSvgProvider = null) + ISvgContentProvider ? svgElementSvgProvider = null, + ContentParserRegistry? contentParserRegistry = null) { ArgumentNullException.ThrowIfNull(limits); ArgumentNullException.ThrowIfNull(options); @@ -60,7 +62,7 @@ internal SvgRender( _rasterRenderer = rasterRenderer; var templateProcessor = new TemplateProcessor(limits); - var expander = new TemplateExpander(limits); + var expander = new TemplateExpander(limits, contentParserRegistry); var pipeline = new TemplatePipeline(expander, templateProcessor); var layoutEngine = new LayoutEngine(limits); layoutEngine.TextShaper = new ApproximateTextShaper(); diff --git a/src/FlexRender.Yaml/Parsing/ElementParsers.cs b/src/FlexRender.Yaml/Parsing/ElementParsers.cs index df6f7e8..056694e 100644 --- a/src/FlexRender.Yaml/Parsing/ElementParsers.cs +++ b/src/FlexRender.Yaml/Parsing/ElementParsers.cs @@ -182,6 +182,7 @@ internal static TemplateElement ParseTextElement(YamlMappingNode node) { Content = GetStringValue(node, "content", ""), Font = GetExprStringValue(node, "font", "main"), + FontFamily = GetExprStringValueOptional(node, "fontFamily", "font-family"), Size = GetExprStringValue(node, "size", "1em"), Color = GetExprStringValue(node, "color", "#000000"), Wrap = GetExprBoolValue(node, "wrap", true), @@ -227,6 +228,44 @@ internal static TemplateElement ParseTextElement(YamlMappingNode node) }; } + var fontWeightStr = GetStringValue(node, "fontWeight", "normal"); + if (ContainsExpression(fontWeightStr)) + { + text.FontWeight = ExprValue .Expression(fontWeightStr); + } + else + { + text.FontWeight = fontWeightStr.ToLowerInvariant() switch + { + "thin" or "100" => FontWeight.Thin, + "extralight" or "extra-light" or "200" => FontWeight.ExtraLight, + "light" or "300" => FontWeight.Light, + "normal" or "regular" or "400" => FontWeight.Normal, + "medium" or "500" => FontWeight.Medium, + "semibold" or "semi-bold" or "600" => FontWeight.SemiBold, + "bold" or "700" => FontWeight.Bold, + "extrabold" or "extra-bold" or "800" => FontWeight.ExtraBold, + "black" or "900" => FontWeight.Black, + _ => FontWeight.Normal + }; + } + + var fontStyleStr = GetStringValue(node, "fontStyle", "normal"); + if (ContainsExpression(fontStyleStr)) + { + text.FontStyle = ExprValue .Expression(fontStyleStr); + } + else + { + text.FontStyle = fontStyleStr.ToLowerInvariant() switch + { + "normal" => FontStyle.Normal, + "italic" => FontStyle.Italic, + "oblique" => FontStyle.Oblique, + _ => FontStyle.Normal + }; + } + ApplyFlexItemProperties(node, text); return text; } @@ -333,6 +372,26 @@ internal TemplateElement ParseFlexElement(YamlMappingNode node) return flex; } + /// + /// Parses a content element from YAML. + /// + /// The YAML node containing the content element definition. + ///The parsed content element. + internal static TemplateElement ParseContentElement(YamlMappingNode node) + { + var content = new ContentElement + { + Source = GetExprStringValue(node, "source", ""), + Format = GetExprStringValue(node, "format", ""), + Rotate = GetExprStringValue(node, "rotate", "none"), + Padding = GetExprStringValue(node, "padding", "0"), + Margin = GetExprStringValue(node, "margin", "0") + }; + + ApplyFlexItemProperties(node, content); + return content; + } + ////// Parses a QR code element from YAML. /// @@ -745,6 +804,9 @@ internal static TemplateElement ParseTableElement(YamlMappingNode node) RowGap = GetStringValue(node, "rowGap") ?? GetStringValue(node, "row-gap"), ColumnGap = GetStringValue(node, "columnGap") ?? GetStringValue(node, "column-gap"), HeaderFont = GetStringValue(node, "headerFont") ?? GetStringValue(node, "header-font"), + HeaderFontWeight = ParseOptionalFontWeight(node, "headerFontWeight", "header-fontWeight"), + HeaderFontStyle = ParseOptionalFontStyle(node, "headerFontStyle", "header-fontStyle"), + HeaderFontFamily = GetStringValue(node, "headerFontFamily") ?? GetStringValue(node, "header-fontFamily"), HeaderColor = GetStringValue(node, "headerColor") ?? GetStringValue(node, "header-color"), HeaderSize = GetStringValue(node, "headerSize") ?? GetStringValue(node, "header-size"), HeaderBorderBottom = GetStringValue(node, "headerBorderBottom") ?? GetStringValue(node, "header-border-bottom"), @@ -844,6 +906,54 @@ private static ListParseTableRows(YamlSequenceNode sequence, IReadOnl return rows; } + /// + /// Parses an optional font weight value from a YAML mapping node, trying two key variants. + /// + /// The YAML mapping node to read from. + /// The primary key (camelCase). + /// The alternate key (kebab-case). + ///The parsed + private static FontWeight? ParseOptionalFontWeight(YamlMappingNode node, string key1, string key2) + { + var raw = GetStringValue(node, key1) ?? GetStringValue(node, key2); + if (raw is null) return null; + + return raw.ToLowerInvariant() switch + { + "thin" or "100" => FontWeight.Thin, + "extralight" or "extra-light" or "200" => FontWeight.ExtraLight, + "light" or "300" => FontWeight.Light, + "normal" or "regular" or "400" => FontWeight.Normal, + "medium" or "500" => FontWeight.Medium, + "semibold" or "semi-bold" or "600" => FontWeight.SemiBold, + "bold" or "700" => FontWeight.Bold, + "extrabold" or "extra-bold" or "800" => FontWeight.ExtraBold, + "black" or "900" => FontWeight.Black, + _ => null + }; + } + + ///, or null if the key is absent or unrecognized.+ /// Parses an optional font style value from a YAML node, trying two key variants. + /// + /// The YAML mapping node to read from. + /// The primary key name (camelCase). + /// The secondary key name (kebab-case). + ///The parsed + private static FontStyle? ParseOptionalFontStyle(YamlMappingNode node, string key1, string key2) + { + var raw = GetStringValue(node, key1) ?? GetStringValue(node, key2); + if (raw is null) return null; + + return raw.ToLowerInvariant() switch + { + "normal" => FontStyle.Normal, + "italic" => FontStyle.Italic, + "oblique" => FontStyle.Oblique, + _ => null + }; + } + ///value, or null if not present or unrecognized./// Parses a sequence of child elements from a named key in a YAML mapping node. /// diff --git a/src/FlexRender.Yaml/Parsing/TemplateParser.cs b/src/FlexRender.Yaml/Parsing/TemplateParser.cs index 9d5067c..856284c 100644 --- a/src/FlexRender.Yaml/Parsing/TemplateParser.cs +++ b/src/FlexRender.Yaml/Parsing/TemplateParser.cs @@ -63,7 +63,8 @@ public TemplateParser(ResourceLimits limits) ["each"] = _parsers.ParseEachElement, ["if"] = _parsers.ParseIfElement, ["table"] = ElementParsers.ParseTableElement, - ["svg"] = ElementParsers.ParseSvgElement + ["svg"] = ElementParsers.ParseSvgElement, + ["content"] = ElementParsers.ParseContentElement }; } @@ -121,10 +122,17 @@ public Template Parse(string content) template.Culture = GetStringValue(templateNode, "culture"); } - // Parse fonts section (optional) - if (TryGetMapping(root, "fonts", out var fontsNode)) + // Parse fonts section (optional) — supports both mapping and list formats + var fontsKey = new YamlScalarNode("fonts"); + if (root.Children.TryGetValue(fontsKey, out var fontsYamlNode)) { - template.Fonts = ParseFonts(fontsNode); + template.Fonts = fontsYamlNode switch + { + YamlMappingNode fontsMapping => ParseFonts(fontsMapping), + YamlSequenceNode fontsSequence => ParseFontsList(fontsSequence), + _ => throw new TemplateParseException( + "Invalid 'fonts' section. Expected a mapping (name: path) or a list of font entries.") + }; } // Parse canvas (required) @@ -286,6 +294,72 @@ private static DictionaryParseFonts(YamlMappingNode nod return fonts; } + /// + /// Parses the fonts section when provided as a YAML sequence (list format). + /// Supports two item formats: + /// - Simple string: + /// The YAML sequence node containing font entries. + ///- "path/to/font.ttf" + /// - Object:- { path: "...", name: "heading", fallback: "Arial" } + /// The first font in the list automatically becomes "default" if no font is explicitly named "default". + ///A dictionary mapping font names to their definitions. + private static DictionaryParseFontsList(YamlSequenceNode node) + { + var fonts = new Dictionary (StringComparer.OrdinalIgnoreCase); + var index = 0; + var hasDefault = false; + + foreach (var item in node.Children) + { + string? name = null; + string path; + string? fallback = null; + + switch (item) + { + // Simple string: - "path/to/font.ttf" + case YamlScalarNode scalar: + path = scalar.Value ?? string.Empty; + break; + + // Object: - { path: "...", name: "heading", fallback: "Arial" } + case YamlMappingNode mapping: + path = GetStringValue(mapping, "path") ?? string.Empty; + name = GetStringValue(mapping, "name"); + fallback = GetStringValue(mapping, "fallback"); + break; + + default: + continue; + } + + if (string.IsNullOrEmpty(path)) + continue; + + // Auto-assign name if not provided + if (string.IsNullOrEmpty(name)) + { + // First unnamed font becomes "default" + if (!hasDefault && !fonts.ContainsKey("default")) + { + name = "default"; + } + else + { + name = $"__font_{index}"; + } + } + + if (string.Equals(name, "default", StringComparison.OrdinalIgnoreCase)) + hasDefault = true; + + fonts[name] = new FontDefinition(path, fallback); + index++; + } + + return fonts; + } + /// /// Parses a sequence of template elements. /// diff --git a/tests/FlexRender.ImageSharp.Tests/Fonts/Inter-Bold.ttf b/tests/FlexRender.ImageSharp.Tests/Fonts/Inter-Bold.ttf new file mode 100755 index 0000000..a787792 --- /dev/null +++ b/tests/FlexRender.ImageSharp.Tests/Fonts/Inter-Bold.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:30a5c45ec23a594af2effe8d3b589ad22c2dede27441050a1604d00ff82fd0dc +size 344152 diff --git a/tests/FlexRender.ImageSharp.Tests/Rendering/ImageSharpFontManagerTests.cs b/tests/FlexRender.ImageSharp.Tests/Rendering/ImageSharpFontManagerTests.cs index d86d074..f2d7998 100644 --- a/tests/FlexRender.ImageSharp.Tests/Rendering/ImageSharpFontManagerTests.cs +++ b/tests/FlexRender.ImageSharp.Tests/Rendering/ImageSharpFontManagerTests.cs @@ -1,5 +1,8 @@ using FlexRender.ImageSharp.Rendering; +using SixLabors.Fonts; using Xunit; +using AstFontWeight = FlexRender.Parsing.Ast.FontWeight; +using AstFontStyle = FlexRender.Parsing.Ast.FontStyle; namespace FlexRender.ImageSharp.Tests.Rendering; @@ -7,11 +10,13 @@ public sealed class ImageSharpFontManagerTests : IDisposable { private readonly ImageSharpFontManager _manager = new(); private readonly string _fontPath; + private readonly string _boldFontPath; public ImageSharpFontManagerTests() { var assemblyDir = Path.GetDirectoryName(typeof(ImageSharpFontManagerTests).Assembly.Location)!; _fontPath = Path.Combine(assemblyDir, "Fonts", "Inter-Regular.ttf"); + _boldFontPath = Path.Combine(assemblyDir, "Fonts", "Inter-Bold.ttf"); } [Fact] @@ -61,6 +66,73 @@ public void GetFontFamily_RegisteredFont_ReturnsFamily() Assert.NotEqual(default, family); } + [Fact] + public void GetFont_WithBoldWeight_ReturnsBoldVariantFromSiblingFile() + { + // Register only the Regular font — Bold sibling is in the same directory + _manager.RegisterFont("default", _fontPath); + + var font = _manager.GetFont("default", 16f, AstFontWeight.Bold, AstFontStyle.Normal); + + Assert.NotNull(font); + Assert.Equal(16f, font.Size); + // The font should be created with Bold style from the shared collection + Assert.True(font.IsBold, "Expected Bold font from sibling Inter-Bold.ttf"); + } + + [Fact] + public void GetFont_WithNormalWeight_DoesNotUseSiblingScanning() + { + _manager.RegisterFont("default", _fontPath); + + var font = _manager.GetFont("default", 14f, AstFontWeight.Normal, AstFontStyle.Normal); + + Assert.NotNull(font); + Assert.Equal(14f, font.Size); + // Should use the isolated collection, returning Regular + Assert.False(font.IsBold); + } + + [Fact] + public void GetFont_WithBoldWeight_NoSiblingFiles_FallsBackToIsolatedCollection() + { + // Create a temp directory with only the Regular font (no Bold sibling) + var tempDir = Path.Combine(Path.GetTempPath(), $"FontTest_{Guid.NewGuid():N}"); + Directory.CreateDirectory(tempDir); + try + { + var tempFontPath = Path.Combine(tempDir, "Inter-Regular.ttf"); + File.Copy(_fontPath, tempFontPath); + + _manager.RegisterFont("isolated", tempFontPath); + + // Should still return a font (fallback), not throw + var font = _manager.GetFont("isolated", 16f, AstFontWeight.Bold, AstFontStyle.Normal); + Assert.NotNull(font); + Assert.Equal(16f, font.Size); + } + finally + { + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void GetFont_WithBoldWeight_CalledTwice_UsesCachedSharedCollection() + { + _manager.RegisterFont("default", _fontPath); + + var font1 = _manager.GetFont("default", 16f, AstFontWeight.Bold, AstFontStyle.Normal); + var font2 = _manager.GetFont("default", 20f, AstFontWeight.Bold, AstFontStyle.Normal); + + Assert.NotNull(font1); + Assert.NotNull(font2); + Assert.True(font1.IsBold); + Assert.True(font2.IsBold); + Assert.Equal(16f, font1.Size); + Assert.Equal(20f, font2.Size); + } + public void Dispose() { _manager.Dispose(); diff --git a/tests/FlexRender.Tests/Configuration/FlexRenderBuilderContentParserTests.cs b/tests/FlexRender.Tests/Configuration/FlexRenderBuilderContentParserTests.cs new file mode 100644 index 0000000..4e34815 --- /dev/null +++ b/tests/FlexRender.Tests/Configuration/FlexRenderBuilderContentParserTests.cs @@ -0,0 +1,54 @@ +using FlexRender.Abstractions; +using FlexRender.Configuration; +using FlexRender.Parsing.Ast; +using Xunit; + +namespace FlexRender.Tests.Configuration; + +public sealed class FlexRenderBuilderContentParserTests +{ + [Fact] + public void WithContentParser_RegistersParser() + { + var builder = new FlexRenderBuilder(); + + builder.WithContentParser(new StubContentParser("markdown")); + + Assert.NotNull(builder.ContentParserRegistry); + Assert.NotNull(builder.ContentParserRegistry!.GetParser("markdown")); + } + + [Fact] + public void WithContentParser_MultipleParsers_RegistersAll() + { + var builder = new FlexRenderBuilder(); + + builder.WithContentParser(new StubContentParser("markdown")); + builder.WithContentParser(new StubContentParser("escpos")); + + Assert.NotNull(builder.ContentParserRegistry!.GetParser("markdown")); + Assert.NotNull(builder.ContentParserRegistry!.GetParser("escpos")); + } + + [Fact] + public void WithContentParser_Null_ThrowsArgumentNull() + { + var builder = new FlexRenderBuilder(); + + Assert.Throws(() => builder.WithContentParser(null!)); + } + + [Fact] + public void ContentParserRegistry_DefaultsToNull() + { + var builder = new FlexRenderBuilder(); + + Assert.Null(builder.ContentParserRegistry); + } + + private sealed class StubContentParser(string formatName) : IContentParser + { + public string FormatName => formatName; + public IReadOnlyList Parse(string text) => []; + } +} diff --git a/tests/FlexRender.Tests/Integration/ContentElementIntegrationTests.cs b/tests/FlexRender.Tests/Integration/ContentElementIntegrationTests.cs new file mode 100644 index 0000000..86bdc5c --- /dev/null +++ b/tests/FlexRender.Tests/Integration/ContentElementIntegrationTests.cs @@ -0,0 +1,86 @@ +using FlexRender.Abstractions; +using FlexRender.Configuration; +using FlexRender.Parsing.Ast; +using FlexRender.TemplateEngine; +using Xunit; + +namespace FlexRender.Tests.Integration; + +/// +/// Integration tests that validate ContentElement works through the full +/// TemplatePipeline (Expand -> Resolve -> Materialize). +/// +public sealed class ContentElementIntegrationTests +{ + [Fact] + public void FullPipeline_ContentElement_ExpandsAndProcesses() + { + // Arrange: a parser that returns text elements + var registry = new ContentParserRegistry(); + registry.Register(new SimpleTestParser()); + + var expander = new TemplateExpander(new ResourceLimits(), contentParserRegistry: registry); + var processor = new TemplateProcessor(new ResourceLimits()); + var pipeline = new TemplatePipeline(expander, processor); + + var template = new Template + { + Name = "test", + Version = 1, + Canvas = new CanvasSettings { Width = 300 }, + Elements = + [ + new FlexElement + { + Children = + [ + new TextElement { Content = "Header" }, + new ContentElement { Source = "{{receiptBody}}", Format = "simple" }, + new TextElement { Content = "Footer" } + ] + } + ] + }; + + var data = new ObjectValue + { + ["receiptBody"] = new StringValue("Item 1|10.00\nItem 2|20.00") + }; + + // Act + var result = pipeline.Process(template, data); + + // Assert + var flex = Assert.IsType(result.Elements[0]); + Assert.Equal(4, flex.Children.Count); // Header + 2 items + Footer + Assert.Equal("Header", ((TextElement)flex.Children[0]).Content.Value); + Assert.Equal("Item 1: 10.00", ((TextElement)flex.Children[1]).Content.Value); + Assert.Equal("Item 2: 20.00", ((TextElement)flex.Children[2]).Content.Value); + Assert.Equal("Footer", ((TextElement)flex.Children[3]).Content.Value); + } + + /// + /// Simple test parser: splits lines, each line is "name|price" -> TextElement "name: price". + /// + private sealed class SimpleTestParser : IContentParser + { + ///+ public string FormatName => "simple"; + + /// + public IReadOnlyList Parse(string text) + { + ArgumentNullException.ThrowIfNull(text); + if (string.IsNullOrWhiteSpace(text)) return []; + + return text.Split('\n', StringSplitOptions.RemoveEmptyEntries) + .Select(line => + { + var parts = line.Split('|'); + var content = parts.Length == 2 ? $"{parts[0]}: {parts[1]}" : line; + return (TemplateElement)new TextElement { Content = content }; + }) + .ToList(); + } + } +} diff --git a/tests/FlexRender.Tests/Parsing/Ast/ContentElementTests.cs b/tests/FlexRender.Tests/Parsing/Ast/ContentElementTests.cs new file mode 100644 index 0000000..58dda52 --- /dev/null +++ b/tests/FlexRender.Tests/Parsing/Ast/ContentElementTests.cs @@ -0,0 +1,73 @@ +using FlexRender.Parsing.Ast; +using Xunit; + +namespace FlexRender.Tests.Parsing.Ast; + +/// +/// Tests for ContentElement AST model. +/// +public sealed class ContentElementTests +{ + ///+ /// Verifies that the element type is Content. + /// + [Fact] + public void Type_ReturnsContent() + { + var element = new ContentElement(); + Assert.Equal(ElementType.Content, element.Type); + } + + ///+ /// Verifies that Source defaults to an empty string. + /// + [Fact] + public void Source_DefaultsToEmptyString() + { + var element = new ContentElement(); + Assert.Equal("", element.Source.Value); + } + + ///+ /// Verifies that Format defaults to an empty string. + /// + [Fact] + public void Format_DefaultsToEmptyString() + { + var element = new ContentElement(); + Assert.Equal("", element.Format.Value); + } + + ///+ /// Verifies that ResolveExpressions resolves Source and Format expressions, + /// and Materialize populates the typed values. + /// + [Fact] + public void ResolveExpressions_ResolvesSourceAndFormat() + { + var element = new ContentElement + { + Source = ExprValue.Expression("{{body}}"), + Format = ExprValue .Expression("{{fmt}}") + }; + + var data = new ObjectValue + { + ["body"] = new StringValue("hello world"), + ["fmt"] = new StringValue("markdown") + }; + + element.ResolveExpressions( + (raw, _) => raw.Replace("{{body}}", "hello world").Replace("{{fmt}}", "markdown"), + data); + + // After resolve, raw values are populated + Assert.Equal("hello world", element.Source.RawValue); + Assert.Equal("markdown", element.Format.RawValue); + + // After materialize, typed values are populated + element.Materialize(); + Assert.Equal("hello world", element.Source.Value); + Assert.Equal("markdown", element.Format.Value); + } +} diff --git a/tests/FlexRender.Tests/Parsing/TemplateParserContentTests.cs b/tests/FlexRender.Tests/Parsing/TemplateParserContentTests.cs new file mode 100644 index 0000000..adb9409 --- /dev/null +++ b/tests/FlexRender.Tests/Parsing/TemplateParserContentTests.cs @@ -0,0 +1,117 @@ +using FlexRender.Parsing; +using FlexRender.Parsing.Ast; +using Xunit; + +namespace FlexRender.Tests.Parsing; + +/// +/// Tests for TemplateParser ContentElement parsing. +/// +public sealed class TemplateParserContentTests +{ + private readonly TemplateParser _parser = new(); + + ///+ /// Verifies that source and format are parsed correctly from a content element. + /// + [Fact] + public void Parse_ContentElement_ParsesSourceAndFormat() + { + const string yaml = """ + template: + name: test + version: 1 + canvas: + width: 200 + layout: + - type: content + source: "hello world" + format: markdown + """; + + var template = _parser.Parse(yaml); + + Assert.Single(template.Elements); + var content = Assert.IsType(template.Elements[0]); + Assert.Equal("hello world", content.Source.Value); + Assert.Equal("markdown", content.Format.Value); + } + + /// + /// Verifies that expression-based source and format are detected as expressions. + /// + [Fact] + public void Parse_ContentElement_WithExpressions() + { + const string yaml = """ + template: + name: test + version: 1 + canvas: + width: 200 + layout: + - type: content + source: "{{body}}" + format: "{{fmt}}" + """; + + var template = _parser.Parse(yaml); + + var content = Assert.IsType(template.Elements[0]); + Assert.True(content.Source.IsExpression); + Assert.True(content.Format.IsExpression); + } + + /// + /// Verifies that format defaults to an empty string when not specified. + /// + [Fact] + public void Parse_ContentElement_DefaultFormat() + { + const string yaml = """ + template: + name: test + version: 1 + canvas: + width: 200 + layout: + - type: content + source: "some text" + """; + + var template = _parser.Parse(yaml); + + var content = Assert.IsType(template.Elements[0]); + Assert.Equal("some text", content.Source.Value); + Assert.Equal("", content.Format.Value); + } + + /// + /// Verifies that flex-item properties (padding, margin, grow) are inherited correctly. + /// + [Fact] + public void Parse_ContentElement_InheritsFlexItemProperties() + { + const string yaml = """ + template: + name: test + version: 1 + canvas: + width: 200 + layout: + - type: content + source: "text" + format: markdown + padding: "10" + margin: "5" + grow: 1 + """; + + var template = _parser.Parse(yaml); + + var content = Assert.IsType(template.Elements[0]); + Assert.Equal("10", content.Padding.Value); + Assert.Equal("5", content.Margin.Value); + Assert.Equal(1, content.Grow.Value); + } +} diff --git a/tests/FlexRender.Tests/Parsing/TemplateParserFontTests.cs b/tests/FlexRender.Tests/Parsing/TemplateParserFontTests.cs index 30ca6e7..93d8ddc 100644 --- a/tests/FlexRender.Tests/Parsing/TemplateParserFontTests.cs +++ b/tests/FlexRender.Tests/Parsing/TemplateParserFontTests.cs @@ -310,4 +310,184 @@ public void Parse_TextWithoutFont_UsesMainAsDefault() var textElement = Assert.IsType (template.Elements[0]); Assert.Equal("main", textElement.Font); } + + // ── List format tests ─────────────────────────────────────────────── + + /// + /// Verifies that a simple list of font paths is parsed, with the first becoming "default". + /// + [Fact] + public void Parse_FontsList_SimpleStrings_FirstBecomesDefault() + { + const string yaml = """ + fonts: + - "assets/fonts/Inter-Regular.ttf" + - "assets/fonts/Inter-Bold.ttf" + canvas: + width: 300 + """; + + var template = _parser.Parse(yaml); + + Assert.Equal(2, template.Fonts.Count); + Assert.True(template.Fonts.ContainsKey("default")); + Assert.Equal("assets/fonts/Inter-Regular.ttf", template.Fonts["default"].Path); + Assert.True(template.Fonts.ContainsKey("__font_1")); + Assert.Equal("assets/fonts/Inter-Bold.ttf", template.Fonts["__font_1"].Path); + } + + ///+ /// Verifies that object entries with explicit name are registered under that name. + /// + [Fact] + public void Parse_FontsList_ObjectWithName_UsesProvidedName() + { + const string yaml = """ + fonts: + - path: "assets/fonts/Roboto-Regular.ttf" + name: heading + fallback: "Arial" + canvas: + width: 300 + """; + + var template = _parser.Parse(yaml); + + Assert.Single(template.Fonts); + Assert.True(template.Fonts.ContainsKey("heading")); + Assert.Equal("assets/fonts/Roboto-Regular.ttf", template.Fonts["heading"].Path); + Assert.Equal("Arial", template.Fonts["heading"].Fallback); + } + + ///+ /// Verifies that mixed string and object entries are parsed correctly in list format. + /// + [Fact] + public void Parse_FontsList_MixedEntries_ParsesAllCorrectly() + { + const string yaml = """ + fonts: + - "assets/fonts/Inter-Regular.ttf" + - "assets/fonts/Inter-Bold.ttf" + - path: "assets/fonts/Roboto-Regular.ttf" + name: heading + fallback: "Arial" + canvas: + width: 300 + """; + + var template = _parser.Parse(yaml); + + Assert.Equal(3, template.Fonts.Count); + Assert.Equal("assets/fonts/Inter-Regular.ttf", template.Fonts["default"].Path); + Assert.Null(template.Fonts["default"].Fallback); + Assert.Equal("assets/fonts/Inter-Bold.ttf", template.Fonts["__font_1"].Path); + Assert.Equal("assets/fonts/Roboto-Regular.ttf", template.Fonts["heading"].Path); + Assert.Equal("Arial", template.Fonts["heading"].Fallback); + } + + ///+ /// Verifies that when a font is explicitly named "default", no auto-default is assigned. + /// + [Fact] + public void Parse_FontsList_ExplicitDefault_NoAutoDefault() + { + const string yaml = """ + fonts: + - "assets/fonts/Inter-Regular.ttf" + - path: "assets/fonts/Roboto-Regular.ttf" + name: default + canvas: + width: 300 + """; + + var template = _parser.Parse(yaml); + + // The first string got auto-assigned "default", then the explicit "default" overwrites it + Assert.Equal("assets/fonts/Roboto-Regular.ttf", template.Fonts["default"].Path); + } + + ///+ /// Verifies that an empty list results in an empty fonts dictionary. + /// + [Fact] + public void Parse_FontsList_EmptyList_ReturnsEmptyDictionary() + { + const string yaml = """ + fonts: [] + canvas: + width: 300 + """; + + var template = _parser.Parse(yaml); + + Assert.NotNull(template.Fonts); + Assert.Empty(template.Fonts); + } + + ///+ /// Verifies that entries with empty paths are skipped. + /// + [Fact] + public void Parse_FontsList_EmptyPath_SkipsEntry() + { + const string yaml = """ + fonts: + - "" + - "assets/fonts/Inter-Regular.ttf" + canvas: + width: 300 + """; + + var template = _parser.Parse(yaml); + + Assert.Single(template.Fonts); + Assert.True(template.Fonts.ContainsKey("default")); + Assert.Equal("assets/fonts/Inter-Regular.ttf", template.Fonts["default"].Path); + } + + ///+ /// Verifies that font list names are case-insensitive. + /// + [Fact] + public void Parse_FontsList_NamesCaseInsensitive() + { + const string yaml = """ + fonts: + - path: "assets/fonts/Roboto-Regular.ttf" + name: Heading + canvas: + width: 300 + """; + + var template = _parser.Parse(yaml); + + Assert.True(template.Fonts.ContainsKey("heading")); + Assert.True(template.Fonts.ContainsKey("Heading")); + Assert.True(template.Fonts.ContainsKey("HEADING")); + } + + ///+ /// Verifies that the original dictionary format still works after adding list support. + /// + [Fact] + public void Parse_FontsDictionaryFormat_StillWorks() + { + const string yaml = """ + fonts: + main: "assets/fonts/Roboto-Regular.ttf" + heading: + path: "assets/fonts/OpenSans-Bold.ttf" + fallback: "Helvetica" + canvas: + width: 300 + """; + + var template = _parser.Parse(yaml); + + Assert.Equal(2, template.Fonts.Count); + Assert.Equal("assets/fonts/Roboto-Regular.ttf", template.Fonts["main"].Path); + Assert.Equal("assets/fonts/OpenSans-Bold.ttf", template.Fonts["heading"].Path); + Assert.Equal("Helvetica", template.Fonts["heading"].Fallback); + } } diff --git a/tests/FlexRender.Tests/Parsing/TextElementFontWeightStyleTests.cs b/tests/FlexRender.Tests/Parsing/TextElementFontWeightStyleTests.cs new file mode 100644 index 0000000..e0e5581 --- /dev/null +++ b/tests/FlexRender.Tests/Parsing/TextElementFontWeightStyleTests.cs @@ -0,0 +1,109 @@ +using FlexRender.Parsing; +using FlexRender.Parsing.Ast; +using Xunit; + +namespace FlexRender.Tests.Parsing; + +public sealed class TextElementFontWeightStyleTests +{ + private readonly TemplateParser _parser = new(); + + [Fact] + public void FontWeight_DefaultsToNormal() + { + var text = new TextElement(); + Assert.Equal(FontWeight.Normal, text.FontWeight.Value); + } + + [Fact] + public void FontStyle_DefaultsToNormal() + { + var text = new TextElement(); + Assert.Equal(FontStyle.Normal, text.FontStyle.Value); + } + + [Theory] + [InlineData("bold", FontWeight.Bold)] + [InlineData("700", FontWeight.Bold)] + [InlineData("light", FontWeight.Light)] + [InlineData("300", FontWeight.Light)] + [InlineData("normal", FontWeight.Normal)] + [InlineData("400", FontWeight.Normal)] + [InlineData("thin", FontWeight.Thin)] + [InlineData("100", FontWeight.Thin)] + [InlineData("black", FontWeight.Black)] + [InlineData("900", FontWeight.Black)] + [InlineData("semi-bold", FontWeight.SemiBold)] + [InlineData("600", FontWeight.SemiBold)] + public void Parse_FontWeight_ParsesCorrectly(string yamlValue, FontWeight expected) + { + var yaml = $""" + canvas: + width: 200 + layout: + - type: text + content: "test" + fontWeight: "{yamlValue}" + """; + + var template = _parser.Parse(yaml); + var text = Assert.IsType(template.Elements[0]); + Assert.Equal(expected, text.FontWeight.Value); + } + + [Theory] + [InlineData("normal", FontStyle.Normal)] + [InlineData("italic", FontStyle.Italic)] + [InlineData("oblique", FontStyle.Oblique)] + public void Parse_FontStyle_ParsesCorrectly(string yamlValue, FontStyle expected) + { + var yaml = $""" + canvas: + width: 200 + layout: + - type: text + content: "test" + fontStyle: "{yamlValue}" + """; + + var template = _parser.Parse(yaml); + var text = Assert.IsType (template.Elements[0]); + Assert.Equal(expected, text.FontStyle.Value); + } + + [Fact] + public void Parse_FontWeight_WithExpression() + { + var yaml = """ + canvas: + width: 200 + layout: + - type: text + content: "test" + fontWeight: "{{weight}}" + """; + + var template = _parser.Parse(yaml); + var text = Assert.IsType (template.Elements[0]); + Assert.True(text.FontWeight.IsExpression); + } + + [Fact] + public void Parse_BothFontWeightAndStyle() + { + var yaml = """ + canvas: + width: 200 + layout: + - type: text + content: "Bold italic text" + fontWeight: bold + fontStyle: italic + """; + + var template = _parser.Parse(yaml); + var text = Assert.IsType (template.Elements[0]); + Assert.Equal(FontWeight.Bold, text.FontWeight.Value); + Assert.Equal(FontStyle.Italic, text.FontStyle.Value); + } +} diff --git a/tests/FlexRender.Tests/Rendering/FontManagerTests.cs b/tests/FlexRender.Tests/Rendering/FontManagerTests.cs index 7803d9b..15b022e 100644 --- a/tests/FlexRender.Tests/Rendering/FontManagerTests.cs +++ b/tests/FlexRender.Tests/Rendering/FontManagerTests.cs @@ -1,5 +1,8 @@ +using FlexRender.Parsing.Ast; using FlexRender.Rendering; +using SkiaSharp; using Xunit; +using AstFontStyle = global::FlexRender.Parsing.Ast.FontStyle; namespace FlexRender.Tests.Rendering; @@ -159,4 +162,133 @@ public void GetTypeface_EmptyString_ReturnsNonNull() Assert.NotNull(typeface); } + + [Fact] + public void GetTypeface_WithDefaultWeightAndStyle_ReturnsSameAsBasic() + { + var basic = _fontManager.GetTypeface("main"); + var withDefaults = _fontManager.GetTypeface("main", FontWeight.Normal, AstFontStyle.Normal); + + // Default weight+style should delegate to the basic overload and return the same instance + Assert.Same(basic, withDefaults); + } + + [Fact] + public void GetTypeface_WithBoldWeight_ReturnsNonNull() + { + var typeface = _fontManager.GetTypeface("main", FontWeight.Bold, AstFontStyle.Normal); + + Assert.NotNull(typeface); + } + + [Fact] + public void GetTypeface_WithItalicStyle_ReturnsNonNull() + { + var typeface = _fontManager.GetTypeface("main", FontWeight.Normal, AstFontStyle.Italic); + + Assert.NotNull(typeface); + } + + [Fact] + public void GetTypeface_WithBoldItalic_ReturnsNonNull() + { + var typeface = _fontManager.GetTypeface("main", FontWeight.Bold, AstFontStyle.Italic); + + Assert.NotNull(typeface); + } + + [Fact] + public void GetTypeface_WithObliqueStyle_ReturnsNonNull() + { + var typeface = _fontManager.GetTypeface("main", FontWeight.Normal, AstFontStyle.Oblique); + + Assert.NotNull(typeface); + } + + [Theory] + [InlineData(FontWeight.Thin)] + [InlineData(FontWeight.ExtraLight)] + [InlineData(FontWeight.Light)] + [InlineData(FontWeight.Medium)] + [InlineData(FontWeight.SemiBold)] + [InlineData(FontWeight.ExtraBold)] + [InlineData(FontWeight.Black)] + public void GetTypeface_AllWeights_ReturnNonNull(FontWeight weight) + { + var typeface = _fontManager.GetTypeface("main", weight, AstFontStyle.Normal); + + Assert.NotNull(typeface); + } + + [Fact] + public void GetTypeface_SameVariantTwice_ReturnsSameInstance() + { + var first = _fontManager.GetTypeface("main", FontWeight.Bold, AstFontStyle.Italic); + var second = _fontManager.GetTypeface("main", FontWeight.Bold, AstFontStyle.Italic); + + Assert.Same(first, second); + } + + [Fact] + public void GetTypeface_DifferentVariants_MayReturnDifferentInstances() + { + var normal = _fontManager.GetTypeface("main", FontWeight.Normal, AstFontStyle.Normal); + var bold = _fontManager.GetTypeface("main", FontWeight.Bold, AstFontStyle.Normal); + + // Both should be non-null; they may or may not be different typefaces + // depending on system fonts, but they should not throw + Assert.NotNull(normal); + Assert.NotNull(bold); + } + + [Fact] + public void GetTypeface_VariantCaseInsensitive_ReturnsSameInstance() + { + var lower = _fontManager.GetTypeface("main", FontWeight.Bold, AstFontStyle.Normal); + var upper = _fontManager.GetTypeface("MAIN", FontWeight.Bold, AstFontStyle.Normal); + + Assert.Same(lower, upper); + } + + [Fact] + public void ToSkFontStyle_NormalDefaults_ReturnsUpright400() + { + var skStyle = FontManager.ToSkFontStyle(FontWeight.Normal, AstFontStyle.Normal); + + Assert.Equal((int)SKFontStyleWeight.Normal, skStyle.Weight); + Assert.Equal(SKFontStyleSlant.Upright, skStyle.Slant); + } + + [Fact] + public void ToSkFontStyle_Bold_ReturnsWeight700() + { + var skStyle = FontManager.ToSkFontStyle(FontWeight.Bold, AstFontStyle.Normal); + + Assert.Equal(700, skStyle.Weight); + Assert.Equal(SKFontStyleSlant.Upright, skStyle.Slant); + } + + [Fact] + public void ToSkFontStyle_Italic_ReturnsItalicSlant() + { + var skStyle = FontManager.ToSkFontStyle(FontWeight.Normal, AstFontStyle.Italic); + + Assert.Equal(SKFontStyleSlant.Italic, skStyle.Slant); + } + + [Fact] + public void ToSkFontStyle_Oblique_ReturnsObliqueSlant() + { + var skStyle = FontManager.ToSkFontStyle(FontWeight.Normal, AstFontStyle.Oblique); + + Assert.Equal(SKFontStyleSlant.Oblique, skStyle.Slant); + } + + [Fact] + public void ToSkFontStyle_Black_ReturnsWeight900() + { + var skStyle = FontManager.ToSkFontStyle(FontWeight.Black, AstFontStyle.Normal); + + Assert.Equal(900, skStyle.Weight); + } } diff --git a/tests/FlexRender.Tests/Rendering/TextRendererTests.cs b/tests/FlexRender.Tests/Rendering/TextRendererTests.cs index 8857f6e..f4b4a72 100644 --- a/tests/FlexRender.Tests/Rendering/TextRendererTests.cs +++ b/tests/FlexRender.Tests/Rendering/TextRendererTests.cs @@ -2,6 +2,7 @@ using FlexRender.Rendering; using SkiaSharp; using Xunit; +using AstFontStyle = global::FlexRender.Parsing.Ast.FontStyle; namespace FlexRender.Tests.Rendering; @@ -306,4 +307,88 @@ public void DrawText_WithLineHeight_DoesNotThrow() Assert.Null(exception); } + + [Fact] + public void DrawText_WithBoldFontWeight_DoesNotThrow() + { + var element = new TextElement + { + Content = "Bold text", + Size = "16", + FontWeight = FontWeight.Bold + }; + + var exception = Record.Exception(() => + _textRenderer.DrawText(_canvas, element, new SKRect(0, 0, 200, 100), baseFontSize: 12f)); + + Assert.Null(exception); + } + + [Fact] + public void DrawText_WithItalicFontStyle_DoesNotThrow() + { + var element = new TextElement + { + Content = "Italic text", + Size = "16", + FontStyle = AstFontStyle.Italic + }; + + var exception = Record.Exception(() => + _textRenderer.DrawText(_canvas, element, new SKRect(0, 0, 200, 100), baseFontSize: 12f)); + + Assert.Null(exception); + } + + [Fact] + public void DrawText_WithBoldItalic_DoesNotThrow() + { + var element = new TextElement + { + Content = "Bold italic text", + Size = "16", + FontWeight = FontWeight.Bold, + FontStyle = AstFontStyle.Italic + }; + + var exception = Record.Exception(() => + _textRenderer.DrawText(_canvas, element, new SKRect(0, 0, 200, 100), baseFontSize: 12f)); + + Assert.Null(exception); + } + + [Fact] + public void MeasureText_WithBoldWeight_ReturnsNonZeroSize() + { + var element = new TextElement + { + Content = "Bold", + Size = "16", + FontWeight = FontWeight.Bold + }; + + var size = _textRenderer.MeasureText(element, maxWidth: 200f, baseFontSize: 12f); + + Assert.True(size.Width > 0); + Assert.True(size.Height > 0); + } + + [Fact] + public void MeasureText_WithDefaultWeightAndStyle_MatchesPlainText() + { + var plain = new TextElement { Content = "Test", Size = "16" }; + var explicit_ = new TextElement + { + Content = "Test", + Size = "16", + FontWeight = FontWeight.Normal, + FontStyle = AstFontStyle.Normal + }; + + var plainSize = _textRenderer.MeasureText(plain, maxWidth: 200f, baseFontSize: 12f); + var explicitSize = _textRenderer.MeasureText(explicit_, maxWidth: 200f, baseFontSize: 12f); + + Assert.Equal(plainSize.Width, explicitSize.Width, precision: 1); + Assert.Equal(plainSize.Height, explicitSize.Height, precision: 1); + } } diff --git a/tests/FlexRender.Tests/TemplateEngine/ContentParserRegistryTests.cs b/tests/FlexRender.Tests/TemplateEngine/ContentParserRegistryTests.cs new file mode 100644 index 0000000..774b9bb --- /dev/null +++ b/tests/FlexRender.Tests/TemplateEngine/ContentParserRegistryTests.cs @@ -0,0 +1,67 @@ +using FlexRender.Abstractions; +using FlexRender.Parsing.Ast; +using FlexRender.TemplateEngine; +using Xunit; + +namespace FlexRender.Tests.TemplateEngine; + +public sealed class ContentParserRegistryTests +{ + [Fact] + public void Register_AndResolve_ReturnsParser() + { + var registry = new ContentParserRegistry(); + var parser = new StubContentParser("markdown"); + registry.Register(parser); + + var resolved = registry.GetParser("markdown"); + + Assert.NotNull(resolved); + Assert.Same(parser, resolved); + } + + [Fact] + public void GetParser_CaseInsensitive_ReturnsParser() + { + var registry = new ContentParserRegistry(); + registry.Register(new StubContentParser("Markdown")); + + var resolved = registry.GetParser("MARKDOWN"); + + Assert.NotNull(resolved); + } + + [Fact] + public void GetParser_UnknownFormat_ReturnsNull() + { + var registry = new ContentParserRegistry(); + + var resolved = registry.GetParser("unknown"); + + Assert.Null(resolved); + } + + [Fact] + public void Register_DuplicateFormat_ThrowsInvalidOperation() + { + var registry = new ContentParserRegistry(); + registry.Register(new StubContentParser("markdown")); + + Assert.Throws (() => + registry.Register(new StubContentParser("markdown"))); + } + + [Fact] + public void Register_NullParser_ThrowsArgumentNull() + { + var registry = new ContentParserRegistry(); + + Assert.Throws (() => registry.Register(null!)); + } + + private sealed class StubContentParser(string formatName) : IContentParser + { + public string FormatName => formatName; + public IReadOnlyList Parse(string text) => []; + } +} diff --git a/tests/FlexRender.Tests/TemplateEngine/TemplateExpanderContentTests.cs b/tests/FlexRender.Tests/TemplateEngine/TemplateExpanderContentTests.cs new file mode 100644 index 0000000..e20e9ed --- /dev/null +++ b/tests/FlexRender.Tests/TemplateEngine/TemplateExpanderContentTests.cs @@ -0,0 +1,156 @@ +using FlexRender.Abstractions; +using FlexRender.Configuration; +using FlexRender.Parsing.Ast; +using FlexRender.TemplateEngine; +using Xunit; + +namespace FlexRender.Tests.TemplateEngine; + +public sealed class TemplateExpanderContentTests +{ + [Fact] + public void Expand_ContentElement_ReplacesWithParsedSubtree() + { + var registry = new ContentParserRegistry(); + registry.Register(new StubContentParser("test", [ + new TextElement { Content = "Line 1" }, + new TextElement { Content = "Line 2" } + ])); + + var expander = new TemplateExpander(new ResourceLimits(), contentParserRegistry: registry); + + var template = CreateTemplate( + new ContentElement { Source = "some text", Format = "test" }); + + var result = expander.Expand(template, new ObjectValue()); + + Assert.Equal(2, result.Elements.Count); + Assert.IsType (result.Elements[0]); + Assert.IsType (result.Elements[1]); + Assert.Equal("Line 1", ((TextElement)result.Elements[0]).Content.Value); + Assert.Equal("Line 2", ((TextElement)result.Elements[1]).Content.Value); + } + + [Fact] + public void Expand_ContentElement_ResolvesSourceFromData() + { + var capturedText = ""; + var registry = new ContentParserRegistry(); + registry.Register(new CapturingContentParser("test", t => + { + capturedText = t; + return [new TextElement { Content = t }]; + })); + + var expander = new TemplateExpander(new ResourceLimits(), contentParserRegistry: registry); + + var template = CreateTemplate( + new ContentElement { Source = "{{body}}", Format = "test" }); + + var data = new ObjectValue { ["body"] = new StringValue("resolved body") }; + expander.Expand(template, data); + + Assert.Equal("resolved body", capturedText); + } + + [Fact] + public void Expand_ContentElement_ResolvesFormatFromData() + { + var registry = new ContentParserRegistry(); + registry.Register(new StubContentParser("markdown", [ + new TextElement { Content = "parsed" } + ])); + + var expander = new TemplateExpander(new ResourceLimits(), contentParserRegistry: registry); + + var template = CreateTemplate( + new ContentElement { Source = "text", Format = "{{fmt}}" }); + + var data = new ObjectValue { ["fmt"] = new StringValue("markdown") }; + var result = expander.Expand(template, data); + + Assert.Single(result.Elements); + Assert.Equal("parsed", ((TextElement)result.Elements[0]).Content.Value); + } + + [Fact] + public void Expand_ContentElement_UnknownFormat_ThrowsTemplateEngineException() + { + var registry = new ContentParserRegistry(); + var expander = new TemplateExpander(new ResourceLimits(), contentParserRegistry: registry); + + var template = CreateTemplate( + new ContentElement { Source = "text", Format = "unknown" }); + + Assert.Throws (() => + expander.Expand(template, new ObjectValue())); + } + + [Fact] + public void Expand_ContentElement_NoRegistry_ThrowsTemplateEngineException() + { + var expander = new TemplateExpander(new ResourceLimits()); + + var template = CreateTemplate( + new ContentElement { Source = "text", Format = "markdown" }); + + Assert.Throws (() => + expander.Expand(template, new ObjectValue())); + } + + [Fact] + public void Expand_ContentElement_InsideFlex_PreservesStructure() + { + var registry = new ContentParserRegistry(); + registry.Register(new StubContentParser("test", [ + new TextElement { Content = "from content" } + ])); + + var expander = new TemplateExpander(new ResourceLimits(), contentParserRegistry: registry); + + var flex = new FlexElement + { + Children = + [ + new TextElement { Content = "before" }, + new ContentElement { Source = "text", Format = "test" }, + new TextElement { Content = "after" } + ] + }; + var template = CreateTemplate(flex); + + var result = expander.Expand(template, new ObjectValue()); + + Assert.Single(result.Elements); + var resultFlex = Assert.IsType (result.Elements[0]); + Assert.Equal(3, resultFlex.Children.Count); + Assert.Equal("before", ((TextElement)resultFlex.Children[0]).Content.Value); + Assert.Equal("from content", ((TextElement)resultFlex.Children[1]).Content.Value); + Assert.Equal("after", ((TextElement)resultFlex.Children[2]).Content.Value); + } + + private static Template CreateTemplate(params TemplateElement[] elements) + { + return new Template + { + Name = "test", + Version = 1, + Canvas = new CanvasSettings { Width = 200 }, + Elements = elements.ToList() + }; + } + + private sealed class StubContentParser(string formatName, IReadOnlyList result) : IContentParser + { + public string FormatName => formatName; + public IReadOnlyList Parse(string text) => result; + } + + private sealed class CapturingContentParser( + string formatName, + Func > parseFunc) : IContentParser + { + public string FormatName => formatName; + public IReadOnlyList Parse(string text) => parseFunc(text); + } +}