Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ jobs:
src/FlexRender.SvgElement/FlexRender.SvgElement.csproj \
src/FlexRender.Content.Markdown/FlexRender.Content.Markdown.csproj \
src/FlexRender.Content.Html/FlexRender.Content.Html.csproj \
src/FlexRender.Content.Ndc/FlexRender.Content.Ndc.csproj \
src/FlexRender.DependencyInjection/FlexRender.DependencyInjection.csproj \
src/FlexRender.MetaPackage/FlexRender.MetaPackage.csproj; do
dotnet pack "$project" \
Expand Down
7 changes: 5 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ 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.Ndc/ # NDC (ATM receipt) content parser (-> Core)
src/FlexRender.DependencyInjection/ # Microsoft.Extensions.DI integration
src/FlexRender.MetaPackage/ # Meta-package (core + all backends + DI)

Expand Down Expand Up @@ -167,15 +168,16 @@ byte[] png = await render.Render(_templates["receipt"], data);
|-------|------------|
| Configuration | `FlexRenderBuilder`, `SkiaBuilder`, `FlexRenderOptions`, `ResourceLimits` |
| Abstractions | `IFlexRender`, `IResourceLoader` |
| Parsing | `TemplateParser`, `Template`, `CanvasSettings`, `TextElement`, `FlexElement`, `QrElement`, `BarcodeElement`, `ImageElement`, `SeparatorElement`, `TableElement`, `TableColumn`, `TableRow`, `EachElement`, `IfElement` |
| Template Engine | `TemplateExpander`, `TemplateProcessor`, `ExpressionLexer`, `ExpressionEvaluator`, `TemplateContext`, `InlineExpressionParser`, `InlineExpressionEvaluator`, `FilterRegistry`, `ITemplateFilter` |
| Parsing | `TemplateParser`, `Template`, `CanvasSettings`, `TextElement`, `FlexElement`, `QrElement`, `BarcodeElement`, `ImageElement`, `SeparatorElement`, `TableElement`, `TableColumn`, `TableRow`, `EachElement`, `IfElement`, `ContentElement` |
| Template Engine | `TemplateExpander`, `TemplateProcessor`, `ExpressionLexer`, `ExpressionEvaluator`, `TemplateContext`, `InlineExpressionParser`, `InlineExpressionEvaluator`, `FilterRegistry`, `ITemplateFilter`, `ContentSourceResolver` |
| Layout | `LayoutEngine`, `LayoutNode`, `LayoutContext`, `LayoutSize`, `IntrinsicSize`, `Unit`, `UnitParser`, `MarginValue`, `MarginValues`, `PaddingParser.ParseMargin` |
| Rendering (Skia) | `SkiaRender` (IFlexRender impl), `SkiaRenderer`, `TextRenderer`, `FontManager`, `ColorParser`, `RotationHelper`, `BmpEncoder`, `BoxShadowParser`, `GradientParser` |
| Rendering (ImageSharp) | `ImageSharpRender` (IFlexRender impl), `ImageSharpRenderingEngine`, `ImageSharpTextRenderer`, `ImageSharpFontManager` |
| Providers | `IContentProvider<T,O>`, `QrProvider`, `BarcodeProvider`, `ImageProvider` |
| Loaders | `FileResourceLoader`, `Base64ResourceLoader`, `EmbeddedResourceLoader`, `HttpResourceLoader` |
| DI | `ServiceCollectionExtensions.AddFlexRender()` |
| Values | `TemplateValue` (abstract), `StringValue`, `NumberValue`, `BoolValue`, `NullValue`, `ArrayValue`, `ObjectValue` |
| Content Parsers | `IContentParser`, `IBinaryContentParser`, `ContentParserRegistry`, `ContentSourceResolver`, `NdcContentParser` |

## Coding Conventions

Expand Down Expand Up @@ -317,6 +319,7 @@ The release workflow (`.github/workflows/release.yml`) publishes all packages to
| QR providers | `FlexRender.QrCode.Skia.Render`, `FlexRender.QrCode.ImageSharp.Render`, `FlexRender.QrCode.Svg.Render` |
| Barcode providers | `FlexRender.Barcode.Skia.Render`, `FlexRender.Barcode.ImageSharp.Render`, `FlexRender.Barcode.Svg.Render` |
| SvgElement providers | `FlexRender.SvgElement.Skia.Render`, `FlexRender.SvgElement.Svg.Render` |
| Content parsers | `FlexRender.Content.Markdown`, `FlexRender.Content.Html`, `FlexRender.Content.Ndc` |
| Extensions | `FlexRender.HarfBuzz` |
| Meta (backend) | `FlexRender.Skia`, `FlexRender.ImageSharp`, `FlexRender.Svg` |
| Meta (feature) | `FlexRender.QrCode`, `FlexRender.Barcode`, `FlexRender.SvgElement` |
Expand Down
1 change: 1 addition & 0 deletions FlexRender.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<Folder Name="/src/Content/">
<Project Path="src/FlexRender.Content.Markdown/FlexRender.Content.Markdown.csproj" />
<Project Path="src/FlexRender.Content.Html/FlexRender.Content.Html.csproj" />
<Project Path="src/FlexRender.Content.Ndc/FlexRender.Content.Ndc.csproj" />
</Folder>
<Folder Name="/src/SvgElement/">
<Project Path="src/FlexRender.SvgElement.Skia.Render/FlexRender.SvgElement.Skia.Render.csproj" />
Expand Down
28 changes: 28 additions & 0 deletions docs/wiki/API-Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,10 @@ Builder for configuring and creating `IFlexRender` instances. Defined in `FlexRe
| `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 |
| `WithBinaryContentParser(IBinaryContentParser)` | Register a binary content parser for `type: content` elements |
| `WithMarkdown()` | Enable Markdown content parsing (`format: markdown`) |
| `WithHtml()` | Enable HTML content parsing (`format: html`) |
| `WithNdc()` | Enable NDC content parsing (`format: ndc`) |
| `Build()` | Create the configured `IFlexRender` instance |

### Usage
Expand Down Expand Up @@ -124,6 +126,26 @@ var render = new FlexRenderBuilder()

The builder can only be built once. Creating a second instance requires a new `FlexRenderBuilder`.

### Data Binding with Binary Content

Content elements support binary data via `BytesValue`:

```csharp
// String content (for Markdown, HTML parsers)
var data = new ObjectValue { ["body"] = new StringValue("# Hello") };

// Binary content (for NDC and other IBinaryContentParser implementations)
var data = new ObjectValue { ["receipt"] = new BytesValue(rawBytes) };

// From Stream
var data = new ObjectValue { ["receipt"] = BytesValue.FromStream(fileStream) };

// From base64 in YAML source
// source: "base64:SGVsbG8gV29ybGQ="
```

`BytesValue` wraps `ReadOnlyMemory<byte>` and is passed directly to `IBinaryContentParser` without encoding conversion. When only `IContentParser` is registered, binary data is decoded as UTF-8.

---

## SkiaBuilder
Expand Down Expand Up @@ -514,6 +536,11 @@ var obj = new ObjectValue
["zip"] = "123456"
}
};

// Binary data
var bytes = new BytesValue(binaryData); // from byte[]
var bytes = new BytesValue(readOnlyMemory); // from ReadOnlyMemory<byte>
var bytes = BytesValue.FromStream(stream); // from Stream
```

| Type | C# Class | Description |
Expand All @@ -524,6 +551,7 @@ var obj = new ObjectValue
| Null | `NullValue` | Null sentinel (`NullValue.Instance`) |
| Array | `ArrayValue` | Implements `IReadOnlyList<TemplateValue>` |
| Object | `ObjectValue` | Dictionary-like, `StringComparer.OrdinalIgnoreCase` |
| Binary | `BytesValue` | Binary data with optional MIME type |

---

Expand Down
108 changes: 106 additions & 2 deletions docs/wiki/Element-Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1451,7 +1451,7 @@ 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.
Embeds dynamically formatted content (Markdown, HTML, NDC binary data, etc.) from template data. The `source` supports multiple input types: plain text, `base64:`-encoded binary data, `file:` URIs, `text:` prefixed strings, and template variables bound to `string` or `byte[]` (`BytesValue`). The source 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.

Expand All @@ -1465,15 +1465,54 @@ This is a **control-flow element** — like `each` and `if`, it is expanded duri

| 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. |
| Source | `source` | string | `""` | Any string, typically `{{variable}}` | Yes | The content to parse. Supports plain text, `base64:` binary, `file:` URIs, `text:` prefix, and `{{variable}}` expressions resolving to `string` or `BytesValue` (`byte[]`). See [Content Source Resolution](#content-source-resolution) below. |
| Format | `format` | string | `""` | `markdown`, `html`, or any registered parser name | Yes | The content format. Must match a registered `IContentParser.FormatName`. |
| Options | `options` | dict? | `null` | Key-value dictionary | No | Parser-specific options (e.g., NDC `columns`, `charsets`). Passed to the content parser. |

### Content Source Resolution

The `source` property is resolved at render time through `ContentSourceResolver`, which supports multiple input types:

| Source Format | Example | Resolved As | Description |
|---------------|---------|-------------|-------------|
| Template variable (`BytesValue`) | `source: "{{rawData}}"` | Binary (`byte[]`) | When `{{variable}}` resolves to `BytesValue` in the data context, binary data is passed directly to `IBinaryContentParser`. No string encoding overhead. |
| `base64:` prefix | `source: "base64:SGVsbG8="` | Binary (`byte[]`) | Base64-encoded payload decoded into bytes. Useful for embedding binary data in JSON/YAML. |
| `file:` scheme | `source: "file:receipt.bin"` | Binary (`byte[]`) | Loads content via registered resource loaders (file system, HTTP, embedded). Also supports `file:///` URIs. Throws if file not found. |
| `text:` prefix | `source: "text:# Hello"` | Text (`string`) | Forces text interpretation, skipping file path detection. |
| File path heuristic | `source: "receipt.md"` | Binary (`byte[]`) | If source looks like a file path (contains `/`, `\`, or a file extension), tries resource loaders first. Falls back to text if no loader matches. |
| Plain text | `source: "**bold text**"` | Text (`string`) | Default fallback -- treated as literal text content. |

**Resolution order:** The resolver processes sources in the order listed above. The first matching rule wins.

**Binary vs Text parsers:**
- `IContentParser` receives text (`string`) -- used by Markdown, HTML parsers
- `IBinaryContentParser` receives binary data (`ReadOnlyMemory<byte>`) -- used by NDC parser
- When source resolves to binary and the parser implements `IBinaryContentParser`, bytes are passed directly without encoding conversion
- When source resolves to binary but the parser only implements `IContentParser`, bytes are decoded as UTF-8 text

**Data binding examples:**

```csharp
// String data -- parsed as text
var data = new ObjectValue { ["body"] = new StringValue("# Hello World") };

// Binary data -- passed directly to IBinaryContentParser
var data = new ObjectValue { ["receiptData"] = new BytesValue(ndcBytes) };

// Binary data with MIME type
var data = new ObjectValue { ["receiptData"] = new BytesValue(ndcBytes, "application/octet-stream") };

// From Stream
var data = new ObjectValue { ["receiptData"] = BytesValue.FromStream(stream) };
```

### Supported Formats

| Format | Package | Builder Method | Library |
|--------|---------|----------------|---------|
| `markdown` | `FlexRender.Content.Markdown` | `.WithMarkdown()` | Markdig |
| `html` | `FlexRender.Content.Html` | `.WithHtml()` | HtmlAgilityPack |
| `ndc` | `FlexRender.Content.Ndc` | `.WithNdc()` | (none) |

### Element Mapping

Expand All @@ -1490,6 +1529,71 @@ Content parsers convert formatted text into standard FlexRender elements:
| Image (`![](url)` or `<img>`) | `ImageElement` |
| Code (`` `code` `` or `<code>`) | `TextElement { Background = "#f0f0f0" }` |

### NDC Format Options

The `ndc` format parses binary NDC (NCR ATM protocol) printer data streams from ATM/banking terminals. It supports both text and binary input via `IContentParser` and `IBinaryContentParser`.

#### Properties

The `content` element with `format: ndc` supports an `options` block:

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `columns` | int | 40 | Maximum characters per line (auto-wrapping) |
| `input_encoding` | string | `"latin1"` | Input byte encoding (`latin1`, `utf-8`, `iso-8859-1`, `ascii`) |
| `font_family` | string | null | Global font family for all text |
| `char_width_ratio` | double | 0.6 | Character width as fraction of font size |
| `charsets` | dict | `{}` | Per-charset style overrides (see below) |

#### Charset Style Properties

Each charset designator (e.g., `"1"`, `"I"`, `">"`) can have individual styling:

| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `font` | string | null | Font registration name |
| `font_family` | string | null | Font family (overrides global) |
| `font_style` | string | null | `"bold"`, `"italic"`, `"bold-italic"`, `"regular"` |
| `font_size` | int | null | Explicit font size in pixels |
| `color` | string | null | Hex color (e.g., `"#333333"`) |
| `encoding` | string | null | Character encoding: `"qwerty-jcuken"`, `"none"`, `"ascii"` |
| `uppercase` | bool | false | Convert text to uppercase |

#### Example: NDC Receipt

```yaml
canvas:
fixed: width
width: 384
background: "#ffffff"

fonts:
- "assets/fonts/JetBrainsMono-Regular.ttf"
- "assets/fonts/JetBrainsMono-Bold.ttf"

layout:
- type: content
source: "{{receiptData}}"
format: ndc
options:
columns: 40
input_encoding: latin1
font_family: "JetBrains Mono"
charsets:
"1":
encoding: "qwerty-jcuken"
font_style: bold
```

Data (JSON with base64-encoded NDC binary):
```json
{
"receiptData": "base64:G1sxfjQwHSgxHQ=="
}
```

The NDC parser produces `TextElement`, `BarcodeElement`, and `SeparatorElement` nodes with auto-calculated font sizes based on the parent element width.

### Example: Markdown Content

```yaml
Expand Down
4 changes: 3 additions & 1 deletion docs/wiki/Getting-Started.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Install only what you need:
| `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.Content.Ndc` | NDC (ATM receipt) content parsing for `type: content` | (none) |
| `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 |
Expand Down Expand Up @@ -196,6 +197,7 @@ Native rendering via SkiaSharp. Best quality, widest feature set.
var render = new FlexRenderBuilder()
.WithMarkdown() // Markdown content parsing
.WithHtml() // HTML content parsing
.WithNdc() // NDC ATM receipt parsing
.WithSkia(skia => skia
.WithQr() // QR code support
.WithBarcode() // Barcode support
Expand All @@ -205,7 +207,7 @@ var render = new FlexRenderBuilder()

- **Formats:** PNG, JPEG, BMP, Raw
- **Requires:** `SkiaSharp.NativeAssets.Linux` on Linux/Docker
- **Optional:** `.WithHarfBuzz()` for Arabic/Hebrew text shaping, `.WithMarkdown()` / `.WithHtml()` for content parsing
- **Optional:** `.WithHarfBuzz()` for Arabic/Hebrew text shaping, `.WithMarkdown()` / `.WithHtml()` / `.WithNdc()` for content parsing
- **Best for:** Desktop apps, servers with native library support

### ImageSharp Backend
Expand Down
6 changes: 3 additions & 3 deletions docs/wiki/Home.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ A modular .NET library for rendering images from YAML templates with a full CSS
- **RTL Support** -- right-to-left layout with `text-direction: rtl`, logical alignment (`start`/`end`), HarfBuzz text shaping for Arabic/Hebrew
- **Template engine** -- variables (`{{name}}`), inline expressions (`{{price * qty | currency}}`), loops (`type: each`), conditionals (`type: if` with 13 operators)
- **Inline expressions** -- arithmetic (`+`, `-`, `*`, `/`), null coalescing (`??`), 8 built-in filters enabled by default (`currency`, `currencySymbol`, `number`, `upper`, `lower`, `trim`, `truncate`, `format`)
- **Rich content types** -- text, images, SVG, QR codes, barcodes, separators, tables
- **Rich content types** -- text, images, SVG, QR codes, barcodes, separators, tables, NDC receipts
- **Visual effects** -- opacity, box-shadow, linear and radial gradient backgrounds
- **Multiple output formats** -- PNG, JPEG, BMP (6 color modes), Raw pixels, with per-call format options
- **AOT-ready** -- no reflection, no `dynamic`, works with Native AOT publishing
Expand Down Expand Up @@ -91,8 +91,8 @@ byte[] png = await render.RenderFile("template.yaml", data);
| Page | Description |
|------|-------------|
| [[Getting-Started]] | Installation, first template, rendering approaches |
| [[Template-Syntax]] | Canvas, all 10 element types, common properties, units |
| [[Element-Reference]] | Complete property reference for all 10 element types with examples |
| [[Template-Syntax]] | Canvas, all 11 element types, common properties, units |
| [[Element-Reference]] | Complete property reference for all 11 element types with examples |
| [[Visual-Reference]] | Interactive visual examples for all properties and elements |
| [[Template-Expressions]] | Variables, loops, conditionals with 13 operators |
| [[Flexbox-Layout]] | Direction, justify, align, wrapping, grow/shrink, positioning |
Expand Down
5 changes: 3 additions & 2 deletions docs/wiki/Template-Syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,7 @@ 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.
Embeds dynamically formatted text (Markdown, HTML, NDC, 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
Expand All @@ -518,7 +518,8 @@ Embeds dynamically formatted text (Markdown, HTML, etc.) from template data usin
| 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. |
| `format` | string | `""` | Content format: `markdown`, `html`, `ndc`, or any registered parser name. |
| `options` | dict? | `null` | Parser-specific options dictionary (used by NDC and custom parsers). |

See [[Element-Reference#content-element-control-flow]] for full details, element mapping, and examples.

Expand Down
3 changes: 3 additions & 0 deletions examples/assets/fonts/JetBrainsMono-Bold.ttf
Git LFS file not shown
3 changes: 3 additions & 0 deletions examples/assets/fonts/JetBrainsMono-Regular.ttf
Git LFS file not shown
1 change: 1 addition & 0 deletions examples/ndc-data/bank-a-balance-b64.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"body": "base64:GygxICAgICAgICAgICAgICAbKEludGNuamRzcSB+ZnlyIGYbKDEKICAgICAgICAbKEludGsbKDEuIDggKDgwMCkgMDAwLTAwLTAwChsoSWZsaHRjGygxOgpNT1NDT1csIFRFU1RPVkFZQSBVTC4sIDEKICAbKElsZm5mGygxICAgICAgICAbKElkaHR2ehsoMSAgICAgICAgIBsoSX5meXJqdmZuGygxCjAxLjAxLjI1ICAgICAxMDowMDowMCAgICAgICBBVE0wMDAwMQogICAgGyhJeWp2dGgbKDEgGyhJcmZobnMbKDEgKioqKioqKioqKioqMDAwMAoKICAgICAbKElqZ3RoZndiehsoMSAbKEljGygxIBsoSWRkamxqdhsoMSAbKElnYnkbKDEtGyhJcmpsZhsoMVwhCkFJRDogQTAwMDAwMDAwMDAwMDAgVEVTVENBUkQKCiAgICAgICAgICBURVNUQ0FSRCAwMSBWMS4wCgobKElkc2xmeGYbKDEgGyhJeWZrYnh5c3sbKDEKGyhJY2V2dmYbKDE6ICAgICAxMjM0NS42NyAbKEloZX4bKDEK"}
Loading