Skip to content
Ferrigura

Ferrigura / Architecture

0008 Canonical model and typed views

Scope: the data that flows from the parsers to the renderers: the diagram models in ferrigura-model, the configuration and the theme tokens, and how a renderer receives them. It does not cover the layout data that the unified renderer builds from a model, which follows the reference's getData() (0015).

Decision

Each diagram type has a canonical model. The model is ferrigura's own versioned JSON form of a parsed diagram, not a dump of Mermaid's diagram DB: it holds what the input says and leaves out what only rendering needs. The Rust structs serialize to it, and the harness's exporters turn the reference's DB into it, so the two can be compared on every input. Its conventions (text, sanitization, numbers, title precedence, and what each type keeps and leaves out) are in canonical-model.md.

The configuration is resolved once per level, at the level where it can change:

  1. per process: the defaults of the schema and of the reference's TypeScript, and the variables of each shipped theme, computed once and frozen;
  2. per renderer: the site configuration (Mermaid's initialize) merged onto the defaults, and the light and dark theme with its themeVariables, held by a Renderer (Renderer::new(config) in Rust, new Renderer(config) in Node.js); render(text, config) is a shorthand over a small cache keyed by the configuration;
  3. per diagram: only the diagram's own front matter and directives, merged on top when there are any.

The merge keeps the reference's semantics (assignWithDepth with its depth, the sanitizing of directives, the secure keys).

Renderers read a typed, read-only view. Each renderer takes a Context<'_, Model, Section> (ferrigura_render::context): its model, its own configuration section, the global options it reads (Globals: look, theme, font family and size, security level, layout, htmlLabels), the theme as typed tokens (ThemeView, with light, and dark for colorScheme: auto), and the id. A theme token is a field of ThemeTokens (ferrigura_model::theme::tokens), generated by one macro from the list of names the themes compute and the renderers read, so a token a renderer reads exists at compile time. Nothing below the facade writes to the configuration; what a renderer derives is its own.

Why

A model of its own lets Rust leave JavaScript's internals behind while every input stays comparable with the reference. Resolving the configuration at the level where it changes keeps a render cheap: rebuilding the defaults, merging the layers and computing both color schemes for every render was most of the time of a small render, and Mermaid pays the other extreme, a deep copy of its whole global configuration for every read. Typed views make a misspelled token a compile error instead of a silent undefined, and allow concurrent renders with different configurations.

Weighed and not chosen: Mermaid's structure, a global configuration with getConfig and setConfig, which rules out concurrent renders; and the whole typed configuration for every renderer, which makes every renderer depend on everything.

Consequences

  • The model version, CANONICAL_VERSION, changes when a field is added, removed or changes meaning; canonical-model.md describes the change before the code changes.
  • A renderer that needs another global option adds it to Globals. A test fails when a file below ferrigura-render/src names MermaidConfig outside its tests and the two constructors the facade calls.
  • Where the schema does not name what the reference reads (the treemap settings, packet's bitsPerRow), the section gets a hand-written type beside the generated ones, filled by the facade with the reference's arithmetic, wrong types included.
  • pnpm -C tools/port port:diff config and port:diff theme check the resolved configuration and the theme variables against the reference.

0007, 0011, 0015.