|
| 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