Phase 2: HSX-style HTML renderer with render-aware evaluation
S-expression AST → HTML string renderer with ~100 HTML tags, void elements, boolean attributes, XSS escaping, raw!, fragments, and components. Render-aware special forms (if, when, cond, let, map, etc.) handle HTML tags in control flow branches correctly by calling _render instead of _eval. 63 new tests (172 total across parser, evaluator, renderer). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -622,6 +622,38 @@ Each phase is independently deployable. The end state: a platform where the appl
|
||||
|
||||
**Source material ported from:** `artdag/core/artdag/sexp/parser.py` and `evaluator.py`. Stripped DAG-specific types (Binding), replaced Lambda dataclass with callable closure, added defcomp/Component, added web-oriented string primitives, added &key/&rest support in parser.
|
||||
|
||||
### Phase 2: HTML Renderer — COMPLETE
|
||||
|
||||
**Branch:** `sexpression`
|
||||
|
||||
**Delivered** (`shared/sexp/html.py`):
|
||||
- HSX-style renderer: s-expression AST → HTML string
|
||||
- ~100 HTML tags recognised (sections, headings, grouping, text, embedded, table, forms, interactive, template)
|
||||
- 14 void elements (br, img, input, meta, link, etc.) — no closing tag
|
||||
- 23 boolean attributes (disabled, checked, required, hidden, etc.)
|
||||
- Text and attribute escaping (XSS prevention: &, <, >, ")
|
||||
- `raw!` for trusted unescaped HTML
|
||||
- `<>` fragment rendering (no wrapper element)
|
||||
- Render-aware special forms: `if`, `when`, `cond`, `let`/`let*`, `begin`/`do`, `map`, `map-indexed`, `filter`, `for-each`, `define`, `defcomp` — these call `_render` on result branches so HTML tags inside control flow work correctly
|
||||
- `_render_component()` — render-aware component calling (vs evaluator's `_call_component` which only evaluates)
|
||||
- `_render_lambda_call()` — lambda bodies containing HTML tags are rendered directly
|
||||
- `_RawHTML` marker type — pre-rendered children pass through without double-escaping
|
||||
- Component children rendered to HTML string and wrapped as `_RawHTML` for safe embedding
|
||||
|
||||
**Key architectural decision:** The renderer maintains a parallel set of special form handlers (`_RENDER_FORMS`) that mirror the evaluator's special forms but call `_render` on results instead of `_eval`. This is necessary because the evaluator doesn't know about HTML tags — `_eval((p "Hello"))` fails with "Undefined symbol: p". The renderer intercepts these forms before they reach the evaluator.
|
||||
|
||||
**Dispatch order in `_render_list`:**
|
||||
1. `raw!` → unescaped HTML
|
||||
2. `<>` → fragment
|
||||
3. `_RENDER_FORMS` (checked before HTML_TAGS because `map` is both a render form and an HTML tag)
|
||||
4. `HTML_TAGS` → element rendering
|
||||
5. `~prefix` → component rendering
|
||||
6. Fallthrough → `_eval` then `_render`
|
||||
|
||||
**Tests** (`shared/sexp/tests/test_html.py`):
|
||||
- 63 tests: escaping (4), atoms (8), elements (6), attributes (8), boolean attrs (4), void elements (7), fragments (3), raw! (3), components (4), expressions with control flow (8), full pages (3), edge cases (5)
|
||||
- **172 total tests across all 3 files, all passing**
|
||||
|
||||
### Test Infrastructure — COMPLETE
|
||||
|
||||
**Delivered:**
|
||||
|
||||
Reference in New Issue
Block a user