# Skryb reference
This reference describes the source format accepted by the Skryb runtime.
Source is embedded in an HTML document and remains canonical after diagram
edits.
## HTML document shell
A Skryb document requires:
```html
<script src="https://sparkkz-nz.github.io/skryb/latest/skryb-runtime.js" defer></script>
<template id="source" type="text/markdown">
# Document title
</template>
<main id="rendered-document"></main>
```
Use a normal HTML document with UTF-8 encoding and a viewport meta tag. The
runtime reads only `template#source` and renders into `main#rendered-document`.
## Frontmatter
Frontmatter is optional and must be the first non-empty source content:
```yaml
---
theme: auto
colourScheme: ice
---
```
| Key | Values | Default |
| --- | --- | --- |
| `theme` | `auto`, `light`, `dark` | `auto` |
| `colourScheme` | `classic`, `fire`, `ice`, `midnight`, `paper` | `classic` |
| `doctype` | `document`, `diagram` | `document` |
The runtime reports malformed frontmatter and unsupported `theme`,
`colourScheme`, or `doctype` values. `auto` follows the viewer's system
preference. The selected colour scheme supplies light and dark variants for
document chrome, components, diagrams, edges, and markers. Changes made through
the document menu become canonical when the document is saved.
`doctype: diagram` opens the first diagram at full-window size. It is a
presentation default, not a separate format. Other Markdown still renders and
becomes visible when the diagram is collapsed. Editing, export, and save
behaviour is unchanged.
## Markdown compatibility
Skryb supports CommonMark-style document structure and the GFM additions listed
below. Unsupported syntax remains visible in the source and is not converted.
| Construct | Status | Notes |
| --- | --- | --- |
| Headings | Supported | Levels 1 through 6. |
| Paragraphs and thematic breaks | Supported | Use blank lines between paragraphs; `---`, `***`, and `___` create a thematic break. |
| Ordered and unordered lists | Supported | Nested lists are supported. Ordered lists retain a non-`1` start value. |
| Block quotes | Supported | Prefix consecutive lines with `>`. |
| Emphasis, strong text, and strikethrough | Supported | Use `*text*`, `**text**`, and `~~text~~`. |
| Inline code | Supported | Use backticks, such as `` `value` ``. |
| Links | Supported with safe URLs | Relative URLs, fragments, `http:`, `https:`, and `mailto:` are rendered. Other schemes, including `javascript:`, stay readable Markdown source. |
| Fenced code blocks | Supported | Three or more backticks open a block; the closing run must be at least as long, so a longer fence can contain a shorter one. The fence language produces a `language-<name>` class on `<code>`, and a recognised language is syntax highlighted. |
| Tables | Supported | Header rows, left/centre/right alignment, and escaped cell separators (`\|`) are supported. |
| Task lists | Supported | `- [ ]` and `- [x]` render as disabled checkboxes. Prose editing is not available. |
| Images | Supported with safe URLs | Relative images and safe `http:`, `https:`, or `data:image/(gif\|jpeg\|png\|webp);base64,...` sources render with their Markdown alt text. |
| Raw HTML | Intentionally literal | HTML is escaped and displayed as source; it is never executed or interpreted. |
| Other Markdown extensions | Intentionally literal | Unsupported input is displayed as source. |
`diagram` remains a skryb-specific fence rather than an ordinary code block.
All other fenced blocks, including a `text` block containing the word `diagram`,
render as code.
### Table example
```markdown
| Component | Owner | Status |
| :--- | :---: | ---: |
| API\|gateway | Platform | Ready |
```
The alignment separator row is required. Escape a literal pipe inside a cell
with a backslash.
## Formatting extensions
skryb adds a small, nested directive syntax for structured presentation without
raw HTML wrappers. Directive bodies contain ordinary supported Markdown,
including diagrams. The source remains readable when viewed without the
runtime.
```markdown
:::panel { title="Payment lifecycle" palette=accent }
This panel contains **ordinary Markdown**.
::: (payment lifecycle panel)
```
Open directives must begin in column 1 and use one of `section`, `panel`,
`callout`, `grid`, `stack`, `diagram`, or `toc`. An opening directive has the form
`:::name { key=value }`; braces are required when attributes are present.
Attribute values are bare non-space values or double-quoted strings.
Whether a directive holds content is a property of its name. The container
directives - `section`, `panel`, `callout`, `grid`, and `stack` - wrap content
and are closed with `:::` in column 1. Any text after whitespace is an ignored
closing annotation, so `::: (panel)` and `::: End panel` close exactly like
`:::`. Annotations are for human readability only and are not checked against
the opening directive name.
Void directives take **no closing fence**. `:::diagram` and `:::toc` are each a
single self-contained line. An extra closing `:::` after either directive is
ignored.
Directives nest. Invalid attributes, unknown directive names, unclosed
directives, and layout content that does not follow the rules below remain
visible source rather than being silently dropped or reinterpreted.
### Sections, panels, and callouts
`section` semantically groups related content. `panel` is a bordered,
padded visual container. Both may use:
| Attribute | Values / behaviour |
| --- | --- |
| `title` | Optional visible title. |
| `palette` | Optional semantic role: `background`, `pale`, `light`, `neutral`, `dark`, `accent-soft`, `accent`, `accent-strong`, `note`, `success`, `warning`, `danger`, `highlight`, or `none` (clears fill and stroke, leaving only text). |
| `fill`, `stroke`, `text` | Optional `#RGB`, `#RGBA`, `#RRGGBB`, or `#RRGGBBAA` overrides. These take precedence over a selected palette. |
Palettes use the document's `colourScheme` and resolved theme. Use structural
roles for ordinary content, accent roles for emphasis, and status roles for
meaningful exceptions. Without a palette or overrides, components inherit the
document style. Explicit `fill`, `stroke`, and `text` values override those
individual treatments.
`callout` is a specialised panel for a prominent message. It accepts the
attributes above plus optional `kind`: `note`, `info`, `warning`, or `success`
(`info` is the default). Callouts render a visible kind label and an accessible
label, so their meaning never relies on colour alone.
### Responsive grids and stacks
`grid` arranges direct child panels, callouts, or stacks in columns on larger
screens, and collapses to one column in source order on narrow screens. Its
required `columns` attribute supports only these intentional presets:
| Value | Layout |
| --- | --- |
| `2` | Two equal columns. |
| `3` | Three equal columns. |
| `"2fr 1fr"` | Two-thirds / one-third columns. |
| `"1fr 2fr"` | One-third / two-thirds columns. |
`fr` is a CSS Grid fractional unit: `2fr 1fr` divides available width into
three shares, assigning two to the first column and one to the second after
grid gaps are accounted for.
`stack` has no attributes and groups blocks vertically in one grid cell. Use it
for an asymmetric layout with a wide primary panel beside stacked supporting
content:
````markdown
:::grid { columns="2fr 1fr" }
:::panel { title="Architecture" palette=accent }
The primary explanation and diagram go here.
```diagram
type: flowchart
canvas: auto
nodes: []
edges: []
```
::: (architecture panel)
:::stack
:::panel { title="Decision" palette=highlight }
Keep canonical source readable and editable.
::: (decision panel)
:::callout { kind=warning title="Review" palette=warning }
Confirm assumptions before publishing.
::: (review callout)
::: (supporting stack)
::: (two-thirds / one-third grid)
````
Do not put ordinary Markdown directly inside a grid: wrap it in a panel,
callout, or stack. Arbitrary CSS grids, fixed widths, column spans, visual
reordering, and custom breakpoints are intentionally unsupported.
## Diagram fences
Use a `diagram` fenced block. Every diagram declares its `type`, which selects
its canonical YAML model:
```yaml
type: flowchart
version: 1
id: payment-flow
description: The customer submits a payment that the payments service records.
canvas: auto
nodes: []
edges: []
```
The parser accepts mappings, lists, and inline mappings such as
`{ x: 60, y: 100 }`. Indentation is significant: top-level sections start in
column 1, list entries use two spaces, item fields use four spaces, and nested
object fields use six spaces. Blank lines and `#` comments are allowed.
### Diagram references
Place a diagram where it should render with a reference directive, then define
its fenced diagram block elsewhere in the document with the same stable `id`.
This keeps longer diagram definitions out of the surrounding prose while
preserving one canonical source:
````markdown
:::diagram { id=payment-flow }
```diagram
id: payment-flow
type: flowchart
canvas: auto
nodes: []
edges: []
```
````
A referenced diagram must have exactly one matching definition. When a document
uses a diagram reference, every diagram fence in that document must declare an
`id`. `:::diagram` is a void directive, so it takes no closing `:::`.
For a large or detailed diagram, put the `:::diagram` reference beside the
explanatory prose and its fenced definition at the end of the document. The
reference determines the rendered position.
### Captions, anchors, and cross-references
A diagram's `id` is also its anchor, so `#payment-flow` links directly to it.
This applies with or without a caption, including a diagram placed through a
`:::diagram` reference.
When a diagram id matches a generated heading slug, the diagram retains the id
and the heading slug receives a numeric suffix.
A `caption` renders below the diagram, centred, as a `<figcaption>` inside the
diagram's own `<figure>`. It is hidden while the frame is expanded, since the
expanded frame is a working view for editing the diagram, not the document
view. Caption text renders through the inline Markdown subset, so `**bold**`
and `` `code` `` work but block content does not.
```yaml
type: flowchart
id: auth-flow
caption: "Figure #: Authentication flow"
```
`#` is replaced by the figure number. A caption without one is a title:
```yaml
caption: Authentication flow
```
**Only a caption containing the placeholder consumes a number.** Write `\#` for
a literal `#`.
Numbers follow **render order, not definition order**. A diagram referenced with
`:::diagram` is numbered at the reference, not at its fenced definition. A
diagram may be referenced at most once.
`{ref=auth-flow}` renders a link to the diagram, whose text is:
- **the figure number**, when the target's caption has a placeholder, so
`See Figure {ref=auth-flow}` renders as "See Figure 3";
- **the caption text**, when it has none, so `See {ref=auth-flow}` renders as
"See Authentication flow".
A reference to an unknown or uncaptioned id renders a visible error.
There is a single figure counter shared by every diagram type.
### Accessible descriptions
Give each diagram a concise plain-text `description` that identifies its purpose and primary relationship:
```yaml
description: The customer submits a payment that the payments service records.
```
When a caption is present, it names the SVG through `<title>` and `description` supplies the related `<desc>`. Without a caption, `description` is the SVG title and accessible name. Both forms are retained when saving a standalone SVG, printing it, or saving it as a Skryb diagram. A diagram without `description` keeps the compatible generic accessible label.
Keep the full explanation in nearby prose. `description` is a concise text alternative, not a transcript of every node and connector.
### Table of contents
`:::toc` builds a contents listing from the document's heading tree. It holds no
content, so like `:::diagram` it takes no closing fence:
```markdown
:::toc { depth=3 diagrams=true }
```
| Attribute | Values / behaviour |
| --- | --- |
| `depth` | Deepest heading level to list, `1` to `6`. Default `3`. |
| `diagrams` | `true` or `false` (default). Lists captioned diagrams alongside headings, nested under the heading they fall within. |
Only captioned diagrams are listed. Each entry uses the resolved caption and
appears in render order.
The directive may appear anywhere, including before the headings it lists.
### Diagram fields
| Field | Required | Description |
| --- | --- | --- |
| `type` | Yes | `flowchart` or `sequence`. |
| `version` | No | A document-defined diagram version, commonly `1`. |
| `id` | No | A document-defined diagram identifier. It is also the diagram's anchor and the target of `{ref=}`. |
| `caption` | No | Caption rendered below the diagram. A `#` in it is replaced by the figure number; `\#` is a literal `#`. |
| `description` | No | Concise plain-text summary of the diagram's purpose and primary relationship for screen readers and exported SVGs. Nearby prose should provide the full explanation. |
| `layout` | No | Flowchart only. Marks the diagram machine-managed: nodes may omit `position` and edges may omit anchors, and the engine fills in what is missing. |
| `relayout` | No | Flowchart only; requires `layout`. `all` replaces all positions, `unpinned` preserves positions marked `pinned: true`, and `autowrap` replaces all positions and wraps an eligible long linear flow. The field is consumed during serialization. |
| `styles` | No | Flowchart only. Named styles applied to nodes and edges with `class`. |
| `canvas` | No | Canvas mapping, or the scalar `auto` for a flowchart. Flowcharts support `width`, `height`, `auto`, and optional `grid`; sequences support `width`, `height`, `participantSpacing`, and `participantSize`. Omitted canvases default to `1000` by `560`. |
| `nodes` | Flowchart | List of flowchart nodes. |
| `edges` | Flowchart | List of flowchart connectors. |
| `participants` | Sequence | Ordered sequence participants. |
| `messages` | Sequence | Ordered sequence messages. |
| `activations` | Sequence | Optional participant activation ranges. |
| `notes` | Sequence | Optional notes anchored after a message. |
| `groups` | Sequence | Optional labelled ranges of messages. |
`canvas.width` and `canvas.height` are numeric SVG canvas dimensions. Prefer
`canvas: auto` on a flowchart and let them be derived; write them out only when
a fixed aspect ratio matters. A positive numeric `canvas.grid` enables snapping
while moving or resizing; omit it or use `0` to disable snapping.
Set `grid: 5` on a flowchart unless there is a reason not to. Snapping keeps
dragged nodes, resized nodes, waypoints, and callout targets on shared
coordinates, so edges meet anchors squarely and node edges line up instead of
missing each other by a pixel or two. A grid of `5` is fine enough to place
anything precisely while still aligning automatically; larger values align
more aggressively but make small adjustments coarse.
#### Derived canvas bounds
A flowchart canvas can derive its own size from its content:
```yaml
canvas: auto
```
or, keeping other canvas fields:
```yaml
canvas:
auto: true
grid: 5
```
A derived canvas is recomputed on every load as the content extent plus 40
units of padding. It grows or shrinks when the content changes. It serialises as
`auto`; computed `width` and `height` values are not written to the source.
Prefer `canvas: auto` when authoring. The canvas defines the SVG `viewBox`,
export and print bounds, and the diagram aspect ratio. It does not constrain node
positions. Set explicit `width` and `height` when a fixed aspect ratio is
required.
### Auto-layout
A flowchart can place its own nodes:
```yaml
type: flowchart
layout: right
canvas: auto
nodes:
- id: api
label: Payments API
shape: rounded-rectangle # no position: layout places it
- id: ledger
label: Customer ledger
shape: database
position: { x: 640, y: 240 } # already positioned: kept as-is
edges:
- source: api
target: ledger
sourceAnchor: right
targetAnchor: left
```
`layout` takes `right`, `down`, `left`, or `up`.
**`layout` determines whether a diagram is machine-managed.** When it is set,
nodes may omit `position` and edges may omit either anchor, and the engine
fills in whatever is missing. Without it, a node with no `position` or an edge
with no anchors is an error.
**Layout runs only when geometry is missing.** It fills in missing values when
the diagram loads and updates the model immediately. Serialisation writes the
current positions. Layout does not override existing positions; there is no
`layout: manual` mode or persistent layout state.
**Bake positions into the source before reviewing the diagram.** Before baking,
generated positions exist only in the open document. Use one of the browser
review routes to read the baked `template#source` and write it back to the
original file. Only fences declaring a `layout` are rewritten; a diagram
without one is copied unchanged, including its comments.
**Keep the `layout` key after baking.** It has no effect on a fully specified
diagram, but controls the placement of geometry omitted by later edits. Delete
the key only when no further automatic layout is required.
The rules:
- Existing `position` values are preserved.
- Explicit anchors are preserved. Only omitted anchors are derived.
- Containers are laid out recursively inside their parent. A container without
`size` expands to contain the result; an explicit `size` is preserved.
- Layout runs before canvas measurement, so `canvas: auto` fits the result.
- Layout is deterministic: the same source produces the same positions.
How a node is placed depends on what the diagram already says:
**No node has a position** - a fresh diagram. The whole graph is laid out in
*stages*: each node's stage is the longest path to it from the sources, and each
stage is drawn perpendicular to the flow, so `right` gives columns and `down`
gives rows. Nodes within a stage are then ordered to reduce edge crossings.
**Some nodes have positions** - existing positions are preserved. Explicit edge
anchors determine placement relative to connected nodes. For example, a
connector entering a new node at `left` places that node to the right of its
neighbour, centred with one stage gap between them. When an edge has no anchors,
the diagram direction determines placement. For multiple connectors, placement
clears every neighbour along the flow and is centred between them on the
perpendicular axis.
**A node with no connectors** - such as standalone text or a legend - takes the
first free, grid-snapped position that does not overlap existing nodes.
**Anchors left out are derived from where the nodes ended up**, once placement
has finished - not from the declared direction. Two nodes side by side face
`right` to `left`; a node above another faces `bottom` to `top`. A pair with no
geometry to read, a self connector or a node overlapping its neighbour, falls
back to the declared direction.
An edge whose anchors contradict the declared direction is treated as a
back-edge and excluded from stage assignment. If every edge contradicts the
direction, all edges participate in stage assignment. Cycles are handled the
same way.
Spacing defaults to a stage gap of 120 and a sibling gap of 60. Use the expanded
layout form to change these values:
```yaml
layout: { direction: right, stageGap: 120, siblingGap: 60 }
```
#### Constrained auto-layout geometry
Use automatic layout by default, but pin one or two nodes when their placement
communicates meaning - for example, an external actor at the left, a system
boundary, or a durable outcome. Leave the other nodes and ordinary connectors
without positions and anchors so the engine can arrange them around those
constraints.
Flowchart nodes default to `190` by `80`. Automatic layout uses a `120`-unit
stage gap and a `60`-unit sibling gap. When choosing a position for a pinned
node, leave room for the neighbouring node plus at least one stage gap in the
flow direction, and one sibling gap perpendicular to it. In a right-flowing
diagram, a default node immediately after a `190`-wide node begins about `310`
units to its right (`190 + 120`). With `canvas.grid: 5`, use multiples of `5`
for manually written positions and sizes.
These values are not limits. Set an explicit `size` when a label needs more
room. Set an anchor or waypoint only when required for a feedback edge, side
branch, or other specific route.
A `pinned: true` node must have an explicit `position`. Ordinary incremental
layout preserves every existing position. The pin distinguishes constraints
that `relayout: unpinned` must retain from previously generated positions.
#### One-shot relayout
Add one of these modifiers to a flowchart with `layout` when its baked geometry
should be regenerated on the next open or repository bake:
- `relayout: all` clears every node position and runs layout again;
- `relayout: unpinned` clears every position except those marked `pinned: true`
and places the remaining nodes around those constraints; or
- `relayout: autowrap` clears every position, runs layout again, and wraps an
eligible long linear flow into rows or columns.
Every form preserves node sizes, clears and regenerates every connector's
anchors, route, and waypoint, and removes `relayout` from the serialized source
while leaving the persistent `layout` setting in place.
The modifier is removed during serialisation, making subsequent opens
idempotent. A fully positioned diagram without `relayout` is unchanged. The
flowchart toolbar's **Relayout diagram** action applies `all` after confirmation.
```yaml
type: flowchart
layout: right
relayout: unpinned
canvas: { auto: true, grid: 5 }
nodes:
- id: boundary
label: System boundary
shape: rounded-rectangle
position: { x: 350, y: 180 }
pinned: true
- id: worker
label: Worker
shape: rounded-rectangle
edges:
- source: boundary
target: worker
```
#### Wrapping a long linear flow
After geometry has baked, lint diagnoses fitted content that remains a very long
strip. `unbalanced-aspect-ratio` requires at least eight nodes, a dominant path
of at least eight nodes covering 75% of the graph, and no more than 20% branching
nodes. Horizontal content warns at a 4:1 fitted ratio; vertical content warns at
5:1. Cycles, containers, small diagrams, branching trees, and wide disconnected
maps are not candidates. Canvas dimensions and empty canvas space do not affect
the diagnosis.
The warning does not change the layout. **Check document** provides a **Wrap
this … flow** action. Its confirmation shows the dimensions before and after
wrapping and states that the action replaces every authored position, anchor,
route, and waypoint. The result uses deterministic, grid-snapped rows or columns
with orthogonal transitions outside the occupied lines. It retains `layout:
right` or `layout: down`, and repeated application makes no further changes. A
fixed canvas expands only when required; an automatic canvas remains automatic.
A portable agent workflow must opt in explicitly. Open the document with
`?skryb=lint` and inspect `template[data-skryb-lint]`. Only when that report
contains `unbalanced-aspect-ratio`, reopen the original document with
`?skryb=autowrap`. The hosted runtime then wraps every eligible flow, writes the
updated canonical Markdown to `template#source`, and refreshes the lint report.
Extract the source through browser automation or Chromium DOM dumping and save
it over the original file. Running autowrap on the result again is a no-op; no
repository checkout or local CLI is required.
For example, author this eight-stage request path as a graph rather than
computing wrapped coordinates by hand. Open it once to bake and check it, then
use the suggested action if the resulting strip is difficult to read:
```yaml
type: flowchart
id: checkout-request
layout: right
canvas: { auto: true, grid: 5 }
nodes:
- id: browser
label: Browser
shape: rounded-rectangle
- id: gateway
label: API gateway
shape: rounded-rectangle
- id: auth
label: Authorisation
shape: rounded-rectangle
- id: checkout
label: Checkout API
shape: rounded-rectangle
- id: pricing
label: Pricing
shape: rounded-rectangle
- id: inventory
label: Inventory
shape: rounded-rectangle
- id: payment
label: Payment
shape: rounded-rectangle
- id: receipt
label: Receipt store
shape: database
edges:
- source: browser
target: gateway
- source: gateway
target: auth
- source: auth
target: checkout
- source: checkout
target: pricing
- source: pricing
target: inventory
- source: inventory
target: payment
- source: payment
target: receipt
```
### Named styles
Use a `styles:` block to define presentation shared by multiple nodes or edges:
```yaml
type: flowchart
styles:
external:
palette: neutral
style: { strokeWidth: 3 }
store:
palette: accent-strong
canvas: auto
nodes:
- id: bank
label: Partner bank
shape: rounded-rectangle
class: external
edges:
- source: bank
target: ledger
sourceAnchor: right
targetAnchor: left
class: external
```
A style definition must declare `palette`, `style`, or both.
Precedence is **theme, class, then element values**. Values written directly on
a node or edge override its class.
A class palette renders identically to the same palette written inline,
including gradients.
An edge has no palette of its own, so a class contributes only its `style`
values to an edge; a `palette` in that class is ignored there.
An undeclared class is an error. Named styles apply only to flowcharts;
sequence diagrams define `palette` and `style` on individual elements.
### Annotation badges
Annotation badges are available in the next runtime release, not the published
`latest` runtime or an existing pinned release. They identify elements for
explanation in adjacent prose. They do not navigate, resolve a destination, or
number themselves.
Flowchart nodes, flowchart edges, and sequence messages accept optional `ref`:
```yaml
ref: 3
```
```yaml
ref: Start
```
```yaml
ref: { label: 3, position: ne }
```
The scalar forms use position `NW`. In the mapping form, `label` is required
and `position` is optional. A label is a finite number or a nonblank,
single-line string without control characters. Labels render as plain text,
not Markdown or HTML. Strings are preserved without trimming; quote a
numeric-looking label such as `ref: "03"` to retain its leading zero.
The inspector trims surrounding whitespace when a label is edited.
Repeated labels are allowed. A mapping accepts only `label` and `position`; custom
badge styles are unsupported. Invalid labels, unknown fields, and unsupported
positions produce diagram validation errors reported by lint as `schema`.
Booleans, null, arrays, blank strings, and nonfinite numbers are not valid
labels. Omit `ref` to remove the badge.
One or two ASCII digits, such as `3` or `12`, produce a circle. All other
labels produce a stadium shape: a rectangle with semicircular ends. Badge width
fits the label. All badges use a solid blue treatment with contrasting text
selected for the document's colour scheme and light or dark theme. A node's
palette, class, or style does not customise its badge.
| Element | Position behaviour |
| --- | --- |
| Flowchart node, including a child | Uppercase `N`, `S`, `E`, `W`, `NE`, `NW`, `SE`, `SW` places the badge outside the node's bounding box with a gap. Lowercase `n`, `s`, `e`, `w`, `ne`, `nw`, `se`, `sw` places it inside with an inset. |
| Flowchart edge | The same directions place the badge outside the actual connector-label box in either case. Without a label, the reference point is the routed path's midpoint. Lowercase does not mean inside for connectors. |
| Sequence message | A fixed left gutter aligns the badge with the message's arrow row. A self-message has one badge on its outgoing row. A valid `position` is accepted but ignored; the message inspector has no badge-position control. |
Positions are case-sensitive; mixed-case forms such as `Ne` are invalid.
Directions use the bounding box, not the curved outline of a circle or diamond.
Badge bounds are included when fitting flowchart content and exporting the
diagram. This does not provide automatic obstacle avoidance. Leave space for
outside badges, and keep inside badges clear of node labels and children.
Inspect the rendered result after changing labels, sizes, or positions.
Lint reports `annotation-overflow` when an inside node badge extends beyond
its node bounds. `annotation-overlap` checks node badges against unrelated
nodes, excluding their host and all ancestors and descendants. Edge badges
are checked against every node, including connector endpoints. These checks do
not cover badge-to-badge or badge-to-label collisions, or sequence geometry.
Use `{annotation=3}` or `{annotation=Start}` in ordinary Markdown prose to show
the matching noninteractive badge:
```markdown
At {annotation=3}, the service records the accepted request.
{annotation=Start} marks the processing boundary.
```
Repeat the label explicitly; neither form automatically connects a badge to a
diagram element. This syntax is separate from `{ref=diagram-id}`, which remains
a link to a captioned diagram. A node's optional `href` also remains independent
of its annotation.
In the node, edge, or sequence-message inspector, use **Reference** to set or
clear the badge label. Flowchart inspectors also provide **Position**. The
edge inspector offers the uppercase outside positions; lowercase source values
have the same placement. Sequence-message inspectors omit that control because
their gutter position is fixed.
Omit a sequence message's `label`, use `label: ""`, or clear **Label** in the
message inspector to retain the arrow and badge without message text.
Baking, relayout, duplication, graphical edits, and serialization retain `ref`.
**Save As** and **Save for Offline** preserve diagram and inline badges.
Isolated SVG exports retain diagram badges as vector graphics. **Save as Skryb
diagram** retains the exported diagram's badges, but does not include omitted
explanatory prose. Export leaves the original document unchanged.
### Nodes
Every node requires `id` and `shape`; `label` must be present but may be empty.
Use `position` and `size` to control placement and geometry. Nodes may contain
child nodes at any depth:
```yaml
- id: payments-api
label: Payments API
subtitle: Owns payment intents
shape: oval
position: { x: 420, y: 210 }
size: { width: 220, height: 100 }
palette: accent
strokeType: dashed
style: { stroke: "#1D4ED8", strokeWidth: 3, fill: "#DBEAFE", text: "#17202A" }
children:
- id: idempotency-store
label: Idempotency store
shape: database
position: { x: 20, y: 120 }
size: { width: 160, height: 80 }
```
| Field | Values / behaviour |
| --- | --- |
| `id` | **Required.** Stable identifier used by edges. |
| `label` | **Required.** Node text; use `label: ""` for an unlabeled shape. A multiline label is written as a YAML literal block scalar (`label: \|+` followed by indented lines); a single-line double-quoted scalar with `\n`, for example `label: "Payments\nAPI"`, still parses for backward compatibility. |
| `subtitle` | Optional text below the label; multiline subtitles use the same literal block scalar (or legacy double-quoted `\n`) form. |
| `href` | Optional same-document destination, such as `"#payment-flow"` or `"#operating-notes"`. Must be a nonempty fragment string. See [Node navigation](#node-navigation); available in the next runtime release. |
| `ref` | Optional annotation badge: a string or finite number, or `{ label, position }`. See [Annotation badges](#annotation-badges); available in the next runtime release. |
| `textVAlign` | Optional vertical text-stack alignment: `top` or `center` (default). |
| `textHAlign` | Optional horizontal text-stack alignment: `left`, `center` (default), or `right`. |
| `class` | Optional name of a style declared in the diagram's `styles:` block. Node-level `palette` and `style` values take precedence. |
| `shape` | **Required.** `rounded-rectangle`, `circle`, `oval`, `database`, `diamond`, `rhombus`, `flattened-hexagon`, `chevron`, `right-chevron`, `document`, or `text`. The `document` shape is a sheet of paper with a folded top-right corner. The `text` shape is a plain text box: it renders its (multiline) `label` with a native-SVG Markdown subset, and its fill and stroke default to transparent unless a `palette` or `style` override is set. |
| `position` | **Required unless the diagram declares a `layout`.** `{ x: number, y: number }` top-left canvas position for top-level nodes, or top-left position relative to its parent for children. |
| `pinned` | Optional Boolean. `true` requires `position` and preserves that position during `relayout: unpinned`; `all` and `autowrap` replace it. |
| `size` | `{ width: number, height: number }`. Nodes have a minimum size; circles remain square. |
| `palette` | Optional semantic palette role; selects the scheme-aware node treatment and clears explicit node colour overrides. |
| `style` | Optional overrides: `fill`, `stroke`, `text`, and `strokeWidth`. `style.width` is rejected. |
| `strokeType` | `solid`, `dotted`, `dashed`, or `double`. Omit for `solid`. This applies to the node outline and detail lines. A double stroke uses two visibly separated rails. |
| `arrow` | Optional `{ x: number, y: number }` canvas coordinate. Draws a callout pointer from the node centre out to that point, in the node's own fill and stroke, so the pointer renders as part of the same shape as the node. Works on any shape. A node with no fill and no stroke (a plain `text` shape) draws the pointer in its text colour, starting at the node outline. |
| `children` | Optional list of child nodes. Any shape can contain children, nesting has no depth limit, and child positions are relative to their parent. |
Palette roles are `background`, `pale`, `light`, `neutral`, `dark`,
`accent-soft`, `accent`, `accent-strong`, `note`, `success`, `warning`,
`danger`, `highlight`, and `none`. The `none` role sets both fill and stroke
to `none` (no background, no border) while keeping readable text, and works
on any node shape; selecting any other palette restores that palette's normal
styling. A diagram always inherits the document-wide theme and colour scheme;
per-diagram scheme overrides are not supported.
#### Node navigation
Node navigation is available in the next runtime release. It is not yet part of
the published `latest` runtime or an existing pinned release.
A flowchart node can link to a diagram or heading in the same document:
```yaml
- id: processing
label: Processing details
shape: rounded-rectangle
href: "#processing-detail"
```
Use a diagram's `id` for `"#processing-detail"`, or a rendered heading slug
such as `"#operating-notes"` for `## Operating notes`. Quote the value: an
unquoted `#` starts a YAML comment. Both captioned and uncaptioned diagrams are
valid destinations. A `:::diagram` reference places the destination at its
reading position, not at the fenced definition. Heading slugs follow the
rendered document's collision rules; use the actual anchor when headings repeat
or share a name with a diagram. Directive titles and generated SVG element IDs
are not destinations.
Only nonempty same-document fragments are supported. External URLs, cross-file
paths, an empty string, and `"#"` are invalid. Unescaped whitespace, control
characters, backslashes, angle brackets, double quotes, and backticks are
rejected. Percent escapes must be valid UTF-8; the fragment is decoded once and
must produce a nonblank anchor with no control characters or browser
text-fragment directive (`:~:`). Percent-encoded internal spaces are allowed.
Omit `href` to remove a link.
The field applies to flowchart nodes, including child nodes, not edges or
sequence participants. A Markdown link inside a node label does not replace
`href`.
In read mode, linked nodes are native links with keyboard focus and Enter
activation. The accessible name uses the node label, then its subtitle if the
label is blank, then `Go to #destination` if both are blank.
A linked parent and a linked child have separate activation
targets; following the child does not also follow the parent. Unlinked nodes
keep their existing behaviour. Following a link closes diagram expansion and
browser fullscreen before revealing and focusing the destination. The fragment
URL supports direct opening and browser back/forward navigation.
In edit mode, a click selects the node instead of following its link. Use the
node inspector's **Destination** field to set, change, or clear `href`.
Navigation does not replace selection, dragging, resizing, panning, or zooming.
Baking, relayout, node duplication, graphical edits, and source serialization
retain the destination.
Build overview/detail documents with explicit return links in adjacent
Markdown. For example, place `[Back to overview](#overview-flow)` after a
detail diagram whose overview has `id: overview-flow`. No parent-diagram field
or automatic breadcrumb is required.
Malformed `href` values produce a `schema` error. A valid fragment with no
rendered heading or diagram target produces a `missing-node-destination`
warning; one matching multiple rendered anchors produces an
`ambiguous-node-destination` warning. These warnings identify the source node
and do not remove the link or choose a fallback target.
**Save As** and **Save for Offline** retain node destinations in the complete
document. Isolated SVG exports remove all node links. **Save as Skryb diagram**
retains only links to that exported diagram's own `id`; it removes links to
headings and other diagrams because their content is omitted. Export does not
change the original document.
#### Label wrapping and node width
Node labels and subtitles wrap on word boundaries inside the node's declared
width. Nodes are not resized to fit their text; authored widths are preserved.
Explicit line breaks in a label are always honoured. Wrapping applies only to a
line that does not fit. A single word wider than the line is not broken.
Choose a width that keeps labels to one or two lines. For approximate sizing,
allow **9px per label character** and **7px per subtitle character**. The label
is 16px semibold, the subtitle is 13px regular, and text is inset 12 units on
each side:
| Node width | Label characters per line | Subtitle characters per line |
| --- | --- | --- |
| 160 | ~15 | ~19 |
| 190 (default) | ~18 | ~23 |
| 220 | ~21 | ~28 |
| 260 | ~26 | ~33 |
Label line height is 20 and subtitle line height is 15. A default 80-unit-high
node fits two label lines. Text dominated by wide characters may require more
width than these averages indicate.
#### The `text` shape and its Markdown subset
The `text` shape is a borderless, unfilled rectangle intended for free-form
annotations. Author its content in the same `label` field as any other node;
each line is rendered with a small, native-SVG Markdown subset instead of
plain text:
- A line starting `# ` renders as a level-1 heading; `## ` renders as a
level-2 heading.
- Inline `**bold**`, `_italic_`, and `` `code` `` are supported within any
line, including heading lines.
- Line breaks are explicit: each line of the `label` becomes its own rendered
line, with no reflow or wrapping.
No other Markdown (links, lists, images, nested emphasis, HTML) is
recognised; unsupported syntax renders as literal text. This subset is
implemented with `<text>`/`<tspan>` elements only, so `text`-shape nodes
render identically inside the editor and in a standalone exported SVG file
(no `foreignObject` is used). A `text` shape's optional `subtitle` still
renders in the normal, unformatted subtitle style directly below the
formatted label content.
```yaml
- id: note
label: |+
# Summary
Retries use **exponential backoff** with `jitter`.
shape: text
position: { x: 40, y: 40 }
size: { width: 260, height: 100 }
```
Node IDs are unique across the whole diagram, including descendants. Edges can
connect to a parent or any child node. A child can visually extend beyond its
parent; its relationship is independent of its shape bounds.
### Edges
Every edge requires both explicit endpoint anchors:
```yaml
- source: web-app
target: payments-api
sourceAnchor: right
targetAnchor: left
label: |+
POST /payments
(idempotent)
route: orthogonal
strokeType: dashed
start: none
end: arrow
style: { stroke: "#52616B", strokeWidth: 2, text: "#3E4A54" }
```
| Field | Values / behaviour |
| --- | --- |
| `source`, `target` | IDs of the connected nodes. |
| `sourceAnchor`, `targetAnchor` | **Required unless the diagram declares a `layout`**, in which case either may be left out and is derived from where the nodes end up. `top`, `right`, `bottom`, or `left`. |
| `label` | Optional edge label; a multiline label is written as a YAML literal block scalar, and the legacy double-quoted `\n` form still parses. |
| `ref` | Optional annotation badge: a string or finite number, or `{ label, position }`. Both uppercase and lowercase positions stay outside the connector label. See [Annotation badges](#annotation-badges). |
| `route` | `orthogonal`, `straight`, or `curved`. Omit for the default orthogonal route. |
| `strokeType` | `solid`, `dotted`, `dashed`, or `double`. Omit for `solid`. A double stroke uses two visibly separated rails; endpoint markers remain solid for legibility. |
| `class` | Optional name of a style declared in the diagram's `styles:` block. Only its `style` values apply to an edge. |
| `waypoint` | Optional `{ x: number, y: number }` canvas coordinate. The flowchart editor exposes one draggable waypoint for a selected edge; it splits the route into two segments via that point, honouring the edge's `route`: orthogonal legs, a two-segment polyline for `straight`, and two smoothly joined cubic curves for `curved`. |
| `start`, `end` | `none`, `arrow`, or `circle`. Omit `start` for `none`; omit `end` for `arrow`. |
| `style` | Optional `stroke`, `strokeWidth`, and `text` overrides. `style.width` is rejected. |
Anchors resolve on the rendered shape perimeter. Endpoint markers follow the
edge stroke colour and maintain their own definitions, so one edge's styling
does not affect another. The default connector uses the colour scheme's neutral
mid-contrast fill while its label uses the normal document text colour, keeping
labels readable where they overlap a line.
Edge-label placement uses the rendered 15px text bounds and a 16-unit line
height. Candidate positions on both sides of the longest route segments are
tested in deterministic order against unrelated nodes, placed labels, and edge
routes. If all candidates overlap, the first candidate remains visible and lint
emits `edge-label-overlap`. For a long label on a short connector, use a literal
block scalar with a line break at a phrase boundary; labels are not wrapped
automatically. Resolve overlap by wrapping the label first, then moving only the
involved nodes by the smallest useful grid increment. Widen the full diagram
only if those changes are insufficient.
Prefer straight connector geometry unless a bend distinguishes a branch,
feedback path, or obstacle detour. After automatic layout, align node centres on
the dominant flow axis and place secondary branches perpendicular to it.
Retain `orthogonal` routing for aligned edges; it renders a single straight
segment when unobstructed and can still route around obstacles.
After automatic layout, inspect back-edges and feedback loops even when lint is
clean. An orthogonal return can overlap a forward connector without crossing a
node. First use separate anchors, such as `bottom`-to-`bottom` on a horizontal
flow. If the routes still overlap, use `curved`. Leave the waypoint unset unless
the default curve is ambiguous.
Use `orthogonal` for most flows. Use `curved` for a long back-edge, several
edges converging on one anchor, or an edge that would otherwise run along or
across an unrelated node.
#### Routing around obstacles
An edge that intersects a node other than its source or target is routed around
the node. Unobstructed routes are unchanged.
The routing cost includes distance and turns. An `orthogonal` edge adds segments
around an obstacle; a `straight` or `curved` edge uses an implicit waypoint and
retains its route type.
Automatic obstacle routing does not change:
- an edge with an explicit `waypoint`; or
- an edge for which no clear route can be calculated, such as a curve whose
anchor points directly towards an adjacent node. The authored path is retained
and lint reports the crossing.
A node containing, or contained by, an edge endpoint is not treated as an
obstacle for that edge.
### Sequence diagrams
Sequence diagrams use a separate model with deterministic layout. Participants
appear left to right in source order and messages appear top to bottom in source
order. They are edited through canonical source rather than the flowchart's
graphical editor.
```yaml
type: sequence
version: 1
id: payment-authorisation
description: The shopper requests authorisation and the payments API returns the result.
canvas:
participantSpacing: 220
participantSize: { width: 180, height: 42 }
participants:
- id: shopper
label: Shopper
kind: actor
- id: payments-api
label: Payments API
messages:
- from: shopper
to: payments-api
label: Authorise payment
style: solid
- from: payments-api
to: shopper
label: Approved
style: dashed
activations:
- participant: payments-api
from: 1
to: 2
notes:
- at: payments-api
after: 1
label: Idempotency key checked
groups:
- from: 1
to: 2
label: Payment flow
```
| Field | Required | Description |
| --- | --- | --- |
| `participants` | Yes | Ordered entries with unique `id`, visible `label`, optional `kind: actor`, and optional `palette`, `style`, or `size`. A multiline participant label is written as a YAML literal block scalar; the legacy double-quoted `\n` form still parses. Participant presentation also styles its activation bars. |
| `messages` | Yes | Ordered entries with existing `from` and `to` participant IDs, optional string `label`, optional `style: solid` or `dashed`, and optional annotation `ref`. Omit `label` or use `label: ""` for an unlabeled message. Message badges use a fixed left gutter; a valid `ref.position` is accepted but ignored. See [Annotation badges](#annotation-badges). |
| `activations` | No | Entries with `participant` and inclusive one-based `from`/`to` message positions. |
| `notes` | No | Entries with `at` participant ID, `after` message position, visible `label`, and optional `palette`, `style`, or `size`. Notes render above activation bars. |
| `groups` | No | Entries with inclusive one-based `from`/`to` message positions and visible `label`. |
For participants and notes, `palette` uses a semantic role, `style` supports
`fill`, `stroke`, `text`, and `strokeWidth`, and `size` supports positive
`width` and `height` values. The graphical sequence inspector can edit
participant, note, and message presentation, but structural changes remain
source-editor-only.
Sequence `canvas.participantSpacing` sets the horizontal distance between
participant lifelines (default `220`). `canvas.participantSize` sets the default
non-actor participant box `{ width, height }` (default `{ width: 180, height:
42 }`). A participant's `size.width` or `size.height` overrides the
corresponding default. The canvas expands horizontally as needed to retain the
configured lifeline spacing.
## Editing and serialization
Click-to-activate wheel controls and background double-click expansion are
available in the next runtime release, not published `latest` or an existing
pinned release. The currently published runtime captures wheel gestures on
hover without requiring activation.
The runtime provides per-diagram zoom, fit, pan, and edit controls. Click a
diagram, or Tab into it, to activate wheel controls. An outline marks the active
diagram. Until activated, the wheel scrolls the document even over a diagram.
Click or move keyboard focus outside it to deactivate it; activating another
diagram deactivates the previous one.
Over the active diagram, the wheel pans. Ctrl or Cmd with the wheel zooms
around the pointer. Shift with a wheel that reports only a vertical delta pans
horizontally. These gestures prevent page scrolling or browser zoom only while
the pointer is over the active diagram. Editable fields retain their native
wheel behaviour.
Panning is unbounded. **Zoom to fit** restores the full diagram to the frame.
Zoom ranges from one quarter to eight times the frame width.
Panning does not change diagram coordinates. In edit mode, authors can select
nodes and edges, edit supported
properties, drag nodes, resize nodes, duplicate or delete nodes, change
connector endpoints, and drag an edge's optional waypoint. A selected edge's
waypoint handle is a circle while the edge has no stored waypoint and a diamond
once one is anchored; the edge inspector's **Remove waypoint** button appears
only for an anchored waypoint and deletes it. The node inspector's **Add
pointer** / **Remove pointer** button toggles a callout pointer, whose target is
then draggable from the node's callout handle. Node and edge label
editing supports multiple lines: **Enter** adds a line, **Ctrl/Cmd+Enter**
commits, and **Escape** cancels.
Every retained edit serializes the diagram back into its matching `diagram`
fence in `template#source`. **Save As** downloads a complete HTML document
containing that updated source.
The document menu's **Edit source** action opens a resizable lower tray with the
complete canonical Markdown source. Drag the tray's top edge to resize it, or
focus that edge and use the arrow keys, Home, and End; double-click it to
restore the default height. Valid changes are rendered after a short
debounce and committed directly to `template#source`; the tray stays open and
keeps focus while the document updates. The runtime preserves the reader and
diagram scroll positions where possible.
The source tray menu can insert a valid flowchart, sequence diagram, diagram
reference, panel, or grid template at the cursor. **Import diagram…** reads a
diagram from another saved Skryb document or a plain Markdown file. If the file
contains multiple diagrams, it prompts for a selection. Imported diagrams are
validated. Conflicting `id` values are rewritten; duplicate diagram ids prevent
document rendering. **Help** opens this reference. Use the tray for
document structure and sequence-diagram structure; use the graphical inspectors
for flowchart presentation and connections, and sequence participant, note, and
message presentation.
If a draft has a frontmatter or diagram schema error, the last valid rendered
document and its canonical source remain unchanged. The tray retains the draft
and reports the precise error, so correcting it resumes live rendering. Saving
always uses the latest valid canonical source; an invalid draft cannot be saved
into a portable document. Closing a tray with an invalid draft asks whether to
discard that draft. Saving while an invalid draft is open asks whether to save
the last valid version instead.
Use Cmd/Ctrl+Shift+E to open or close the source tray; opening is suppressed
while focus is in another editable field, but the shortcut closes an open tray
even from its textarea. Escape closes an open document menu and collapses an
expanded diagram, but does not discard source text. Cmd/Ctrl+S downloads the
current valid source. Native textarea undo
and redo work normally and each undo or redo follows the same live-render path.
Closing the tray returns keyboard focus to the document-menu button.
Double-click rendered document text to open the source tray and select its first
matching canonical-source occurrence. Generated controls and text with no exact
source match are ignored. This navigation intentionally runs only from rendered
content to source; source text does not attempt to infer a rendered location.
The document menu also changes the theme, colour scheme, and doctype, which
writes frontmatter into the canonical source. **Save As** downloads a portable
HTML copy that retains an external runtime URL. **Save for Offline** downloads a
self-contained copy with the selected runtime embedded at the end of its body.
A hosted runtime is fetched for that export; the explicitly named
`skryb-runtime-self-packaged.js` artifact includes the same source for a local
`file:` workflow with no network access. The export reports an error rather than
producing a partial document if the runtime cannot be obtained.
Each diagram frame has an **Expand** control that fills the window. Double-click
empty diagram background to expand it, and double-click again to collapse it.
The same control or Escape also collapses it. Expansion activates wheel controls;
collapsing returns wheel scrolling to the document. These interactions do not
change the saved source. An expanded frame stops above an open source
tray, and its controls move to the document toolbar. Expanding and collapsing
fit the diagram to the new frame width without changing stored coordinates. A
collapsed frame returns to its previous height.
Each rendered diagram has an **Export** menu in its toolbar. **Open full
diagram** opens a standalone SVG in a new tab without editor controls. **Save
as Skryb diagram** downloads that one diagram as its own editable Skryb
document with `doctype: diagram` set, which the source tray's **Import
diagram…** can read back into any other document. **Save
as SVG** downloads that same vector image; it can be opened directly in a
browser or a compatible graphics application. **Print / Save as PDF** opens the
browser print dialog for the diagram alone. All three SVG actions preserve the
diagram's current theme background and system sans-serif font stack. The
standalone export never includes editing controls, current zoom, or pan state.
## Syntax highlighting
A fenced block with a recognised language is syntax highlighted. Highlighting
does not alter the code. Blocks with unrecognised languages render as plain
text.
Recognised languages, by the names that select them:
| Family | Names |
| --- | --- |
| C-like | `javascript`, `js`, `jsx`, `mjs`, `cjs`, `typescript`, `ts`, `tsx`, `java`, `kotlin`, `kt`, `swift`, `scala`, `go`, `golang`, `rust`, `rs`, `c`, `cpp`, `c++`, `cs`, `csharp`, `php`, `dart` |
| Python | `python`, `py` |
| Ruby | `ruby`, `rb` |
| JSON | `json`, `jsonc` |
| YAML | `yaml`, `yml` |
| SQL | `sql`, `postgresql`, `mysql` |
| Shell | `bash`, `sh`, `shell`, `zsh`, `console`, `terminal` |
| Markup | `html`, `xml`, `svg`, `vue` |
| CSS | `css`, `scss`, `less` |
| Diff | `diff`, `patch` |
| INI | `ini`, `toml`, `conf` |
Names are matched case-insensitively. Tokens are wrapped in
`<span class="docdiagram-token-...">` with one of `comment`, `string`, `number`,
`keyword`, `literal`, `type`, `tag`, `attribute`, `meta`, `inserted`, or
`deleted`, and coloured with theme-aware custom properties. The underlying code
text remains readable without these colours.
The highlighter is a tokeniser, not a parser. It recognises comments, strings,
numbers, keywords, and limited markup structure. C-like languages share a
keyword list, so some keywords may be highlighted in languages that do not use
them.
## Baking and checking a document
When a document opens, the runtime lays out incomplete diagrams, writes the
result to the source, runs the checks, and publishes the report. Baked source
changes are unsaved changes and prompt the reader to save. Publishing the report
does not.
The results are available in these elements:
| Element | Contents |
| --- | --- |
| `template#source` | The document's Markdown, with diagram geometry baked in. |
| `template[data-skryb-lint]` | A JSON report: `errors`, `warnings`, `sourceHash`, and `messages`. |
The report uses an attribute selector. IDs are reserved for document anchors. Both elements are HTML-escaped inside the file; decode entities when
reading them.
`sourceHash` is the eight-character FNV-1a digest of the exact UTF-16 source the
report describes. Recompute it before following locations: if it does not match
the source you are holding, the report predates an edit and every range must be
treated as stale. Locations use one-based `line` and `column` values and
zero-based UTF-16 `offset` values. Ranges are start-inclusive and end-exclusive,
matching browser text selection APIs.
The report is written after a bake, when the URL contains `?skryb=lint` (or
legacy `?skryb-lint`) or `?skryb=autowrap`, or when a reader selects **Check
document**. Writing or replacing this derived metadata does not mark the
document as changed or cause a save prompt. Existing source changes remain.
**Save As** includes the current report.
### What baking touches
Only a fence that declares `layout` and has a missing position or anchor is
rewritten in canonical form. Canonicalisation reorders fields and removes
comments inside that fence. Complete fences and fences without `layout` remain
unchanged. Content outside diagram fences and line endings are preserved.
Baking is idempotent. A diagram parse error fails the bake.
### The rules
Each message carries a `severity` (`error` or `warning`), a `rule`, a `message`,
and the compatibility `diagram` display name. Geometry messages also carry a
`location` with the nullable canonical `diagramId`, zero-based `diagramIndex`,
the complete `fenceRange`, and `subjects`. A subject is either a node with its
`id`, or an edge with its zero-based `index`, `source`, and `target`; when known,
its `sourceRange` selects the defining YAML line. Diagrams without ids remain
addressable by `diagramIndex`, and a rendered `:::diagram` reference points back
to the original fence rather than the reference directive.
The rules are `schema`, `unknown-edge-endpoint`, `missing-node-destination`,
`ambiguous-node-destination`, `node-overlap`,
`edge-crosses-node`, `edge-label-overlap`, `label-overflow`,
`annotation-overflow`, `annotation-overlap`, and
`unbalanced-aspect-ratio`. The last rule may carry a `suggestedAction` with the
`wrap-linear-flow` id and zero-based diagram index; lint itself never executes
it. `schema` and `unknown-edge-endpoint` are errors; the remaining rules are
warnings. Node-destination warnings report unresolved or ambiguous links rather
than geometry. Only errors are blocking. Message text remains stable for
consumers that do not use the structured location.
The repository CLI prints navigable `file:line:column` prefixes where a location
is available:
```sh
npm run lint -- document.md
npm run lint -- document.md --json
npm run lint -- document.md --fix-balanced
```
`--json` emits aggregate `errors` and `warnings` plus a `documents` array. Each
document has `file`, `sourceHash`, counts, and the same structured `messages` as
the browser template. `--errors` may be combined with either output format.
A node with no connector is never reported. A `text` shape used for annotation, a
label, or a legend is a normal part of a diagram.
Lint rules and diagram geometry are implemented in the same runtime.
## Printing a document
The document menu's **Print / Save as PDF** action and the browser print command
use the same print stylesheet.
Print behaviour:
- The document toolbar, source tray, diagram toolbars, and inspectors are not
printed.
- Diagram frames expand to the diagram height and reset zoom and pan.
- Panels, callouts, diagrams, tables, code blocks, and blockquotes are not split
across page boundaries. Headings are kept with following content.
- A `:::grid` changes to one column.
- Palettes and syntax highlighting retain their colours.
The menu action first collapses expanded diagrams, closes editors, and resets
stored frame heights and zoom.
A single diagram can still be printed on its own from its **Export** menu.
## Runtime channels and limitations
Use `/latest/skryb-runtime.js` for normal use, `/dev/skryb-runtime.js` only for
short-lived branch testing, and `/releases/<tag>/skryb-runtime.js` for immutable
published documents. These hosted artifacts omit the encoded source copy. Use
`skryb-runtime-self-packaged.js` only when distributing the runtime beside a
local document that must export offline without fetching. See the
[quickstart](https://sparkkz-nz.github.io/skryb/docs/quickstart.html) for URLs and
examples.
Current limitations include the intentionally small Markdown subset, flowcharts
and sequence diagrams, and no Mermaid import.