Skip to content

Commit aea3b0e

Browse files
committed
feat: implement full CSS flexbox specification (Phases 0-3)
Two-pass layout engine with iterative freeze algorithm inspired by Facebook Yoga. Consolidate flex item properties into TemplateElement base class, add flex-wrap with greedy line-breaking, per-line flex resolution, align-content distribution (7 modes), and wrap-reverse. Phases implemented: - Phase 0-1: Foundation (flex properties on base class, align-self, display:none, direction reverse, non-uniform padding/margin) - Phase 2: Core flexbox (iterative freeze, flex-basis with min/max clamping, factor flooring, shrink scaled by basis, overflow fallback, justify-content SpaceEvenly, SkiaRenderer align support) - Phase 3: Wrapping (flex-wrap, CalculateFlexLines, ResolveFlexForLine, LayoutWrappedFlex, align-content with overflow fallback, wrap-reverse, row-gap/column-gap, MaxFlexLines resource limit) All 1151 tests pass on both net8.0 and net10.0. docs: add flexbox expansion documentation and improve Linux/Docker visibility Add comprehensive documentation for the flexbox expansion project: - Implementation plan, algorithm reviews, test fixtures, architecture reviews, and Yoga comparison review (20 docs, ~380KB) - API reference for all flexbox properties (1152 lines) Improve SkiaSharp Linux/Docker discoverability: - Add prominent [!IMPORTANT] callout in README after install command - Add Linux note to NuGet package descriptions (Skia + MetaPackage) - Add Linux/Docker warning to llms.txt and llms-full.txt - Add MaxFlexLines to AGENTS.md resource limits table feat: implement CSS positioning, overflow, aspect-ratio, and auto margins (Phases 4-5) Phase 4 — Advanced Features: - Position absolute: excluded from flex flow, inset-based positioning and sizing - Position relative: offset from normal flow (left/right/top/bottom with CSS priority) - Overflow:hidden: canvas clipping in SkiaRenderer via Save/ClipRect/Restore - Aspect ratio: width/height inference when one dimension is known Phase 5 — Auto Margins: - MarginValue/MarginValues readonly record structs with auto support - PaddingParser.ParseMargin for auto token handling in CSS shorthand - Main axis auto margins consume free space before justify-content - Cross axis auto margins override align-items (center/push) - Works for both Row and Column directions Tests: 1204 total (53 new), 0 failures on net8.0 and net10.0 fix: absolute positioning fallback + aspect-ratio after flex-grow - Absolute children without insets now respect parent's justify-content and align-items instead of defaulting to (0,0) - Aspect ratio is re-applied after flex-grow/shrink resolution in both column and row layouts - Updated AGENTS.md, llms.txt, llms-full.txt with Phase 4-5 features - Added final architecture and algorithm review documents - 1207 tests passing test: add column-direction tests for absolute positioning and aspect-ratio Cover column layout paths for absolute justify/align fallback and aspect-ratio after flex-grow (1210 tests) fix: cross-axis alignment now subtracts margins in non-wrapped paths Center/End/Stretch cross-axis alignment formulas in LayoutColumnFlex and LayoutRowFlex now correctly subtract margins from available space, matching the wrapped path behavior. Added 6 tests (1216 total). feat: add configurable BMP color depth with 6 encoding modes Add BmpColorMode enum with Bgra32 (32-bit), Rgb24 (24-bit), Rgb565 (16-bit), Grayscale8 (8-bit), Grayscale4 (4-bit), and Monochrome1 (1-bit) modes. Each mode produces valid BMP files with proper headers, color tables, row padding, and sub-byte pixel packing. Expose --bmp-color CLI option in render and watch commands. Pass color mode through SkiaRender → SkiaRenderer → BmpEncoder pipeline. refactor: address code review findings in BmpEncoder Replace stackalloc with ArrayPool<byte> for row buffers exceeding 1024 bytes to prevent stack overflow on wide images. Add exhaustive default throws in WritePixelData and WriteColorTable switch statements. Standardize row buffer clearing with Clear() across all modes. Add Math.Min clamp to Grayscale8 luminance for defensive correctness.
1 parent 75a12d0 commit aea3b0e

74 files changed

Lines changed: 18679 additions & 748 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.

AGENTS.md

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ src/FlexRender.Core/ # Core library (0 external dependencies)
2424
Abstractions/ # ILayoutRenderer<T>, ITemplateParser, IResourceLoader
2525
Configuration/ # ResourceLimits, FlexRenderOptions
2626
Layout/ # Two-pass flexbox layout engine (LayoutEngine, LayoutNode, LayoutSize)
27-
Units/ # Unit, UnitParser, PaddingValues, PaddingParser
27+
Units/ # Unit, UnitParser, PaddingValues, PaddingParser, MarginValue, MarginValues
2828
Loaders/ # FileResourceLoader, Base64ResourceLoader, EmbeddedResourceLoader
2929
Parsing/Ast/ # Template, CanvasSettings, TemplateElement, TextElement, FlexElement, etc.
3030
TemplateEngine/ # TemplateProcessor, ExpressionLexer, ExpressionEvaluator
@@ -145,7 +145,7 @@ byte[] png = await render.Render(_templates["receipt"], data);
145145
| Abstractions | `IFlexRender`, `IResourceLoader` |
146146
| Parsing | `TemplateParser`, `Template`, `CanvasSettings`, `TextElement`, `FlexElement`, `QrElement`, `BarcodeElement`, `ImageElement`, `SeparatorElement`, `EachElement`, `IfElement` |
147147
| Template Engine | `TemplateExpander`, `TemplateProcessor`, `ExpressionLexer`, `ExpressionEvaluator`, `TemplateContext` |
148-
| Layout | `LayoutEngine`, `LayoutNode`, `LayoutContext`, `LayoutSize`, `IntrinsicSize`, `Unit`, `UnitParser` |
148+
| Layout | `LayoutEngine`, `LayoutNode`, `LayoutContext`, `LayoutSize`, `IntrinsicSize`, `Unit`, `UnitParser`, `MarginValue`, `MarginValues`, `PaddingParser.ParseMargin` |
149149
| Rendering | `SkiaRender` (IFlexRender impl), `SkiaRenderer`, `TextRenderer`, `FontManager`, `ColorParser`, `RotationHelper`, `BmpEncoder` |
150150
| Providers | `IContentProvider<T,O>`, `QrProvider`, `BarcodeProvider`, `ImageProvider` |
151151
| Loaders | `FileResourceLoader`, `Base64ResourceLoader`, `EmbeddedResourceLoader`, `HttpResourceLoader` |
@@ -159,7 +159,7 @@ byte[] png = await render.Render(_templates["receipt"], data);
159159
- **`GeneratedRegex`** -- source-generated regex for AOT compatibility
160160
- **`sealed` classes** -- all leaf/value/concrete classes must be `sealed`
161161
- **`sealed record`** -- for token types and small immutable data
162-
- **`readonly record struct`** -- for small value types (`IntrinsicSize`, `LayoutRect`, `FlexItemProperties`)
162+
- **`readonly record struct`** -- for small value types (`IntrinsicSize`, `LayoutRect`, `PaddingValues`)
163163
- **File-scoped namespaces** -- `namespace Foo.Bar;`
164164
- **XML documentation** -- `<summary>`, `<param>`, `<returns>`, `<exception>` on all public APIs
165165
- **Guard clauses** -- `ArgumentNullException.ThrowIfNull()`, `ArgumentException.ThrowIfNullOrWhiteSpace()`, `ObjectDisposedException.ThrowIf()`
@@ -178,6 +178,7 @@ All security limits are centralized in the `ResourceLimits` class (`Configuratio
178178
| `MaxTemplateNestingDepth` | 100 | Expression nesting |
179179
| `MaxRenderDepth` | 100 | Render tree recursion |
180180
| `MaxImageSize` | 10 MB | Image loading |
181+
| `MaxFlexLines` | 1000 | Maximum flex lines when wrapping |
181182

182183
Configure limits via builder:
183184

@@ -201,6 +202,7 @@ These limits exist to prevent abuse and resource exhaustion. Never remove or wea
201202

202203
## Test Conventions
203204

205+
- **Total tests**: 1204 (unit + snapshot + integration)
204206
- **Framework**: xUnit with `[Fact]` and `[Theory]`/`[InlineData]`
205207
- **Assertions**: `Assert.*` from xUnit; `FluentAssertions` in some tests
206208
- **Naming**: `MethodUnderTest_Scenario_ExpectedResult` (e.g., `Parse_SimpleTextElement_ParsesCorrectly`)
@@ -364,6 +366,85 @@ var sb = new StringBuilder(estimatedCapacity);
364366
- **Flex-item properties** -- declared per concrete element class, dispatched via `switch` pattern matching (not on base class)
365367
- **Template processing layers** -- AST-level (`TemplateExpander` for `type: each`/`type: if`) and inline (`TemplateProcessor` for `{{variable}}`)
366368

369+
## AST Element Properties
370+
371+
### TemplateElement (base class)
372+
373+
All elements inherit these properties:
374+
375+
| Property | Type | Default | Description |
376+
|----------|------|---------|-------------|
377+
| `Rotate` | string | `"none"` | Rotation of the element |
378+
| `Background` | string? | null | Background color in hex format |
379+
| `Padding` | string | `"0"` | Padding inside (px, %, em, CSS shorthand) |
380+
| `Margin` | string | `"0"` | Margin outside (px, %, em, auto, CSS shorthand) |
381+
| `Display` | Display | `Flex` | Display mode (Flex, None) |
382+
| `Grow` | float | 0 | Flex grow factor |
383+
| `Shrink` | float | 1 | Flex shrink factor |
384+
| `Basis` | string | `"auto"` | Flex basis (px, %, em, auto) |
385+
| `AlignSelf` | AlignSelf | `Auto` | Self alignment override |
386+
| `Order` | int | 0 | Display order |
387+
| `Width` | string? | null | Width (px, %, em, auto) |
388+
| `Height` | string? | null | Height (px, %, em, auto) |
389+
| `MinWidth` | string? | null | Minimum width constraint |
390+
| `MaxWidth` | string? | null | Maximum width constraint |
391+
| `MinHeight` | string? | null | Minimum height constraint |
392+
| `MaxHeight` | string? | null | Maximum height constraint |
393+
| `Position` | Position | `Static` | Positioning mode (Static, Relative, Absolute) |
394+
| `Top` | string? | null | Top inset for positioned elements |
395+
| `Right` | string? | null | Right inset for positioned elements |
396+
| `Bottom` | string? | null | Bottom inset for positioned elements |
397+
| `Left` | string? | null | Left inset for positioned elements |
398+
| `AspectRatio` | float? | null | Width/height ratio; when one dimension is known, the other is computed |
399+
400+
### FlexElement (container)
401+
402+
Additional container-only properties:
403+
404+
| Property | Type | Default | Description |
405+
|----------|------|---------|-------------|
406+
| `Direction` | FlexDirection | `Column` | Main axis direction |
407+
| `Wrap` | FlexWrap | `NoWrap` | Whether items wrap to new lines |
408+
| `Gap` | string | `"0"` | Gap shorthand (sets both row-gap and column-gap) |
409+
| `ColumnGap` | string? | null | Gap between items along main axis |
410+
| `RowGap` | string? | null | Gap between wrapped lines |
411+
| `Justify` | JustifyContent | `Start` | Main axis alignment |
412+
| `Align` | AlignItems | `Stretch` | Cross axis alignment |
413+
| `AlignContent` | AlignContent | `Start` | Alignment of wrapped lines (note: CSS default is Stretch) |
414+
| `Overflow` | Overflow | `Visible` | Content overflow behavior (Visible, Hidden) |
415+
416+
## Layout Enums
417+
418+
All enums in `FlexRender.Layout.FlexEnums`:
419+
420+
| Enum | Values | Description |
421+
|------|--------|-------------|
422+
| `FlexDirection` | Row, Column, RowReverse, ColumnReverse | Main axis direction |
423+
| `FlexWrap` | NoWrap, Wrap, WrapReverse | Line wrapping behavior |
424+
| `JustifyContent` | Start, Center, End, SpaceBetween, SpaceAround, SpaceEvenly | Main axis alignment |
425+
| `AlignItems` | Start, Center, End, Stretch, Baseline | Cross axis alignment |
426+
| `AlignContent` | Start, Center, End, Stretch, SpaceBetween, SpaceAround, SpaceEvenly | Multi-line alignment |
427+
| `AlignSelf` | Auto, Start, Center, End, Stretch, Baseline | Per-item alignment override |
428+
| `Display` | Flex, None | Element visibility in layout |
429+
| `Position` | Static, Relative, Absolute | CSS positioning mode |
430+
| `Overflow` | Visible, Hidden | Content overflow handling |
431+
432+
## Margin Types
433+
434+
`MarginValue` -- readonly record struct representing a single margin side:
435+
- `MarginValue.Fixed(float px)` -- fixed pixel value
436+
- `MarginValue.Auto` -- auto margin that consumes free space
437+
- `ResolvedPixels` -- resolved value (0 for unresolved auto)
438+
439+
`MarginValues` -- readonly record struct for all four sides:
440+
- `Top`, `Right`, `Bottom`, `Left` -- individual `MarginValue` sides
441+
- `HasAuto` -- whether any side is auto
442+
- `MainAxisAutoCount(bool isColumn)` -- count of auto margins on main axis (0, 1, or 2)
443+
- `CrossAxisAutoCount(bool isColumn)` -- count of auto margins on cross axis (0, 1, or 2)
444+
- `MarginValues.Zero` -- all sides zero, no auto
445+
446+
`PaddingParser.ParseMargin(string, float, float)` -- parses CSS margin shorthand with auto support, returns `MarginValues`.
447+
367448
## Control Flow Elements
368449

369450
### Each Element (Iteration)

README.md

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@ The `receipt-dynamic.yaml` demonstrates AST-level control flow with `type: each`
6060
dotnet add package FlexRender
6161
```
6262

63+
> [!IMPORTANT]
64+
> **Linux / Docker users:** SkiaSharp requires native libraries. Add the native assets package to avoid `DllNotFoundException: libSkiaSharp`:
65+
> ```bash
66+
> dotnet add package SkiaSharp.NativeAssets.Linux
67+
> # For minimal containers without fontconfig/freetype:
68+
> dotnet add package SkiaSharp.NativeAssets.Linux.NoDependencies
69+
> ```
70+
6371
### Individual packages
6472
6573
| Package | Description |
@@ -81,18 +89,6 @@ dotnet add package FlexRender.Yaml
8189
dotnet add package FlexRender.Skia
8290
```
8391
84-
### Linux / Docker
85-
86-
SkiaSharp requires native libraries on Linux. If you get `DllNotFoundException: libSkiaSharp`, add the native assets package:
87-
88-
```bash
89-
# Standard Linux (requires system fontconfig/freetype)
90-
dotnet add package SkiaSharp.NativeAssets.Linux
91-
92-
# Minimal containers without system libs
93-
dotnet add package SkiaSharp.NativeAssets.Linux.NoDependencies
94-
```
95-
9692
### CLI tool
9793

9894
```bash
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Gap Analysis: FlexRender vs Yoga Layout Engine
2+
3+
## Container Properties
4+
5+
| Property | FlexRender | Yoga | Gap |
6+
|---|---|---|---|
7+
| `flex-direction: row` | YES | YES | -- |
8+
| `flex-direction: column` | YES | YES | -- |
9+
| `flex-direction: row-reverse` | NO | YES | MISSING |
10+
| `flex-direction: column-reverse` | NO | YES | MISSING |
11+
| `flex-wrap: nowrap` | YES (default, only mode) | YES | -- |
12+
| `flex-wrap: wrap` | ENUM ONLY (not implemented in layout) | YES | MISSING IMPL |
13+
| `flex-wrap: wrap-reverse` | ENUM ONLY (not implemented) | YES | MISSING IMPL |
14+
| `justify-content: start` | YES | YES | -- |
15+
| `justify-content: center` | YES | YES | -- |
16+
| `justify-content: end` | YES | YES | -- |
17+
| `justify-content: space-between` | YES | YES | -- |
18+
| `justify-content: space-around` | YES | YES | -- |
19+
| `justify-content: space-evenly` | YES | YES | -- |
20+
| `align-items: start` | YES | YES | -- |
21+
| `align-items: center` | YES | YES | -- |
22+
| `align-items: end` | YES | YES | -- |
23+
| `align-items: stretch` | YES | YES | -- |
24+
| `align-items: baseline` | ENUM ONLY (not implemented) | YES | MISSING IMPL |
25+
| `align-content: *` | ENUM ONLY (6 values, none implemented) | YES (7 values incl SpaceEvenly) | MISSING IMPL |
26+
| `gap` (uniform) | YES | YES | -- |
27+
| `row-gap` | NO (single gap only) | YES | MISSING |
28+
| `column-gap` | NO (single gap only) | YES | MISSING |
29+
30+
## Item Properties
31+
32+
| Property | FlexRender | Yoga | Gap |
33+
|---|---|---|---|
34+
| `flex-grow` | YES | YES | -- |
35+
| `flex-shrink` | YES (weighted, NOT scaled by basis) | YES (scaled by basis) | WRONG FORMULA |
36+
| `flex-basis` | PROP ONLY (not used in layout) | YES | MISSING IMPL |
37+
| `align-self` | PROP ONLY (not used in layout) | YES | MISSING IMPL |
38+
| `order` | PROP ONLY (not used in layout) | NO (Yoga does not support) | LOW PRIORITY |
39+
40+
## Sizing Properties
41+
42+
| Property | FlexRender | Yoga | Gap |
43+
|---|---|---|---|
44+
| `width` | YES (px, %, em, auto) | YES (px, %, auto) | -- |
45+
| `height` | YES (px, %, em, auto) | YES (px, %, auto) | -- |
46+
| `min-width` | NO | YES | MISSING |
47+
| `max-width` | NO | YES | MISSING |
48+
| `min-height` | NO | YES | MISSING |
49+
| `max-height` | NO | YES | MISSING |
50+
| `aspect-ratio` | NO | YES | MISSING |
51+
52+
## Spacing Properties
53+
54+
| Property | FlexRender | Yoga | Gap |
55+
|---|---|---|---|
56+
| `padding` (4-side) | YES (top/right/bottom/left) | YES | -- |
57+
| `margin` (uniform) | YES (single value) | YES (4-side + auto) | PARTIAL |
58+
| `margin` (4-side) | NO | YES | MISSING |
59+
| `margin: auto` | NO | YES | MISSING |
60+
| `border` (width affecting layout) | NO | YES | MISSING |
61+
62+
## Positioning Properties
63+
64+
| Property | FlexRender | Yoga | Gap |
65+
|---|---|---|---|
66+
| `position: relative` | NO | YES | MISSING |
67+
| `position: absolute` | NO | YES | MISSING |
68+
| `position: static` | implicit default | YES (default) | -- |
69+
| `top/right/bottom/left` | NO | YES | MISSING |
70+
71+
## Display & Overflow
72+
73+
| Property | FlexRender | Yoga | Gap |
74+
|---|---|---|---|
75+
| `display: flex` | YES (implicit) | YES | -- |
76+
| `display: none` | NO | YES | MISSING |
77+
| `overflow: visible` | default behavior | YES | -- |
78+
| `overflow: hidden` | NO | YES | MISSING |
79+
80+
## Direction
81+
82+
| Property | FlexRender | Yoga | Gap |
83+
|---|---|---|---|
84+
| `direction: LTR` | YES (implicit, only mode) | YES | -- |
85+
| `direction: RTL` | NO | YES | MISSING |
86+
87+
## Yoga Limitations (NOT in CSS spec)
88+
89+
- NO `order` property -- order always determined by document order
90+
- NO z-index
91+
- NO visibility (always visible)
92+
- NO forced breaks
93+
- Default flexDirection = Column (CSS = Row)
94+
- Default flexShrink = 0 (CSS = 1)
95+
- Default alignContent = FlexStart (CSS spec: Stretch)
96+
- Simplified two-pass flex resolution instead of variable-pass from spec

0 commit comments

Comments
 (0)