Skip to content

feat: add render.Options.theme to write the theme as literal values - #13

Open
deodad wants to merge 2 commits into
wevm:mainfrom
deodad:feat/resolved-colors
Open

deodad wants to merge 2 commits into
wevm:mainfrom
deodad:feat/resolved-colors

Conversation

@deodad

@deodad deodad commented Oct 31, 2026 •

Copy link
Copy Markdown

Motivation

Ondine's SVG reads every color, the stroke width, the radius, and the font from CSS custom properties, and chart series mix colors with color-mix(). Renderers without CSS variables draw such an SVG black or not at all: resvg, librsvg, the SVG parsers of native apps, and email clients. Each host now flattens the SVG with its own string pass.

Changes

  • Add the theme render option: an object of --ondine-* values in camel case (bg, fg, surfaceAlt, series1, radius, and so on). An empty object writes the defaults.
  • Add crates/compiler/src/svg/resolve.rs, a final pass that replaces each var() in attributes with the host value, the root alias, or its fallback, and drops custom-property declarations. Text content is not changed.
  • Evaluate color-mix(in srgb, …) to #rrggbb or #rrggbbaa (premultiplied, CSS Color 5 percentage rules). A mix reads hex, transparent, the 148 CSS named colors (sorted table, binary search), and rgb() or rgba() with comma or space syntax and an optional alpha.
  • If a mix reads a host value in another form, such as hsl() or oklch(), render throws a RenderError with the new code unsupported that names the key. Unmixed values, such as border: 'hsl(…)', are written as given.
  • Gate the pass and the option parser behind a new theme Cargo feature on compiler, forwarded by wasm. Only all.fast.generated.wasm (ondine/compiler, the CLI, and ondine/compiler/wasm/all.fast.wasm) and native builds enable it. The size-optimized per-type and all modules leave it out and return unsupported for theme.
  • The image Worker keeps the size all module and its own flattening, since it must fit beside resvg under the 3 MB Worker limit. A later change can move it to theme if that budget allows.
  • Keep the themed corner radius in the rx attribute when theme is set. Without theme, the output is byte-identical.
  • Document the option, the color forms, and the unsupported code on the render, create, and RenderError API pages and in the theming guide.

Example

import { render } from 'ondine/compiler'

// Writes the theme as literal values
const { svg } = render('pie\n  "A" : 3\n  "B" : 2', { theme: { fg: 'rebeccapurple' } })
// Before: fill="var(--ondine-series-2,color-mix(in srgb,var(--_fg) 57%,var(--_bg)))"
// After:  fill="#a88bc5"

// Throws RenderError (unsupported): The theme value for "fg" cannot be mixed.
render('pie\n  "A" : 3', { theme: { fg: 'hsl(0 0% 0%)' } })

Validation

cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings (also wasm32 with flowchart only and all,theme), cargo test --workspace, pnpm build:wasm, pnpm vp test --run --project node (808 passed), pnpm check, and pnpm check:types. On this machine both builds use the stable toolchain (the pinned nightly has no rust-src). Against the base commit, per-type modules are 14 to 46 B smaller raw and 6 to 150 B larger gzip (flowchart +30 B, pie +35 B). They are not byte-identical, because of the unsupported error path. all.fast.wasm grows 27,341 B raw and 10,365 B gzip (1,265,581 to 1,275,946 B).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant