Skip to content

Commit 9b95eb7

Browse files
authored
feat(ndc): add ndc content (#5)
* feat(ndc): add aptra ndc content provider
1 parent d5dbd88 commit 9b95eb7

166 files changed

Lines changed: 6672 additions & 174 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/release.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ jobs:
7777
src/FlexRender.SvgElement/FlexRender.SvgElement.csproj \
7878
src/FlexRender.Content.Markdown/FlexRender.Content.Markdown.csproj \
7979
src/FlexRender.Content.Html/FlexRender.Content.Html.csproj \
80+
src/FlexRender.Content.Ndc/FlexRender.Content.Ndc.csproj \
8081
src/FlexRender.DependencyInjection/FlexRender.DependencyInjection.csproj \
8182
src/FlexRender.MetaPackage/FlexRender.MetaPackage.csproj; do
8283
dotnet pack "$project" \

AGENTS.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ src/FlexRender.ImageSharp.Render/ # ImageSharp renderer (-> Core + SixLabors.Ima
5656
src/FlexRender.ImageSharp/ # ImageSharp backend meta-package (renderer + providers)
5757
src/FlexRender.Svg.Render/ # SVG output renderer (-> Core)
5858
src/FlexRender.Svg/ # SVG backend meta-package (renderer + providers)
59+
src/FlexRender.Content.Ndc/ # NDC (ATM receipt) content parser (-> Core)
5960
src/FlexRender.DependencyInjection/ # Microsoft.Extensions.DI integration
6061
src/FlexRender.MetaPackage/ # Meta-package (core + all backends + DI)
6162
@@ -167,15 +168,16 @@ byte[] png = await render.Render(_templates["receipt"], data);
167168
|-------|------------|
168169
| Configuration | `FlexRenderBuilder`, `SkiaBuilder`, `FlexRenderOptions`, `ResourceLimits` |
169170
| Abstractions | `IFlexRender`, `IResourceLoader` |
170-
| Parsing | `TemplateParser`, `Template`, `CanvasSettings`, `TextElement`, `FlexElement`, `QrElement`, `BarcodeElement`, `ImageElement`, `SeparatorElement`, `TableElement`, `TableColumn`, `TableRow`, `EachElement`, `IfElement` |
171-
| Template Engine | `TemplateExpander`, `TemplateProcessor`, `ExpressionLexer`, `ExpressionEvaluator`, `TemplateContext`, `InlineExpressionParser`, `InlineExpressionEvaluator`, `FilterRegistry`, `ITemplateFilter` |
171+
| Parsing | `TemplateParser`, `Template`, `CanvasSettings`, `TextElement`, `FlexElement`, `QrElement`, `BarcodeElement`, `ImageElement`, `SeparatorElement`, `TableElement`, `TableColumn`, `TableRow`, `EachElement`, `IfElement`, `ContentElement` |
172+
| Template Engine | `TemplateExpander`, `TemplateProcessor`, `ExpressionLexer`, `ExpressionEvaluator`, `TemplateContext`, `InlineExpressionParser`, `InlineExpressionEvaluator`, `FilterRegistry`, `ITemplateFilter`, `ContentSourceResolver` |
172173
| Layout | `LayoutEngine`, `LayoutNode`, `LayoutContext`, `LayoutSize`, `IntrinsicSize`, `Unit`, `UnitParser`, `MarginValue`, `MarginValues`, `PaddingParser.ParseMargin` |
173174
| Rendering (Skia) | `SkiaRender` (IFlexRender impl), `SkiaRenderer`, `TextRenderer`, `FontManager`, `ColorParser`, `RotationHelper`, `BmpEncoder`, `BoxShadowParser`, `GradientParser` |
174175
| Rendering (ImageSharp) | `ImageSharpRender` (IFlexRender impl), `ImageSharpRenderingEngine`, `ImageSharpTextRenderer`, `ImageSharpFontManager` |
175176
| Providers | `IContentProvider<T,O>`, `QrProvider`, `BarcodeProvider`, `ImageProvider` |
176177
| Loaders | `FileResourceLoader`, `Base64ResourceLoader`, `EmbeddedResourceLoader`, `HttpResourceLoader` |
177178
| DI | `ServiceCollectionExtensions.AddFlexRender()` |
178179
| Values | `TemplateValue` (abstract), `StringValue`, `NumberValue`, `BoolValue`, `NullValue`, `ArrayValue`, `ObjectValue` |
180+
| Content Parsers | `IContentParser`, `IBinaryContentParser`, `ContentParserRegistry`, `ContentSourceResolver`, `NdcContentParser` |
179181

180182
## Coding Conventions
181183

@@ -317,6 +319,7 @@ The release workflow (`.github/workflows/release.yml`) publishes all packages to
317319
| QR providers | `FlexRender.QrCode.Skia.Render`, `FlexRender.QrCode.ImageSharp.Render`, `FlexRender.QrCode.Svg.Render` |
318320
| Barcode providers | `FlexRender.Barcode.Skia.Render`, `FlexRender.Barcode.ImageSharp.Render`, `FlexRender.Barcode.Svg.Render` |
319321
| SvgElement providers | `FlexRender.SvgElement.Skia.Render`, `FlexRender.SvgElement.Svg.Render` |
322+
| Content parsers | `FlexRender.Content.Markdown`, `FlexRender.Content.Html`, `FlexRender.Content.Ndc` |
320323
| Extensions | `FlexRender.HarfBuzz` |
321324
| Meta (backend) | `FlexRender.Skia`, `FlexRender.ImageSharp`, `FlexRender.Svg` |
322325
| Meta (feature) | `FlexRender.QrCode`, `FlexRender.Barcode`, `FlexRender.SvgElement` |

FlexRender.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
<Folder Name="/src/Content/">
3636
<Project Path="src/FlexRender.Content.Markdown/FlexRender.Content.Markdown.csproj" />
3737
<Project Path="src/FlexRender.Content.Html/FlexRender.Content.Html.csproj" />
38+
<Project Path="src/FlexRender.Content.Ndc/FlexRender.Content.Ndc.csproj" />
3839
</Folder>
3940
<Folder Name="/src/SvgElement/">
4041
<Project Path="src/FlexRender.SvgElement.Skia.Render/FlexRender.SvgElement.Skia.Render.csproj" />

docs/wiki/API-Reference.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,10 @@ Builder for configuring and creating `IFlexRender` instances. Defined in `FlexRe
8080
| `WithoutDefaultLoaders()` | Remove default File and Base64 loaders (sandboxed mode) |
8181
| `WithoutDefaultFilters()` | Remove all 8 built-in filters (enabled by default), leaving only custom-registered filters |
8282
| `WithContentParser(IContentParser)` | Register a content parser for `type: content` elements |
83+
| `WithBinaryContentParser(IBinaryContentParser)` | Register a binary content parser for `type: content` elements |
8384
| `WithMarkdown()` | Enable Markdown content parsing (`format: markdown`) |
8485
| `WithHtml()` | Enable HTML content parsing (`format: html`) |
86+
| `WithNdc()` | Enable NDC content parsing (`format: ndc`) |
8587
| `Build()` | Create the configured `IFlexRender` instance |
8688

8789
### Usage
@@ -124,6 +126,26 @@ var render = new FlexRenderBuilder()
124126

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

129+
### Data Binding with Binary Content
130+
131+
Content elements support binary data via `BytesValue`:
132+
133+
```csharp
134+
// String content (for Markdown, HTML parsers)
135+
var data = new ObjectValue { ["body"] = new StringValue("# Hello") };
136+
137+
// Binary content (for NDC and other IBinaryContentParser implementations)
138+
var data = new ObjectValue { ["receipt"] = new BytesValue(rawBytes) };
139+
140+
// From Stream
141+
var data = new ObjectValue { ["receipt"] = BytesValue.FromStream(fileStream) };
142+
143+
// From base64 in YAML source
144+
// source: "base64:SGVsbG8gV29ybGQ="
145+
```
146+
147+
`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.
148+
127149
---
128150

129151
## SkiaBuilder
@@ -514,6 +536,11 @@ var obj = new ObjectValue
514536
["zip"] = "123456"
515537
}
516538
};
539+
540+
// Binary data
541+
var bytes = new BytesValue(binaryData); // from byte[]
542+
var bytes = new BytesValue(readOnlyMemory); // from ReadOnlyMemory<byte>
543+
var bytes = BytesValue.FromStream(stream); // from Stream
517544
```
518545

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

528556
---
529557

docs/wiki/Element-Reference.md

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1451,7 +1451,7 @@ Renders tabular data with configurable columns, optional header row, and support
14511451

14521452
## Content Element (Control Flow)
14531453

1454-
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.
1454+
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.
14551455

14561456
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.
14571457

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

14661466
| Property | YAML Name | Type | Default | Valid Values | Expression | Description |
14671467
|----------|-----------|------|---------|--------------|-----------|-------------|
1468-
| Source | `source` | string | `""` | Any string, typically `{{variable}}` | Yes | The formatted text to parse. Usually bound to a data variable. |
1468+
| 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. |
14691469
| Format | `format` | string | `""` | `markdown`, `html`, or any registered parser name | Yes | The content format. Must match a registered `IContentParser.FormatName`. |
1470+
| Options | `options` | dict? | `null` | Key-value dictionary | No | Parser-specific options (e.g., NDC `columns`, `charsets`). Passed to the content parser. |
1471+
1472+
### Content Source Resolution
1473+
1474+
The `source` property is resolved at render time through `ContentSourceResolver`, which supports multiple input types:
1475+
1476+
| Source Format | Example | Resolved As | Description |
1477+
|---------------|---------|-------------|-------------|
1478+
| 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. |
1479+
| `base64:` prefix | `source: "base64:SGVsbG8="` | Binary (`byte[]`) | Base64-encoded payload decoded into bytes. Useful for embedding binary data in JSON/YAML. |
1480+
| `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. |
1481+
| `text:` prefix | `source: "text:# Hello"` | Text (`string`) | Forces text interpretation, skipping file path detection. |
1482+
| 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. |
1483+
| Plain text | `source: "**bold text**"` | Text (`string`) | Default fallback -- treated as literal text content. |
1484+
1485+
**Resolution order:** The resolver processes sources in the order listed above. The first matching rule wins.
1486+
1487+
**Binary vs Text parsers:**
1488+
- `IContentParser` receives text (`string`) -- used by Markdown, HTML parsers
1489+
- `IBinaryContentParser` receives binary data (`ReadOnlyMemory<byte>`) -- used by NDC parser
1490+
- When source resolves to binary and the parser implements `IBinaryContentParser`, bytes are passed directly without encoding conversion
1491+
- When source resolves to binary but the parser only implements `IContentParser`, bytes are decoded as UTF-8 text
1492+
1493+
**Data binding examples:**
1494+
1495+
```csharp
1496+
// String data -- parsed as text
1497+
var data = new ObjectValue { ["body"] = new StringValue("# Hello World") };
1498+
1499+
// Binary data -- passed directly to IBinaryContentParser
1500+
var data = new ObjectValue { ["receiptData"] = new BytesValue(ndcBytes) };
1501+
1502+
// Binary data with MIME type
1503+
var data = new ObjectValue { ["receiptData"] = new BytesValue(ndcBytes, "application/octet-stream") };
1504+
1505+
// From Stream
1506+
var data = new ObjectValue { ["receiptData"] = BytesValue.FromStream(stream) };
1507+
```
14701508

14711509
### Supported Formats
14721510

14731511
| Format | Package | Builder Method | Library |
14741512
|--------|---------|----------------|---------|
14751513
| `markdown` | `FlexRender.Content.Markdown` | `.WithMarkdown()` | Markdig |
14761514
| `html` | `FlexRender.Content.Html` | `.WithHtml()` | HtmlAgilityPack |
1515+
| `ndc` | `FlexRender.Content.Ndc` | `.WithNdc()` | (none) |
14771516

14781517
### Element Mapping
14791518

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

1532+
### NDC Format Options
1533+
1534+
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`.
1535+
1536+
#### Properties
1537+
1538+
The `content` element with `format: ndc` supports an `options` block:
1539+
1540+
| Option | Type | Default | Description |
1541+
|--------|------|---------|-------------|
1542+
| `columns` | int | 40 | Maximum characters per line (auto-wrapping) |
1543+
| `input_encoding` | string | `"latin1"` | Input byte encoding (`latin1`, `utf-8`, `iso-8859-1`, `ascii`) |
1544+
| `font_family` | string | null | Global font family for all text |
1545+
| `char_width_ratio` | double | 0.6 | Character width as fraction of font size |
1546+
| `charsets` | dict | `{}` | Per-charset style overrides (see below) |
1547+
1548+
#### Charset Style Properties
1549+
1550+
Each charset designator (e.g., `"1"`, `"I"`, `">"`) can have individual styling:
1551+
1552+
| Property | Type | Default | Description |
1553+
|----------|------|---------|-------------|
1554+
| `font` | string | null | Font registration name |
1555+
| `font_family` | string | null | Font family (overrides global) |
1556+
| `font_style` | string | null | `"bold"`, `"italic"`, `"bold-italic"`, `"regular"` |
1557+
| `font_size` | int | null | Explicit font size in pixels |
1558+
| `color` | string | null | Hex color (e.g., `"#333333"`) |
1559+
| `encoding` | string | null | Character encoding: `"qwerty-jcuken"`, `"none"`, `"ascii"` |
1560+
| `uppercase` | bool | false | Convert text to uppercase |
1561+
1562+
#### Example: NDC Receipt
1563+
1564+
```yaml
1565+
canvas:
1566+
fixed: width
1567+
width: 384
1568+
background: "#ffffff"
1569+
1570+
fonts:
1571+
- "assets/fonts/JetBrainsMono-Regular.ttf"
1572+
- "assets/fonts/JetBrainsMono-Bold.ttf"
1573+
1574+
layout:
1575+
- type: content
1576+
source: "{{receiptData}}"
1577+
format: ndc
1578+
options:
1579+
columns: 40
1580+
input_encoding: latin1
1581+
font_family: "JetBrains Mono"
1582+
charsets:
1583+
"1":
1584+
encoding: "qwerty-jcuken"
1585+
font_style: bold
1586+
```
1587+
1588+
Data (JSON with base64-encoded NDC binary):
1589+
```json
1590+
{
1591+
"receiptData": "base64:G1sxfjQwHSgxHQ=="
1592+
}
1593+
```
1594+
1595+
The NDC parser produces `TextElement`, `BarcodeElement`, and `SeparatorElement` nodes with auto-calculated font sizes based on the parent element width.
1596+
14931597
### Example: Markdown Content
14941598

14951599
```yaml

docs/wiki/Getting-Started.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ Install only what you need:
4848
| `FlexRender.SvgElement` | SvgElement meta-package (all renderers) | Svg.Skia |
4949
| `FlexRender.Content.Markdown` | Markdown content parsing for `type: content` | Markdig |
5050
| `FlexRender.Content.Html` | HTML content parsing for `type: content` | HtmlAgilityPack |
51+
| `FlexRender.Content.Ndc` | NDC (ATM receipt) content parsing for `type: content` | (none) |
5152
| `FlexRender.HarfBuzz` | HarfBuzz text shaping for Arabic/Hebrew | SkiaSharp.HarfBuzz |
5253
| `FlexRender.Http` | HTTP/HTTPS resource loading | None |
5354
| `FlexRender.DependencyInjection` | Microsoft DI integration | Microsoft.Extensions.DI |
@@ -196,6 +197,7 @@ Native rendering via SkiaSharp. Best quality, widest feature set.
196197
var render = new FlexRenderBuilder()
197198
.WithMarkdown() // Markdown content parsing
198199
.WithHtml() // HTML content parsing
200+
.WithNdc() // NDC ATM receipt parsing
199201
.WithSkia(skia => skia
200202
.WithQr() // QR code support
201203
.WithBarcode() // Barcode support
@@ -205,7 +207,7 @@ var render = new FlexRenderBuilder()
205207

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

211213
### ImageSharp Backend

docs/wiki/Home.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ A modular .NET library for rendering images from YAML templates with a full CSS
1414
- **RTL Support** -- right-to-left layout with `text-direction: rtl`, logical alignment (`start`/`end`), HarfBuzz text shaping for Arabic/Hebrew
1515
- **Template engine** -- variables (`{{name}}`), inline expressions (`{{price * qty | currency}}`), loops (`type: each`), conditionals (`type: if` with 13 operators)
1616
- **Inline expressions** -- arithmetic (`+`, `-`, `*`, `/`), null coalescing (`??`), 8 built-in filters enabled by default (`currency`, `currencySymbol`, `number`, `upper`, `lower`, `trim`, `truncate`, `format`)
17-
- **Rich content types** -- text, images, SVG, QR codes, barcodes, separators, tables
17+
- **Rich content types** -- text, images, SVG, QR codes, barcodes, separators, tables, NDC receipts
1818
- **Visual effects** -- opacity, box-shadow, linear and radial gradient backgrounds
1919
- **Multiple output formats** -- PNG, JPEG, BMP (6 color modes), Raw pixels, with per-call format options
2020
- **AOT-ready** -- no reflection, no `dynamic`, works with Native AOT publishing
@@ -91,8 +91,8 @@ byte[] png = await render.RenderFile("template.yaml", data);
9191
| Page | Description |
9292
|------|-------------|
9393
| [[Getting-Started]] | Installation, first template, rendering approaches |
94-
| [[Template-Syntax]] | Canvas, all 10 element types, common properties, units |
95-
| [[Element-Reference]] | Complete property reference for all 10 element types with examples |
94+
| [[Template-Syntax]] | Canvas, all 11 element types, common properties, units |
95+
| [[Element-Reference]] | Complete property reference for all 11 element types with examples |
9696
| [[Visual-Reference]] | Interactive visual examples for all properties and elements |
9797
| [[Template-Expressions]] | Variables, loops, conditionals with 13 operators |
9898
| [[Flexbox-Layout]] | Direction, justify, align, wrapping, grow/shrink, positioning |

docs/wiki/Template-Syntax.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,7 @@ Renders tabular data with configurable columns, optional headers, and support fo
507507

508508
### content
509509

510-
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.
510+
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.
511511

512512
```yaml
513513
- type: content
@@ -518,7 +518,8 @@ Embeds dynamically formatted text (Markdown, HTML, etc.) from template data usin
518518
| Property | Type | Default | Description |
519519
|----------|------|---------|-------------|
520520
| `source` | string | `""` | The formatted text to parse. Usually bound to a `{{variable}}`. |
521-
| `format` | string | `""` | Content format: `markdown`, `html`, or any registered parser name. |
521+
| `format` | string | `""` | Content format: `markdown`, `html`, `ndc`, or any registered parser name. |
522+
| `options` | dict? | `null` | Parser-specific options dictionary (used by NDC and custom parsers). |
522523

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

271 KB
Binary file not shown.
267 KB
Binary file not shown.

0 commit comments

Comments
 (0)