Skip to content

Commit 8d0183a

Browse files
committed
Init
0 parents  commit 8d0183a

247 files changed

Lines changed: 31791 additions & 0 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/ci.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
build:
14+
runs-on: ${{ matrix.os }}
15+
strategy:
16+
matrix:
17+
os: [ubuntu-latest, windows-latest, macos-latest]
18+
19+
steps:
20+
- uses: actions/checkout@v4
21+
22+
- name: Setup .NET
23+
uses: actions/setup-dotnet@v4
24+
with:
25+
dotnet-version: '10.0.x'
26+
27+
- name: Restore
28+
run: dotnet restore FlexRender.slnx
29+
30+
- name: Build
31+
run: dotnet build FlexRender.slnx --no-restore --configuration Release
32+
33+
- name: Test
34+
run: dotnet test FlexRender.slnx --no-build --configuration Release --logger "trx;LogFileName=results.trx"
35+
36+
- name: Upload test results
37+
uses: actions/upload-artifact@v4
38+
if: always()
39+
with:
40+
name: test-results-${{ matrix.os }}
41+
path: '**/results.trx'

.github/workflows/release.yml

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags: ['v*']
6+
7+
permissions:
8+
contents: write
9+
packages: write
10+
11+
env:
12+
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
13+
DOTNET_NOLOGO: true
14+
15+
jobs:
16+
release:
17+
runs-on: ubuntu-latest
18+
19+
steps:
20+
- uses: actions/checkout@v4
21+
with:
22+
fetch-depth: 0
23+
24+
- name: Setup .NET
25+
uses: actions/setup-dotnet@v4
26+
with:
27+
dotnet-version: '10.0.x'
28+
29+
- name: Extract version from tag
30+
id: version
31+
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
32+
33+
- name: Restore
34+
run: dotnet restore FlexRender.slnx
35+
36+
- name: Build
37+
run: dotnet build FlexRender.slnx --no-restore --configuration Release
38+
39+
- name: Test
40+
run: dotnet test FlexRender.slnx --no-build --configuration Release
41+
42+
- name: Pack NuGet packages
43+
run: |
44+
for project in \
45+
src/FlexRender.Core/FlexRender.Core.csproj \
46+
src/FlexRender.Yaml/FlexRender.Yaml.csproj \
47+
src/FlexRender.Skia/FlexRender.Skia.csproj \
48+
src/FlexRender.QrCode/FlexRender.QrCode.csproj \
49+
src/FlexRender.Barcode/FlexRender.Barcode.csproj \
50+
src/FlexRender.DependencyInjection/FlexRender.DependencyInjection.csproj \
51+
src/FlexRender.MetaPackage/FlexRender.MetaPackage.csproj \
52+
src/FlexRender.Cli/FlexRender.Cli.csproj; do
53+
dotnet pack "$project" \
54+
--no-build --configuration Release \
55+
-p:Version=${{ steps.version.outputs.VERSION }} \
56+
--output ./artifacts
57+
done
58+
59+
- name: Publish to NuGet.org
60+
env:
61+
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
62+
run: |
63+
dotnet nuget push ./artifacts/*.nupkg \
64+
--api-key "$NUGET_API_KEY" \
65+
--source https://api.nuget.org/v3/index.json \
66+
--skip-duplicate
67+
68+
dotnet nuget push ./artifacts/*.snupkg \
69+
--api-key "$NUGET_API_KEY" \
70+
--source https://api.nuget.org/v3/index.json \
71+
--skip-duplicate
72+
73+
- name: Create GitHub Release
74+
uses: softprops/action-gh-release@v2
75+
with:
76+
generate_release_notes: true
77+
files: ./artifacts/*.nupkg

.gitignore

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Build results
2+
[Dd]ebug/
3+
[Rr]elease/
4+
x64/
5+
x86/
6+
[Aa][Rr][Mm]/
7+
[Aa][Rr][Mm]64/
8+
bld/
9+
[Bb]in/
10+
[Oo]bj/
11+
[Ll]og/
12+
[Ll]ogs/
13+
14+
# Visual Studio
15+
.vs/
16+
*.user
17+
*.suo
18+
*.userosscache
19+
*.sln.docstates
20+
21+
# JetBrains Rider
22+
.idea/
23+
*.sln.iml
24+
25+
# NuGet
26+
*.nupkg
27+
*.snupkg
28+
.nuget/
29+
packages/
30+
*.nuget.props
31+
*.nuget.targets
32+
33+
# Test results
34+
[Tt]est[Rr]esult*/
35+
*.trx
36+
*.coverage
37+
*.coveragexml
38+
39+
# Worktrees
40+
.worktrees/
41+
42+
# Private templates (local only)
43+
examples/private/
44+
45+
# Private golden snapshot images
46+
**/golden/**/wb_receipt.png
47+
48+
# Figma metadata (private, large)
49+
docs/figma-wb-receipt-v2-metadata.txt
50+
51+
# Misc
52+
*.swp
53+
*~
54+
.DS_Store
55+
Thumbs.db

AGENTS.md

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
# FlexRender
2+
3+
A modular .NET library for rendering images from YAML templates with flexbox layout. Render-backend agnostic with SkiaSharp as the default backend. AOT-compatible, no reflection.
4+
5+
## LLM Documentation
6+
7+
- `llms.txt` -- concise project overview for LLM context windows (~200 lines)
8+
- `llms-full.txt` -- comprehensive reference with all YAML properties, API details, and conventions (~580 lines)
9+
10+
## Build & Test
11+
12+
```bash
13+
dotnet build FlexRender.slnx # Build entire solution
14+
dotnet test FlexRender.slnx # Run all tests
15+
dotnet test --filter "ClassName" # Filter by test class
16+
dotnet test --filter "MethodName" # Filter by test method
17+
UPDATE_SNAPSHOTS=true dotnet test # Regenerate golden snapshot images
18+
```
19+
20+
## Project Structure
21+
22+
```
23+
src/FlexRender.Core/ # Core library (0 external dependencies)
24+
Abstractions/ # ILayoutRenderer<T>, ITemplateParser, IResourceLoader
25+
Configuration/ # ResourceLimits, FlexRenderOptions
26+
Layout/ # Two-pass flexbox layout engine (LayoutEngine, LayoutNode, LayoutSize)
27+
Units/ # Unit, UnitParser, PaddingValues, PaddingParser
28+
Loaders/ # FileResourceLoader, Base64ResourceLoader, EmbeddedResourceLoader, HttpResourceLoader
29+
Parsing/Ast/ # Template, CanvasSettings, TemplateElement, TextElement, FlexElement, etc.
30+
TemplateEngine/ # TemplateProcessor, ExpressionLexer, ExpressionEvaluator
31+
Values/ # TemplateValue hierarchy (StringValue, NumberValue, etc.)
32+
33+
src/FlexRender.Yaml/ # YAML template parser (-> Core + YamlDotNet)
34+
Parsing/ # TemplateParser, YamlPreprocessor
35+
src/FlexRender.Skia/ # SkiaSharp renderer (-> Core + SkiaSharp)
36+
Abstractions/ # IFlexRenderer, IFontLoader, IImageLoader, IFontManager
37+
Rendering/ # SkiaRenderer, TextRenderer, FontManager, ColorParser, RotationHelper
38+
Loaders/ # FontLoader, ImageLoader
39+
Providers/ # IContentProvider<T,O>, ImageProvider
40+
src/FlexRender.QrCode/ # QR code provider (-> Skia + QRCoder)
41+
src/FlexRender.Barcode/ # Barcode provider (-> Skia)
42+
src/FlexRender.DependencyInjection/ # Microsoft.Extensions.DI integration
43+
src/FlexRender.MetaPackage/ # Meta-package (references all sub-packages)
44+
45+
src/FlexRender.Cli/ # CLI tool (System.CommandLine, uses all packages)
46+
Commands/ # render, validate, info, watch, debug-layout
47+
48+
tests/FlexRender.Tests/ # Unit + snapshot tests
49+
tests/FlexRender.Cli.Tests/ # CLI integration tests
50+
examples/ # Example YAML templates
51+
```
52+
53+
## NuGet Package Structure
54+
55+
```
56+
FlexRender.Core (0 external deps)
57+
^ ^
58+
| |
59+
FlexRender.Yaml FlexRender.Skia (YamlDotNet) (SkiaSharp)
60+
^ ^
61+
| |
62+
FlexRender.QrCode FlexRender.Barcode (QRCoder)
63+
| |
64+
FlexRender.DependencyInjection (Microsoft.Extensions.DI)
65+
|
66+
FlexRender.MetaPackage (references all)
67+
```
68+
69+
## Architecture
70+
71+
The rendering pipeline:
72+
73+
```
74+
YAML Template
75+
-> YamlPreprocessor (expand {{#each}}, {{#if}} at YAML level)
76+
-> TemplateParser (YAML -> AST: Template with CanvasSettings + TemplateElement tree)
77+
-> TemplateProcessor (resolve {{variable}} expressions in element properties)
78+
-> LayoutEngine (two-pass: MeasureAllIntrinsics -> ComputeLayout -> LayoutNode tree)
79+
-> SkiaRenderer (traverse LayoutNode tree -> draw to SKBitmap via SkiaSharp)
80+
```
81+
82+
### Two-Pass Layout Engine
83+
84+
1. **Pass 1 -- Intrinsic Measurement** (`MeasureAllIntrinsics`): Bottom-up traversal computes `IntrinsicSize` (MinWidth, MaxWidth, MinHeight, MaxHeight) for every element. Uses `TextMeasurer` delegate for content-based text sizing.
85+
2. **Pass 2 -- Layout** (`ComputeLayout`): Top-down traversal assigns positions and sizes, producing a `LayoutNode` tree with (X, Y, Width, Height).
86+
87+
### Key Classes by Stage
88+
89+
| Stage | Key Classes |
90+
|-------|------------|
91+
| Preprocessing | `YamlPreprocessor` |
92+
| Parsing | `TemplateParser`, `Template`, `CanvasSettings`, `TextElement`, `FlexElement`, `QrElement`, `BarcodeElement`, `ImageElement`, `SeparatorElement` |
93+
| Template Engine | `TemplateProcessor`, `ExpressionLexer`, `ExpressionEvaluator`, `TemplateContext` |
94+
| Layout | `LayoutEngine`, `LayoutNode`, `LayoutContext`, `LayoutSize`, `IntrinsicSize`, `Unit`, `UnitParser` |
95+
| Rendering | `SkiaRenderer`, `TextRenderer`, `FontManager`, `ColorParser`, `RotationHelper` |
96+
| Providers | `IContentProvider<T,O>`, `QrProvider`, `BarcodeProvider`, `ImageProvider` |
97+
| DI | `ServiceCollectionExtensions.AddFlexRender()`, `FlexRenderBuilder`, `FlexRenderOptions` |
98+
| Abstractions | `IFlexRenderer`, `ILayoutRenderer<T>`, `ITemplateParser` |
99+
| Values | `TemplateValue` (abstract), `StringValue`, `NumberValue`, `BoolValue`, `NullValue`, `ArrayValue`, `ObjectValue` |
100+
101+
## Coding Conventions
102+
103+
- **.NET 10**, C# latest, `Nullable=enable`, `TreatWarningsAsErrors=true`
104+
- **AOT compatible** -- `IsAotCompatible=true` on library and CLI. No reflection anywhere. Use pattern matching (`switch` on concrete types) for type dispatch
105+
- **`GeneratedRegex`** -- source-generated regex for AOT compatibility
106+
- **`sealed` classes** -- all leaf/value/concrete classes must be `sealed`
107+
- **`sealed record`** -- for token types and small immutable data
108+
- **`readonly record struct`** -- for small value types (`IntrinsicSize`, `LayoutRect`, `FlexItemProperties`)
109+
- **File-scoped namespaces** -- `namespace Foo.Bar;`
110+
- **XML documentation** -- `<summary>`, `<param>`, `<returns>`, `<exception>` on all public APIs
111+
- **Guard clauses** -- `ArgumentNullException.ThrowIfNull()`, `ArgumentException.ThrowIfNullOrWhiteSpace()`, `ObjectDisposedException.ThrowIf()`
112+
- **Immutability** -- value types are `readonly`, collections exposed as `IReadOnlyList<T>`
113+
- **Thread safety** -- `ConcurrentDictionary` where concurrent access is expected (e.g., `FontManager`)
114+
- **String comparison** -- `StringComparer.OrdinalIgnoreCase` for dictionaries keyed by names
115+
116+
## Resource Limits
117+
118+
All security limits are centralized in the `ResourceLimits` class (`Configuration/ResourceLimits.cs`) and configurable via the `FlexRenderBuilder.WithLimits()` method. Each property validates that values are positive and throws `ArgumentOutOfRangeException` on invalid input.
119+
120+
| Property | Default | Purpose |
121+
|----------|---------|---------|
122+
| `MaxTemplateFileSize` | 1 MB | YAML template file size |
123+
| `MaxDataFileSize` | 10 MB | JSON data file size |
124+
| `MaxPreprocessorNestingDepth` | 50 | Preprocessing block nesting |
125+
| `MaxPreprocessorInputSize` | 1 MB | Preprocessor input |
126+
| `MaxTemplateNestingDepth` | 100 | Expression nesting |
127+
| `MaxRenderDepth` | 100 | Render tree recursion |
128+
| `MaxImageSize` | 10 MB | Image loading |
129+
| `HttpTimeout` | 30s | Remote resource loading |
130+
131+
Configure limits via builder:
132+
133+
```csharp
134+
services.AddFlexRender(builder => builder
135+
.WithLimits(limits =>
136+
{
137+
limits.MaxRenderDepth = 200;
138+
limits.MaxTemplateFileSize = 2 * 1024 * 1024;
139+
}));
140+
```
141+
142+
Or directly when constructing a renderer:
143+
144+
```csharp
145+
var limits = new ResourceLimits { MaxRenderDepth = 200 };
146+
using var renderer = new SkiaRenderer(limits);
147+
```
148+
149+
These limits exist to prevent abuse and resource exhaustion. Never remove or weaken them without explicit justification.
150+
151+
## Test Conventions
152+
153+
- **Framework**: xUnit with `[Fact]` and `[Theory]`/`[InlineData]`
154+
- **Assertions**: `Assert.*` from xUnit; `FluentAssertions` in some tests
155+
- **Naming**: `MethodUnderTest_Scenario_ExpectedResult` (e.g., `Parse_SimpleTextElement_ParsesCorrectly`)
156+
- **Class naming**: `{ClassName}Tests`
157+
- **Organization**: Mirrors source structure -- `Tests/Values/`, `Tests/Layout/`, `Tests/Parsing/`, etc.
158+
- **Snapshot testing**: `SnapshotTestBase` with platform-specific golden images in `golden/{macos,linux,windows}/`. Pixel-by-pixel comparison with `colorThreshold=5`. Set `UPDATE_SNAPSHOTS=true` to regenerate
159+
- **Pattern**: Arrange-Act-Assert
160+
161+
## Git Conventions
162+
163+
### Branching
164+
165+
All new features and non-trivial changes must be developed in separate branches. Never commit feature work directly to `main`.
166+
167+
- **Branch naming**: `type/short-description` (e.g., `feat/qr-provider`, `fix/layout-overflow`, `refactor/font-manager`)
168+
- **Types**: `feat`, `fix`, `refactor`, `build`, `test`, `docs`, `chore`
169+
- **Do NOT merge into `main`** -- leave the feature branch as-is after completing work. Merging is done manually by the maintainer or via GitHub PR
170+
- **Do NOT use git worktrees** -- work directly in the repository checkout. Worktrees add unnecessary complexity and cause issues with stash conflicts and asset path resolution
171+
172+
### Commits
173+
174+
Conventional Commits: `type(scope): description`
175+
176+
- **Types**: `feat`, `fix`, `refactor`, `build`, `test`, `docs`, `chore`
177+
- **Scopes** (optional): `parser`, `layout`, `renderer`, `ast`, `examples`, `cli`
178+
- **Description**: lowercase, imperative mood
179+
180+
## CLI Tool
181+
182+
```bash
183+
dotnet run --project src/FlexRender.Cli -- render template.yaml -d data.json -o output.png
184+
dotnet run --project src/FlexRender.Cli -- validate template.yaml
185+
dotnet run --project src/FlexRender.Cli -- info template.yaml
186+
dotnet run --project src/FlexRender.Cli -- watch template.yaml -d data.json -o preview.png
187+
dotnet run --project src/FlexRender.Cli -- debug-layout template.yaml -d data.json
188+
```
189+
190+
Global options: `-v`/`--verbose`, `--fonts <dir>`, `--scale <float>`
191+
192+
**Working directory matters:** CLI resolves all relative paths (template, data, fonts, image `src`) from the current working directory. When running examples, `cd` into `examples/` first, otherwise assets like `assets/fonts/Inter-Regular.ttf` won't be found.
193+
194+
## Common Tasks
195+
196+
### Add new element type
197+
198+
1. Create AST model in `Parsing/Ast/` (sealed class extending `TemplateElement`)
199+
2. Add parser function in `TemplateParser.cs` -- register in `_elementParsers` dictionary
200+
3. Add flex-item property support via `switch` pattern matching in layout engine
201+
4. Add rendering in `SkiaRenderer.RenderNode()` or create a provider
202+
5. Write tests for each step
203+
204+
### Add new template expression
205+
206+
1. Add token type as `sealed record` in `ExpressionToken.cs`
207+
2. Add lexer support in `ExpressionLexer.cs`
208+
3. Add evaluation in `TemplateProcessor.cs`
209+
4. Write tests
210+
211+
## Important Patterns
212+
213+
- **TextMeasurer delegate** -- `Func<TextElement, float, float, LayoutSize>?` on `LayoutEngine` (element, fontSize, maxWidth), wired up by `SkiaRenderer` for content-based text sizing with wrap-aware height measurement
214+
- **Content providers** -- `IContentProvider<TElement,TOutput>` returns rendered content for QR, barcode, image elements
215+
- **Resource loader chain** -- `IResourceLoader` with `CanHandle()`, `Load()`, `Priority` -- chain of responsibility pattern
216+
- **Flex-item properties** -- declared per concrete element class, dispatched via `switch` pattern matching (not on base class)
217+
- **Template processing layers** -- structural (`YamlPreprocessor` for `{{#each}}`/`{{#if}}`) and inline (`TemplateProcessor` for `{{variable}}`)

0 commit comments

Comments
 (0)