Relocate codegen.rs + codegen_targets.rs out of openpencil-shell-core
into a dedicated op-codegen crate. The code generators only depend on
jian-ops-schema + op-editor-core, so they form a clean leaf crate.
codegen.rs becomes the crate's lib.rs; crate::codegen:: paths in
codegen_targets.rs collapse to crate::. Pure relocation, no behaviour
change; tests move with their code.
Two final pieces of the Variables/Themes data layer:
- `stroke_refs: BTreeMap<NodeId, String>` parallels `fill_refs`.
Paint reads `var_table.stroke_color_for(node.id)` first, falls
back to `node.stroke.color` otherwise. Both fields registered in
the canonical loader when it sees a `$ref` on the stroke
descriptor in `.op` files.
- `set_active_theme(axis, value)` + `clear_active_axis(axis)`
mutators give the future theme-picker widget a one-line entry
point. `clear_active_axis` removes the axis from the active
map so subsequent resolutions fall back to the variable's
`theme: None` default — matches TS theme-axis reset behavior.
Tests (2 new):
- `stroke_color_for_resolves_registered_ref` — register a Color
variable, register a stroke ref, resolve through the table,
verify blue channel matches `#0000ff`.
- `set_active_theme_round_trips_through_axis_picker` — flip an
axis, add a second, clear one, assert state matches.
#5 Variables/Themes now ~98% — only the panel widget UI is
pending. Plumbing for paint, mutation, loader integration, fill
and stroke ref resolution all working with structural tests.
3 existing codegen test fixtures patched to seed `stroke_refs:
BTreeMap::new()` alongside `fill_refs`.
Tests total: 241 shell-core (+2). Wasm32 build clean.
#5 Variables advances toward 90% with `VariableTable.fill_refs:
BTreeMap<NodeId, String>` — node-id → variable-name map for fills
that should resolve through the variable table instead of using
`node.fill` directly. Avoids touching `Node`'s shape (which would
invalidate every Node literal across test fixtures + builders) by
piggybacking on the same VariableTable the canonical loader fills.
API additions on `VariableTable`:
- `set_fill_ref(node_id, ref_name)` — register a node's fill ref
- `fill_for(node_id) -> Option<Color>` — resolve through the
current `active_theme`; falls back to None when no ref is
registered or the variable doesn't resolve. Canvas paint will
call this first, fall through to `node.fill` on None.
Side-derivation: `NodeId` now derives `PartialOrd + Ord` so it
can key into `BTreeMap`. The existing 3-codegen-test fixtures
gain a `fill_refs: BTreeMap::new()` initializer.
Tests (2 new):
- `fill_for_resolves_registered_node_ref_to_themed_color`:
register a Themed Color variable + a node ref, flip
`active_theme`, verify the resolved Color tracks the theme
- `fill_for_returns_none_when_no_ref_registered`: unknown
NodeId returns None so paint falls back to direct fill
Canvas-side `paint_node` integration (`let fill =
doc.var_table.fill_for(node.id).or(node.fill);`) lands when the
canvas refactor for that path arrives next.
Tests total: 229 shell-core (+2). Wasm32 build clean.
- `React` — JSX functional component named `Page` wrapping nodes in a
fragment; inline `style={{}}` objects with camelCase keys.
- `Flutter` — `Stack(children: [Positioned(left, top, child:
Container/Text)])`. Color emits as `Color.fromARGB`.
- `SwiftUI` — `ZStack { Rectangle()/Ellipse()/Text(...) .frame.position
}`. Color emits as `Color(red, green, blue, opacity)`.
All three reuse the established `Codegen` trait + walk
`doc.pages[active].children` the same way the HTML / Vue / Svelte
emitters do — `hidden` nodes are skipped, children recurse, text
bodies escape special chars (HTML targets) or use Dart/Swift
string-literal-escaping (`{:?}` debug-fmt) for compiled targets.
Tests (4 new):
- React: import statement + component declaration + JSX fragment
- Flutter: Stack wrapper + Positioned/Container shape + ARGB color
- SwiftUI: ZStack + Rectangle + .frame modifier
- SwiftUI ellipse: NodeKind::Ellipse → `Ellipse()` view
Remaining 2 generators (Compose, React Native) ship in a follow-up
commit — both follow the same trait-per-target shape with their
specific framework's geometry primitives.
Tests total: 218 shell-core (+4) + 20 native + 8 desktop. Wasm32
build clean.
Adds `Vue` and `Svelte` codegen targets alongside the existing
`CssVariables` and `Html`. Both reuse `emit_node_html` for markup
and embed the `CssVariables` generator's output verbatim in their
`<style>` block, so design tokens flow through into the framework
output as CSS custom properties.
- `Vue` — Vue 3 SFC: `<template>` (node markup) + `<script setup
lang="ts">` placeholder + `<style scoped>` (variables).
- `Svelte` — Svelte SFC: `<script lang="ts">` placeholder + bare
markup (no wrapping template tag, per Svelte convention) +
`<style>` (variables). Script-then-style order enforced by
test.
Tests (3 new):
- `vue_emits_template_script_style_blocks` — all three SFC
sections present
- `svelte_emits_script_then_markup_then_style` — script appears
before style in output (positional check)
- `vue_includes_variable_css_in_style_block` — variables flow
through into the scoped style block
5 generators remain (React + Tailwind, Flutter, SwiftUI, Compose,
React Native). The HTML-derivable group is done; the remaining 5
are framework-specific component models that need their own
emitters.
Tests total: 214 shell-core (+3) + 20 native + 8 desktop. Wasm32
build clean.
Adds `codegen::Html` alongside the existing `CssVariables` generator.
Walks `doc.pages[active].children` and emits absolute-positioned
`<div>` per Rect/Frame/Group + `<span>` per Text, with inline-style
position/size, RGB fill, stroke as border, corner radius (50% for
ellipses), and rotation transform. Body text is HTML-escaped.
API stays identical — both generators implement the same
`Codegen` trait, so a future CLI / Property-panel codegen
dispatcher fans out by trait object.
Tests (4 new):
- DOCTYPE + body wrapper + generator-attribution comment present
- Rect emits `<div>` with left/top/width/height + `rgb(r,g,b)` fill
- Text emits `<span>` and `&` / `<` / `>` in body get HTML-escaped
- `node.hidden = true` skipped entirely (no orphan markup)
7 generators remain to port (React + Tailwind, Vue, Svelte,
Flutter, SwiftUI, Compose, React Native) — each as a focused
follow-up commit. The trait + dispatcher shape doesn't change.
Tests total: 211 shell-core (+4) + 20 native + 8 desktop. Wasm32
build clean.
#10 on the TS-parity roadmap starts. User directive (memory
project_op_rust_gap_priority) is "codegen last"; CSS Variables is
the simplest and complements the #5 Variables/Themes work already
shipped this session — it emits whatever lands in
`doc.var_table` as a stylesheet without requiring the rest of the
node tree.
API:
- `crate::codegen::Codegen` trait — `target_label()` +
`generate(&Document) -> String`. Pure: no file I/O.
- `CssVariables` impl — walks `doc.var_table.variables` and emits
`:root { --name: value; }` for scalar entries, plus
`:root[data-axis="value"] { ... }` blocks for each themed
combination. CSS ident sanitisation maps non-alphanum chars to
`-` so `primary.color` → `--primary-color`.
Tests (4):
- emits scalars (`#0066ff`, `12`) under a `:root` block
- per-theme variables emit one block per axis combo, both light
and dark CSS variables present
- non-ident chars sanitised (`primary.color` → `primary-color`)
- empty doc emits only the generator header comment
Remaining 8 generators (React + Tailwind, HTML, Vue, Svelte,
Flutter, SwiftUI, Compose, React Native) ship in follow-up
commits; the `Codegen` trait means each is a focused new file.
Tests total: 198 shell-core (+4). Wasm32 build clean.