diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fde7028..f193504 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -75,6 +75,8 @@ jobs: src/FlexRender.QrCode/FlexRender.QrCode.csproj \ src/FlexRender.Barcode/FlexRender.Barcode.csproj \ src/FlexRender.SvgElement/FlexRender.SvgElement.csproj \ + src/FlexRender.Content.Markdown/FlexRender.Content.Markdown.csproj \ + src/FlexRender.Content.Html/FlexRender.Content.Html.csproj \ src/FlexRender.DependencyInjection/FlexRender.DependencyInjection.csproj \ src/FlexRender.MetaPackage/FlexRender.MetaPackage.csproj; do dotnet pack "$project" \ diff --git a/Directory.Packages.props b/Directory.Packages.props index a58153e..6229774 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,7 @@ - + @@ -18,6 +18,8 @@ + + @@ -25,12 +27,12 @@ - + - + - + - + \ No newline at end of file diff --git a/FlexRender.slnx b/FlexRender.slnx index fefd9a0..642398b 100644 --- a/FlexRender.slnx +++ b/FlexRender.slnx @@ -32,6 +32,10 @@ + + + + diff --git a/README.md b/README.md index 6ed7929..f463a7f 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,10 @@ dotnet add package FlexRender.QrCode dotnet add package FlexRender.Barcode dotnet add package FlexRender.SvgElement +# Content parsers (optional) +dotnet add package FlexRender.Content.Markdown +dotnet add package FlexRender.Content.Html + # CLI tool dotnet tool install -g flexrender-cli ``` diff --git a/docs/wiki/API-Reference.md b/docs/wiki/API-Reference.md index b77d7e3..ace2079 100644 --- a/docs/wiki/API-Reference.md +++ b/docs/wiki/API-Reference.md @@ -79,6 +79,9 @@ Builder for configuring and creating `IFlexRender` instances. Defined in `FlexRe | `WithFilter(ITemplateFilter)` | Register a custom template filter for inline expressions. Works alongside built-in filters (enabled by default) | | `WithoutDefaultLoaders()` | Remove default File and Base64 loaders (sandboxed mode) | | `WithoutDefaultFilters()` | Remove all 8 built-in filters (enabled by default), leaving only custom-registered filters | +| `WithContentParser(IContentParser)` | Register a content parser for `type: content` elements | +| `WithMarkdown()` | Enable Markdown content parsing (`format: markdown`) | +| `WithHtml()` | Enable HTML content parsing (`format: html`) | | `Build()` | Create the configured `IFlexRender` instance | ### Usage @@ -104,6 +107,13 @@ var render = new FlexRenderBuilder() .WithBarcode()) .Build(); +// With content parsers +var render = new FlexRenderBuilder() + .WithMarkdown() + .WithHtml() + .WithSkia(skia => skia.WithQr().WithBarcode()) + .Build(); + // Sandboxed (no file system access) var render = new FlexRenderBuilder() .WithoutDefaultLoaders() diff --git a/docs/wiki/Element-Reference.md b/docs/wiki/Element-Reference.md index 1300f12..5c97802 100644 --- a/docs/wiki/Element-Reference.md +++ b/docs/wiki/Element-Reference.md @@ -12,7 +12,7 @@ For rendering options (antialiasing, format settings), see [[Render-Options]]. ## Common Properties (TemplateElement) -All 10 element types (`flex`, `text`, `image`, `svg`, `qr`, `barcode`, `separator`, `table`, `each`, `if`) inherit these properties from the base `TemplateElement` class. You can use any of them on any element. +All 11 element types (`flex`, `text`, `image`, `svg`, `qr`, `barcode`, `separator`, `table`, `content`, `each`, `if`) inherit these properties from the base `TemplateElement` class. You can use any of them on any element. > **Expression support:** All properties on all element types accept `{{expressions}}`. This includes typed properties like `opacity` (float), `grow`/`shrink` (float), `order` (int), `wrap` (bool on text, FlexWrap on flex), and enum properties like `display`, `position`, `align`. See [[Template-Expressions]] for details. @@ -739,6 +739,7 @@ Renders text content with font styling, alignment, wrapping, overflow handling, |----------|-----------|------|---------|--------------|----------|-------------| | Content | `content` | string | `""` | Any string, may contain `{{variable}}` expressions | No | The text to render. | | Font | `font` | string | `"main"` | Any key defined in the `fonts` section | No | Font reference name. Falls back to `"default"` if `"main"` is not defined. | +| FontFamily | `fontFamily` or `font-family` | string | `""` | Any font family name | No | CSS-like font family name. Searches registered fonts by FamilyName metadata, then system fonts. | | Size | `size` | string | `"1em"` | px, em, % | No | Font size. | | Color | `color` | string | `"#000000"` | Hex color (#rgb or #rrggbb) | No | Text color. | | Align | `align` | TextAlign | `left` | left, center, right, start (logical), end (logical) | No | Horizontal text alignment within the element. `start`/`end` resolve based on text direction. | @@ -746,6 +747,8 @@ Renders text content with font styling, alignment, wrapping, overflow handling, | Overflow | `overflow` | TextOverflow | `ellipsis` | ellipsis, clip, visible | No | How overflowing text is handled when `maxLines` is reached or `wrap` is `false`. | | MaxLines | `maxLines` | int? | `null` | Any positive integer, or null for unlimited | No | Maximum number of lines. Text beyond this limit is truncated per `overflow`. | | LineHeight | `lineHeight` | string | `""` | Multiplier, px, em, or empty string | No | Line spacing for multi-line text. | +| FontWeight | `fontWeight` or `font-weight` | FontWeight | `Normal` | `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 | No | Font weight for selecting font variant. CSS-compatible values. | +| FontStyle | `fontStyle` or `font-style` | FontStyle | `Normal` | `normal`, `italic`, `oblique` | No | Font style for selecting font variant. | ```yaml # Font size in pixels @@ -834,6 +837,30 @@ Renders text content with font styling, alignment, wrapping, overflow handling, wrap: true ``` +**fontWeight and fontStyle examples:** + +```yaml +# Font weight and style variants +- type: text + content: "Bold text" + fontWeight: bold + +- type: text + content: "Italic text" + fontStyle: italic + +- type: text + content: "Light italic" + fontWeight: light + fontStyle: italic + +- type: text + content: "Semi-bold" + fontWeight: 600 +``` + +> **Font resolution priority:** `font` (registered name) > `fontFamily` (family name lookup) > fallback (default). When `fontFamily` is set, FlexRender searches registered fonts by FamilyName metadata, then system fonts. When `fontWeight` or `fontStyle` is set, FlexRender automatically scans the same directory as the resolved font file for sibling files with matching family name and weight/style (within +/-100 units). See [[Template-Syntax#fonts]] for details. Variable fonts are not supported -- use separate static `.ttf`/`.otf` files per weight. + ### Complete Example: Multi-line Truncated Description ```yaml @@ -1283,6 +1310,9 @@ Renders tabular data with configurable columns, optional header row, and support | Columns | `columns` | column[] | -- | Array of column definitions | **Yes** | Column definitions. Must have at least one column. | | Rows | `rows` | row[] | `[]` | Array of static row definitions | No | Static rows. Alternative to `array` for fixed data. | | HeaderFont | `headerFont` or `header-font` | string? | `null` | Any font name from `fonts` section | No | Font for the header row. | +| HeaderFontWeight | `headerFontWeight` or `header-fontWeight` | string? | `null` | Font weight name or 100-900 | No | Font weight for the header row. | +| HeaderFontStyle | `headerFontStyle` or `header-fontStyle` | string? | `null` | normal, italic, oblique | No | Font style for the header row. | +| HeaderFontFamily | `headerFontFamily` or `header-fontFamily` | string? | `null` | Any font family name | No | CSS-like font family for the header row. | | HeaderColor | `headerColor` or `header-color` | string? | `null` | Hex color | No | Text color for the header row. | | HeaderSize | `headerSize` or `header-size` | string? | `null` | px, em, % | No | Font size for the header row. | | HeaderBackground | `headerBackground` or `header-background` | string? | `null` | Hex color | No | Background color for the header row. | @@ -1419,6 +1449,110 @@ Renders tabular data with configurable columns, optional header row, and support --- +## Content Element (Control Flow) + +Embeds dynamically formatted text (Markdown, HTML, etc.) from template data. The `source` text is parsed at render time into a subtree of FlexRender elements using pluggable content parsers. + +This is a **control-flow element** — like `each` and `if`, it is expanded during template processing and does not appear in the final render tree. + +```yaml +- type: content + source: "{{body}}" + format: markdown +``` + +### Properties + +| Property | YAML Name | Type | Default | Valid Values | Expression | Description | +|----------|-----------|------|---------|--------------|-----------|-------------| +| Source | `source` | string | `""` | Any string, typically `{{variable}}` | Yes | The formatted text to parse. Usually bound to a data variable. | +| Format | `format` | string | `""` | `markdown`, `html`, or any registered parser name | Yes | The content format. Must match a registered `IContentParser.FormatName`. | + +### Supported Formats + +| Format | Package | Builder Method | Library | +|--------|---------|----------------|---------| +| `markdown` | `FlexRender.Content.Markdown` | `.WithMarkdown()` | Markdig | +| `html` | `FlexRender.Content.Html` | `.WithHtml()` | HtmlAgilityPack | + +### Element Mapping + +Content parsers convert formatted text into standard FlexRender elements: + +| Source Format | Produces | +|---------------|----------| +| Bold text (`**bold**` or ``) | `TextElement { FontWeight = Bold }` | +| Italic text (`*italic*` or ``) | `TextElement { FontStyle = Italic }` | +| Headings (`# H1` or `

`) | `TextElement { FontWeight = Bold, Size = "2em" }` | +| Lists (`- item` or `
    `) | `FlexElement` with bullet-prefixed children | +| Blockquote (`>` or `
    `) | `FlexElement { Padding, Background }` | +| Horizontal rule (`---` or `
    `) | `SeparatorElement` | +| Image (`![](url)` 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 @@ -Jane DoeSenior Developerjane@example.com+1 (555) 123-4567example.com \ No newline at end of file +Jane DoeSenior Developerjane@example.com+1 (555) 123-4567example.com \ 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": "

    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 Review

    Price: $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 Image GenerateCode128(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 instance. The caller is responsible for disposing it. + 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); + } + /// /// 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 @@ + + + + FlexRender.Content.Html + HTML content parser for FlexRender. Converts HTML text into FlexRender template elements. + true + + + + + + + + + + + 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; + +/// +/// Extension methods for configuring HTML content parsing in FlexRender. +/// +public static class FlexRenderBuilderExtensions +{ + /// + /// Adds HTML content parsing support. Enables type: content elements with format: html. + /// + /// The builder to configure. + /// The same builder instance for method chaining. + /// Thrown when is null. + 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; + +/// +/// 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 + /// on deeply nested input. + /// + private const int MaxDepth = 64; + + private static readonly HashSet 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,
    +        List results,
    +        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 @@
    +
    +
    +  
    +    FlexRender.Content.Markdown
    +    Markdown content parser for FlexRender. Converts Markdown text into FlexRender template elements.
    +    true
    +  
    +
    +  
    +    
    +  
    +
    +  
    +    
    +  
    +
    +
    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;
    +
    +/// 
    +/// Extension methods for configuring Markdown content parsing in FlexRender.
    +/// 
    +public static class FlexRenderBuilderExtensions
    +{
    +    /// 
    +    /// Adds Markdown content parsing support. Enables type: content elements with format: markdown.
    +    /// 
    +    /// The builder to configure.
    +    /// The same builder instance for method chaining.
    +    /// Thrown when  is null.
    +    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;
    +
    +/// 
    +/// 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
    +    ///  on deeply nested input.
    +    /// 
    +    private const int MaxDepth = 64;
    +
    +    private static readonly MarkdownPipeline Pipeline = new MarkdownPipelineBuilder()
    +        .UseAdvancedExtensions()
    +        .Build();
    +
    +    /// 
    +    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 List CollectInlines(
    +        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  is null.
    +    IReadOnlyList 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 null if no content parsers have been registered.
    +    /// 
    +    internal ContentParserRegistry? ContentParserRegistry => _contentParserRegistry;
    +
         /// 
         /// Sets the renderer factory function that creates the  implementation.
         /// 
    @@ -199,6 +205,21 @@ public FlexRenderBuilder WithFilter(ITemplateFilter filter)
             return this;
         }
     
    +    /// 
    +    /// Registers a content parser for expanding type: content elements.
    +    /// 
    +    /// The content parser to register.
    +    /// 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 {{expressions}}.
    +    /// 
    +    public ExprValue Source { get; set; } = "";
    +
    +    /// 
    +    /// Gets or sets the format name (e.g., "markdown", "escpos", "xml").
    +    /// Supports {{expressions}}. Must match a registered .
    +    /// 
    +    public ExprValue 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
     }
     
    +/// 
    +/// 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
         /// 
         public ExprValue Font { get; set; } = "main";
     
    +    /// 
    +    /// CSS-like font family name. Searched in registered fonts by FamilyName, then system fonts.
    +    /// When set and  is at its default value, this takes precedence.
    +    /// 
    +    public ExprValue FontFamily { get; set; } = "";
    +
    +    /// 
    +    /// Font weight (100-900 or named: thin, light, normal, bold, black). Default: normal (400).
    +    /// 
    +    public ExprValue FontWeight { get; set; } = Ast.FontWeight.Normal;
    +
    +    /// 
    +    /// Font style (normal, italic, oblique). Default: normal.
    +    /// 
    +    public ExprValue FontStyle { get; set; } = Ast.FontStyle.Normal;
    +
         /// 
         /// Font size (pixels, em, or percentage).
         /// 
    @@ -116,6 +170,9 @@ public override void ResolveExpressions(Func resolv
             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  implementations.
    +/// 
    +public sealed class ContentParserRegistry
    +{
    +    private readonly Dictionary _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, null.
    +    public IContentParser? GetParser(string formatName)
    +    {
    +        return _parsers.GetValueOrDefault(formatName);
    +    }
    +
    +    /// 
    +    /// 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 ExpandElement(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 Func CreateRendererFactory(
                 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.
     /// 
     internal sealed class ImageSharpFontManager : IDisposable
     {
         private readonly ConcurrentDictionary _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 ). Falls back to the isolated collection if no
    +    /// sibling match is found.
    +    /// 
    +    /// The logical font name.
    +    /// The font size in pixels.
    +    /// The AST font weight (100-900).
    +    /// The AST font style (Normal, Italic, Oblique).
    +    /// 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 .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.
    +    /// 
    +    /// The logical font name.
    +    /// The font size in pixels.
    +    /// The desired SixLabors font style.
    +    /// A  if the variant was found; otherwise, null.
    +    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);
    +    }
    +
    +    /// 
    +    /// Loads all .ttf and .otf font files from the specified directory
    +    /// into a new . Files that fail to load (corrupt or
    +    /// unsupported format) are silently skipped.
    +    /// 
    +    /// The directory path to scan for font files.
    +    /// A  containing all loadable fonts from the directory.
    +    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;
    +    }
    +
         /// 
         /// 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 Image RenderToImage(
             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  is null or empty.
         SKTypeface GetTypeface(string fontName);
     
    +    /// 
    +    /// Gets a typeface by font name with specific weight and style, using fallback if necessary.
    +    /// When both  and  are their default values
    +    /// ( and ), this behaves
    +    /// identically to .
    +    /// 
    +    /// The font family name.
    +    /// 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, 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  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 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 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 .
    +    /// Otherwise, resolves the base font family name and uses  to
    +    /// match a typeface variant with the requested weight and slant.
    +    /// 
    +    /// The font name.
    +    /// The desired font weight (100-900).
    +    /// The desired font style (normal, italic, oblique).
    +    /// 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 .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, or null if no suitable sibling is found.
    +    /// Rejected typefaces are disposed immediately to prevent memory leaks.
    +    /// 
    +    /// 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.
    +    /// The best matching sibling typeface, or null if none found.
    +    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;
    +    }
    +
    +    /// 
    +    /// Converts  and  to an .
    +    /// 
    +    /// The font weight (100-900).
    +    /// The font style (normal, italic, oblique).
    +    /// 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  is null.
         public SkiaRenderer(
             ResourceLimits limits,
    @@ -95,7 +96,8 @@ public SkiaRenderer(
             bool deterministicRendering = false,
             FlexRenderOptions? options = null,
             IContentProvider? 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  instance. Caller must dispose.
         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.
         /// 
         /// 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 List ParseTableRows(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 , or null if the key is absent or unrecognized.
    +    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
    +        };
    +    }
    +
    +    /// 
    +    /// 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  value, or null if not present or unrecognized.
    +    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
    +        };
    +    }
    +
         /// 
         /// 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 Dictionary ParseFonts(YamlMappingNode nod
             return fonts;
         }
     
    +    /// 
    +    /// Parses the fonts section when provided as a YAML sequence (list format).
    +    /// Supports two item formats:
    +    /// - Simple string: - "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".
    +    /// 
    +    /// The YAML sequence node containing font entries.
    +    /// A dictionary mapping font names to their definitions.
    +    private static Dictionary ParseFontsList(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);
    +    }
    +}