# Lini — Language Specification
Pretty diagrams, charts, and technical drawings from plain text, with fine-grained
control. One core — composable nodes, a CSS-driven cascade, compile-time layout —
drives a family of layouts (flow, grid, sequence, charts, and engineering drawings),
and compiles to clean, themeable SVG.
**Two brackets and one capsule carry the whole language.** `{ … }` is **style** —
`key: value;` declarations, dash-case, space-separated, exactly like CSS. `[ … ]`
is **content** — a node's children, in source order. `|…|` is **identity** — a
node's type and id. A node is `|type#id| "label" .class { style } [ children ]`;
every part but the bars is optional. Nothing styles outside a `{ }`; nothing is
drawn outside the canvas.
**Two node kinds, like HTML.** A **box** is a drawn node (`|block|`, `|box|`,
`|oval|`, `|group|`, …) and may hold children; a **string** is text *content*
inside or beside one. `"…"` is text, exactly as it sits inside an element on a web
page — stylable in place (`"x" { color: red }`), but a leaf, never a box.
This document is complete: an implementer can build a conforming engine from it
alone. **Everything is defined once and reused** — a property, the cascade, colour,
the expression engine all apply across every node and every layout, and a layout
section states only what is *new* to that layout. **Charts, sequences, and drawings
are layouts** ([Part II](#part-ii--layout)), peers of flow and grid over the same
core. **Link routing** has its own contract — [ROUTING.md](ROUTING.md).
---
## Table of Contents
### Part I — Core
1 [Mental Model](#1-mental-model) · 2 [Lexical Syntax](#2-lexical-syntax) ·
3 [Statements & the Label](#3-statements--the-label) ·
4 [Selectors, Cascade & Specificity](#4-selectors-cascade--specificity) ·
5 [The Box Model](#5-the-box-model) · 6 [Paint, Stroke & Text](#6-paint-stroke--text) ·
7 [Nodes](#7-nodes) · 8 [Templates](#8-templates) · 9 [Links](#9-links) ·
10 [Colour, Variables & Expressions](#10-colour-variables--expressions)
### Part II — Layout
11 [The Layout Model](#11-the-layout-model) · 12 [Flow & Grid](#12-flow--grid) ·
13 [Sequence](#13-sequence) · 14 [Charts](#14-charts) · 15 [Drawing](#15-drawing)
### Part III — Reference
16 [Property Ledger & Support](#16-property-ledger--support) · 17 [SVG Output](#17-svg-output) ·
18 [Compile Pipeline](#18-compile-pipeline) · 19 [CLI](#19-cli) · 20 [Errors](#20-errors) ·
21 [Grammar](#21-grammar) · 22 [Reserved Words](#22-reserved-words) ·
23 [Deferred](#23-deferred) · 24 [Examples](#24-examples)
---
## Quickstart
```
cat -> dog -> bird
```
That's a complete diagram: three boxes, two links. Lini fills in the rest.
| `\|type#id\|` | **Identity** — a type, an optional `#id`. Always in bars: an **instance** (`\|oval#cat\|`), a **rule** (`\|oval\| { … }`), a **define** (`\|cat::oval\| { … }`). |
| `"…"` | The **label** — what the node is called, placed by its type (text, a caption, a symbol, a chart title). |
| `{ … }` | A **style block** — `key: value;` declarations. |
| `[ … ]` | A **content list** — a node's children. |
| `.name` | A **class** — define it (`.hot { … }`), wear it (`\|box\| .hot`). |
| `#name` | An **id** — declare it (`\|box#cat\|`), select it (`#cat { … }`), reference it bare (`cat -> b`). |
| `--name` | A themeable **variable** (`fill: --accent`). |
| `a -> b` | A **link**. |
Three defaults make small diagrams trivial:
- Omit the type → `|box|` (a rounded, framed card); `|#cat|` is a default box.
- Omit the label → the box is empty.
- Name an undeclared id in a link → it's auto-created as a labelled `|box|` (`cat -> dog` adds `|box#cat| "cat"`).
**A file is a stylesheet, then drawn statements.** The stylesheet is one `{ }` block at the
top — setup that draws nothing. After it come the instances and links, in source order
(usually instances first, then links — a `layout: sequence` reads the order as time, [SPEC 13](#13-sequence)):
```
{ // the stylesheet — setup only
layout: grid; columns: repeat(3); gap: 30; // scene config
}
server -> client "requests" // a link, with a label
```
---
# Part I — Core
The language every node and every layout shares. Read top-to-bottom once; the layout
sections ([Part II](#part-ii--layout)) and the reference ([Part III](#part-iii--reference))
build on it and never restate it.
---
## 1. Mental Model
A Lini file is the body of an implicit **root** container: a **stylesheet** of setup
first, then the drawn **canvas** instances and **links** in source order — and every
statement is exactly one of the three:
| **stylesheet** | one `{ }` block: scene config (incl. link & routing defaults), rules, classes, defines | no — it styles |
| **canvas** | instances — boxes (`\|type#id\|`) and text (`"…"`) | yes |
| **links** | `a -> b` connections | yes |
The "is this drawn or styled?" question never arises: **styling lives in the
stylesheet block; drawing lives on the canvas.**
**One character tells a statement's kind.** A leading `|` opens a node, a `"`
opens text, a bare name opens a link, and inside the stylesheet a `.`/`#`/`|…|`
opens a rule. There is no prescan, no ambiguity.
**Two brackets and one capsule, one meaning each.**
- `|…|` — **identity**: a type and an optional `#id`. The *only* place a type
lives — on an instance (`|box#cat|`), a rule (`|box| { }`), or a define
(`|treat::box| { }`).
- `{ … }` — **style**: `key: value;` declarations. The *only* place styling lives.
- `[ … ]` — **content**: a node's children (boxes and text) and its internal
links, in source order.
A drawn node is `|type#id| "label" .class { style } [ children ]`. Only the bars
are required; everything after is optional. A link is the same tail on a different
head: `a -> b "label" .class { style } [ labels ]`.
**Three sigils, one meaning each.**
- `|…|` — a **type** (with an optional `#id`). Always in bars.
- `.name` — a **class**: a worn style bundle. Defined `.hot { … }`, worn after the
identity (`|box| .hot`, `a -> b .hot`) — never inside the bars.
- `#name` — an **id**: a node's unique name. Declared in the bars (`|box#cat|`),
selected as a rule (`#cat { … }`), referenced **bare** in a link (`cat -> b`).
A name goes **bare only when referenced**, and the one thing you reference is an
**id** (you link to it). Types and classes are never linked, so they are always
sigil-marked.
**Boxes and text.** A *box* has a type, an id, classes, a style block, and
children. A *string* is text content — no identity or children, but it **may carry
a style block** (`"x" { color: red; translate: 0 -6 }`). A string in a box's `[ ]`
(or trailing the head as its label) is that box's text; a string on its own is a
free-standing text node. Text is a leaf: to give it children, a border, padding, a
`pin`, or a wirable id, put it in a box (a `|block|` is the minimal one) — exactly
like wrapping a web page's text in an element.
**The file is the root container.** The stylesheet `{ }` is the root's own setup
block; the canvas instances are its children (written bare — the file *is* its
`[ ]`); the links are its internal links. Scene properties (`layout`, `gap`,
`padding`, `fill`, `font-size`, `clearance`, `routing`, …) sit in that block, alongside
rules like `|-| { stroke: … }` for link look; inheritable ones (`font-*`, `color`,
`clearance`, `routing`) cascade to every node and link.
**Render order is source order; the cascade is whole-file.** Instances draw in the
order written (later on top, pinned children above the flow; `layer:` overrides),
and every rule applies to every instance. Links are the one thing that needs no
declaration: naming an id declared nowhere auto-creates it ([SPEC 3](#3-statements--the-label)).
**Two kinds of variable.**
- *Visual* values that don't affect layout — colours and the font family — are
exposed as live CSS variables (`--lini-fill`, `--lini-accent`, …) so a host page
can re-theme them, and each colour carries a built-in dark variant that follows
the viewer's OS or a `data-theme` toggle ([SPEC 10](#10-colour-variables--expressions)).
- *Layout* values — sizes, gaps, paddings, widths, **and font size** — bake into
the SVG as literals. Text is measured at compile time, so its size can never be a
runtime `var()`; a standalone SVG always looks right.
---
## 2. Lexical Syntax
| Extension | `.lini` |
| Encoding | UTF-8 (BOM ignored) |
| Line endings | LF or CRLF (normalized on read) |
| Comments | `// …` to end of line. No block comments. |
| Statement end | A node/link/text statement ends at a newline or `;`. A **declaration** ends at `;` — its value runs to that `;` (or a closing `}`), so a value may span lines. |
| Identifier | `[a-zA-Z_][a-zA-Z0-9_-]*` — case-sensitive, ASCII, dash-case |
Whitespace is insignificant except as a token separator and where a rule below
says otherwise:
| `\|…\|` | Identity in bars: a type, an optional `#id` (`\|box#cat\|`), or an id alone (`\|#cat\|`). `::` is the define operator (`\|cat::oval\|`). Bars are paired; surrounding space at the boundary is not allowed. |
| `#id` | Inside the bars it names the node's id; at a rule's head it is an **id selector** (`#cat { }`). A `#` followed by hex digits in a *value* is a colour (`#f80`); the two never meet — one heads a statement or sits in bars, the other is a value. |
| `key: value` | `:` separates name and value; surrounding space optional, canonical is one space after (`radius: 5`). |
| `a:side` | A `:` after a link endpoint forces a side (`a:left`). Distinct from the declaration `:` by position — it follows an endpoint, never opens a value. |
| `.name` (class) | At a rule head it is a class **selector** / definition (`.hot { … }`). On an instance or link it is a **worn class**, following the identity — **spaced** off it (`\|box\| .hot`, `a -> b .loud`), the rest of the chain **glued** (`.hot.loud`). |
| `id.child` | **No space** — an endpoint path into a child (`kitchen.bowl`). |
| `--name` | A variable, in a value or at a statement start to declare one. |
| link op | `[marker?] line [marker?]`, glued, no internal space (`->`, `--->`, `<->`). |
| `[ … ]` | A content list. Paired; whitespace inside is insignificant. |
**Strings** — double-quoted UTF-8: `"…"`. Escapes: `\"`, `\\`, `\n`, `\t`. A
double-quoted string is always text; leading and trailing whitespace in its value is
**trimmed** (`" ABC "` is "ABC", and a spaces-only `" "` becomes `""`), so source
spacing never leaks into the render.
Single quotes are **not** strings (reserved, [SPEC 22](#22-reserved-words)).
**A bare word is an identifier, never a string.** In a value, an unquoted word is
always an identifier — a keyword, a colour or `symbol` name, a `font-family`, or an id
reference — so literal **text** is always quoted: a string-valued property (`title`,
`href`, `src`, `path`) takes a `"…"` even with no spaces. The one hybrid is a name that
may contain spaces — `font-family` — bare or quoted, quoted only when needed
(`font-family: "SF Mono"`), as in CSS. Numbers and `(…)` expressions are bare too;
only text is quoted.
**Expressions** — a parenthesized region `(…)` is a **compile-time math expression**:
operators and the math library, folded to a literal number (or a point) at compile
time. Parentheses are the **only place operators appear** — outside them `-` is a link
line or a sign and `<` / `>` are markers. A call's own parens count (`up(5 * r, 10)`);
groups may span lines ([SPEC 10](#10-colour-variables--expressions)).
**Numbers** — integer or decimal, optional sign, no units (px for lengths, degrees
for angles, 0–1 for opacities/fractions). `10`, `-5`, `0.25`, `+3`. A trailing `%`
makes a **percentage** (`50%`), valid only in colour components.
**Values are space-separated and positional**, like CSS: `padding: 5 2 5 5`,
`shadow: 2 2 4 #0003`, `translate: 10 -4`, `columns: 80 140 80`. A **comma**
separates list items and appears only where a property takes a list of groups
(`points: 0 0, 10 10`). **Functions** use parentheses and sit in value position —
`rgb(…)`, `hsl(…)`, `repeat(…)`, the math library, and any you bind
([SPEC 10](#10-colour-variables--expressions)). A call's `(` **glues to its name**
(`rgb(…)`, never `rgb (…)`); a free-standing `(…)` is a math group, and a free-standing
`(-)`, `(o)`, or `(<)` a measuring op ([SPEC 15.6](#156-dimensions)) — which is how
`move(-2, 5)`, `(8 * 2)`, and `pin (o)` never meet.
**Colors** — `#fff`, `#f80c`, `#ffaa00`, `#ffaa00cc` (3/4/6/8 hex digits; the 4-
and 8-digit forms carry alpha), CSS names (`red`, `cornflowerblue`), `rgb(…)`,
`rgba(…)`, `hsl(…)`, `hsla(…)` (percentages allowed — `hsl(200, 50%, 50%)`),
`oklch(L, C, H[, A])` (the palette's own space — L/A in 0–1, C the chroma, H in
degrees; folded to a hex at compile time, so it renders in every target), a
`--name` variable reference, or `none`. Out-of-range channels are an error. Beyond
a flat colour, a **paint** (`fill` / `stroke` / `gap-fill`) may be a **gradient** —
`gradient(…)`, `linear-gradient(…)`, or `radial-gradient(…)` — reached, like the
built-in hue palette, through the colour system ([SPEC 10](#10-colour-variables--expressions)).
---
## 3. Statements & the Label
A file is a **stylesheet, then drawn statements in source order** ([SPEC 1](#1-mental-model)), and
a container's body nests the same idea: a `{ }` style block, then a `[ ]` of children and
internal links.
### The stylesheet
One `{ }` block at the very top of the file — optional, omitted when there is
nothing to set up. Unlike an ordinary style block (declarations only), it is the
root's setup block, so it additionally holds the file-global definitions:
| Scene config | `layout: grid;` `routing: orthogonal;` | a declaration on the root — `clearance` / `routing` cascade to every link ([SPEC 9](#9-links)) |
| Variable | `--brand: #f60;` | a themeable visual variable (colour / font) |
| Binding | `my_r = 5;` `scale(n) = (…)` | a compile-time value / function, bound with `=` — read in any expression ([SPEC 10](#10-colour-variables--expressions)) |
| Rule | `\|box\| { … }` | style every box (an element selector) |
| Link rule | `\|-\| { stroke: #666; }` | style every link — the `\|-\|` selector ([SPEC 9](#9-links)) |
| Descendant rule | `\|table\| \|box\| { … }` | style every box inside a table |
| Class | `.hot { … }` | define class `hot` |
| Id rule | `#hero { … }` | style the one node with id `hero` |
| Define | `\|treat::box\| { … }` | a new type `treat`, base `box`, with its defaults |
```
{
gap: 16; fill: --bg;
--brand: #ff6600;
scale(n) = (100 * 1.2^n);
.hot { stroke-width: 2; }
|treat::box| { radius: 5; }
}
```
`|treat::box|` reads "treat **is a** box"; the `::` sets a define apart from a
plain reference (`|box|`) at a glance. Defines chain (`|panel::treat|`) and may
carry intrinsic children ([SPEC 9](#9-links)). Max inheritance depth 16; cycles are an
error.
### Node declaration
```
The **bars are identity** — a type and an optional `#id`. The **`"label"`** is the
node's name; the **`.class`es** are worn styling; the **`{ }`** is style; the
**`[ ]`** is content. Only the bars are required; at least a type or an `#id` must
sit inside them.
A node's **type and id live in the bars**, its **classes follow** them:
`|oval#cat|`, `|box| .hot` (a box with class `hot`), `|box| .hot.loud` (two
classes), `|#cat|` (a default box with id `cat`).
```
]
```
| `\|box#cat\|` | a box, id `cat` (empty — no label). |
| `\|treat#cat\|` | type `treat`, id `cat`. |
| `\|treat#cat\| "Friendly cat"` | + label "Friendly cat". |
| `\|treat#cat\| { fill: red }` | + a style block. |
| `\|box#cat\| ""` | same as `\|box#cat\|` — `""` is just an empty string. |
| `\|box#cat\| .bold.loud { padding: 5 }` | type + id + classes + own style. |
| `\|group#garden\| { … } [ … ]` | container with style and a body. |
| `\|box\| "Load balancer"` | anonymous labelled box (can't be linked to). |
| `\|#cat\|` | a default `\|box\|`, id `cat`. |
### The label
A node has **no label unless you give it one** — a bare `|box#cat|` is an empty box
(the `#cat` is a handle, like HTML's `id=`, not text):
| no string at all | nothing — an empty box |
| `"X"` | the label "X" |
| `""` | an empty string — nothing in flow, an empty cell in a grid ([SPEC 12](#12-flow--grid)) |
A link to an *undeclared* name still draws a labelled box: `cat -> dog -> bird`
desugars to three boxes labelled "cat"/"dog"/"bird" ([Implicit nodes](#implicit-nodes)). A multi-word label needs no `[ ]`: `|box#lb| "Load balancer"`; an
*anonymous* labelled box needs no id: `|box| "Load balancer"`.
**The label is smart — each type places it.** The same `"X"` does the most useful
thing for the shape it sits on. This **one rule** is extended by every layout — a
chart's label is its title, a series' its legend entry ([SPEC 14](#14-charts)) — so no type
needs a hand-written caption or symbol:
| `\|box\|` and the shapes (`\|oval\|`, `\|hex\|`, `\|cyl\|`, `\|diamond\|`, …) | its centred text |
| `\|group\|` / `\|table\|` | its **caption** ([SPEC 8](#8-templates)) |
| `\|icon\|` / `\|sign\|` | its **symbol** — `\|icon\| "heart"` is `\|icon\| { symbol: heart }` |
| a **link** | a label along the route ([SPEC 9](#9-links)) |
| a `\|chart\|` / series / `\|axis\|` / participant / frame | its title / legend / axis title / header / guard ([SPEC 13](#13-sequence), [SPEC 14](#14-charts)) |
Because a group's label is its caption, `|group#kitchen| "Kitchen" [ … ]` needs no
hand-written `|caption|`; because an icon's label is its symbol, `|icon| "bell"`
needs no `{ symbol: … }`. Give no label and a type places nothing — one rule, no
per-type exception.
**The label takes no style of its own.** The `{ }` after the head is the *node's*
block, so a styled or nudged label rides the `[ ]` content form instead, where each
string is a leaf in its own right ([Text content](#text-content)):
```
```
**The label and `[ ]` coexist.** The label is the node's one inline item, lowered by
its type — a text or caption child prepended to the `[ ]`, or (for `|icon|`/`|sign|`)
the `symbol` — and the `[ ]` holds the rest:
```
```
One inline label only — two or more strings go in the `[ ]`.
### Text content
A string is a **text node** — always a `<text>` leaf, never wrapped:
- In a box's `[ ]` (or as the box's label) it is that box's text — centred when it
is the only in-flow child, else a flow child laid out by the box's `layout`.
- On its own (on the canvas, or in a `[ ]`) it is a free-standing flow / canvas
text node.
- Several strings are several text nodes — `"a" "b" "c"` is three (a string is
self-delimiting, so no `;` is needed between them).
- An empty `""` is suppressed (adds no text) — except as a **grid cell**, where it
holds its track ([SPEC 12](#12-flow--grid)).
- Multi-line text uses `\n`; the box sizes to the widest line, with a
`font-size × 1.2` leading between lines (plus any `line-spacing`).
A string carries **no children** — text is a leaf, not a box — but where it is
**content** (free-standing, or a child in a `[ ]`) it **may carry a style block** of
text properties: `"X" { color: red; font-weight: bold; translate: 0 -6;
rotate: 12 }`. Only text-valid properties apply (colour, every `font-*`, `opacity`,
`letter-spacing`, `line-spacing`, `text-transform`, `text-decoration`, `translate`,
`rotate`, `layer`); any other — `pin`, `padding`, `width`, a border, children, even
`href` / `title` — needs a real box, so wrap the text in a `|block|`. Set on the
string the style applies to it directly; set on a containing box it cascades down
([SPEC 6](#6-paint-stroke--text)). A string in the **label** position is the one place it is
not content but a shorthand for it, so it takes no style block — write it in `[ ]`
to style it (above).
### Implicit nodes
A link endpoint that is a **single bare id** not present in the link's **scope**
auto-creates the node `|box#cat| "cat"` in that scope — a box named `cat`, labelled
"cat" — so `cat -> dog -> bird` is a complete three-box diagram. The same holds inside
a container body: a body link auto-creates its missing endpoints among that body's own
children. Declaring the id in the scope — before or after the link — uses it instead
of creating one. A **path** endpoint (`kitchen.bowl`) is never auto-created: it must
resolve to an existing node, or it is an error. If a same-named node exists elsewhere
in the tree, the box is still created here and a warning names the other match.
### Declarations
A declaration `key: value;` lives only in a `{ }` style block — the stylesheet
(configuring the root) or a node's own block. Property names are dash-case; values
are space-separated and positional. A declaration **ends with `;`** — its value runs
to that `;` (or the block's closing `}`), so a value may span several lines (a long
expression, a per-segment list); the `;` is optional only immediately before `}`. A
bare `key: value` outside a `{ }` is an error. Every property, its value shape, and
where it applies is in the [Property Ledger](#16-property-ledger--support).
---
## 4. Selectors, Cascade & Specificity
A **rule** is `selector { declarations }`. A selector is one or more
space-separated **units**; the space is the descendant combinator. A unit is a type
`|box|` (with an optional `#id`, `|table#main|`), the **link type `|-|`**, its
drawing subtype the **dimension type `(-)`** ([SPEC 15.6](#156-dimensions)), a class
`.hot`, or an id `#hero`:
```
(-) { … } // every dimension — the |-| subtype ([SPEC 15.6](#156-dimensions))
.hot { … } // every node with class .hot
#hero { … } // the one node with id hero
.sidebar |box| { … } // every box inside a .sidebar
|table| .hot { … } // every .hot inside a table
```
A **descendant selector** matches a node (or link) whose ancestor chain contains each
unit in order (not necessarily adjacent), exactly like CSS's descendant combinator.
Every construct keeps its sigil — `|box|`, `|-|`, `(-)`, `.hot`, `#hero` — so a selector
reads as a run of marked units; a bare word is never a selector. `|-|` and its
dimension subtype `(-)` are selector-only: a link is drawn by an operator, never
instantiated ([SPEC 9](#9-links)).
A type's class never glues into its bars (`|box.hot|` is rejected): a class is
**worn**, not part of identity. To match boxes-with-a-class, style the class
(`.hot { … }`); to match within one, use a descendant (`.hot |box|`).
A **define** introduces a new type from a base: `|treat::box| { … }`. Its
declarations are the type's defaults; an optional `[ ]` gives it intrinsic children
(materialized per instance — see [SPEC 9](#9-links)).
A **class** is defined by `.name { … }` and **worn** by writing it after the
identity (`|box| .hot`) or after a link's endpoints (`a -> b .hot`) — the same
`.class` slot on both, never inside the bars.
**Selecting vs. drawing is decided by the section, not the syntax.** `|box| .hot`
in the stylesheet is a descendant *rule* (.hot inside a box); on the canvas it is
an *instance* (a box wearing .hot). One reads as a selector, the other draws —
because rules live in the stylesheet and instances on the canvas.
### The cascade
Properties on a node merge like CSS — **the more specific source wins**, ties broken
by **later wins** (source order). The tiers, low to high:
1. **Type cascade** — walked from the base primitive up to the node's declared type,
layering each type's element-rule (`|box| { }`) and define defaults. A more-derived
type overrides what it builds on. (This is where a template's and a define's baked
defaults live — [SPEC 8](#8-templates).)
2. **Descendant rules** — `|table| |box| { }`, `.sidebar |box| { }`, matched against
the ancestor chain.
3. **Class rules** — `.hot { }`, worn via `|box| .hot` on the node.
4. **Id rule** — `#hero { }`, the node's own id.
5. **The instance's own block** — `|box#client| { fill: white }` — the most specific,
beats everything above.
A link walks the **same ladder** — its type is `|-|`, its ancestors are its scope's
container chain, it has no id: the baked link base plus the scope's `clearance` /
`routing` (tier 0), the `|-|` element rule (type), descendant `|…| |-|` and worn-class
rules, then the link's own block ([SPEC 9](#9-links)). A **dimension** is a link
subtype — type chain `|-|` → `(-)` — so a `(-) { }` rule beats `|-| { }` for
dimensions (the more-specific type, tier 1) ([SPEC 15.6](#156-dimensions)).
**Complex values replace wholesale.** The merge is per-property, not deep:
`translate: x y` or `padding: t r b l` on a higher tier replaces the whole value from a
lower one, never blending component-by-component. A `pin`ned child ignores `cell:` —
pinning takes it out of the grid ([SPEC 5](#5-the-box-model)).
Inheritable properties (the text family, `color`, `clearance`, `routing`) additionally
flow **down** the tree — nearest ancestor wins — independent of the specificity tiers
above ([SPEC 6](#6-paint-stroke--text)).
---
## 5. The Box Model
A node's **bounding box** is the smallest axis-aligned rectangle containing it,
stroke included.
1. **Center origin.** Every bbox is centered at the parent's origin by default.
2. **Source order = render order;** later draws on top, with pinned children above
the in-flow ones. `layer: N` overrides; ties break by source order.
3. **Strokes count** toward the bbox — `width: 100 height: 50 stroke-width: 4` →
104×54.
4. **`|path|`** is the only center-origin exception — `path:` uses native top-left
coordinates.
5. **Rotation** applies last as an SVG transform; the rotated bounding rectangle
propagates upward.
### `pin` — out of the flow
Every child is **in flow** by default — laid out by its container's `layout`
([SPEC 11](#11-the-layout-model)). **`pin` lifts a child out**, aligning the child's
**matching point** flush with a named point of the parent:
| `none` *(default)* | — in flow; nothing is pinned |
| `center` | centre on the parent's centre |
| `top` · `bottom` · `left` · `right` | flush against that parent edge |
| `top left` · `top right` · `bottom left` · `bottom right` | with its corner on that parent corner |
The child's *own* matching point lands on the parent's, so it sits **flush**. The
anchor is the parent's **drawn box** — border and padding included. Corners fall out
of the value, so one switch covers every anchor.
A pinned child is an **overlay**. It **does not grow the parent** — a parent of only
pinned children collapses to `2 × padding` — and it **paints above** the in-flow
children, so a badge needs no explicit `layer`. The canvas always includes it, so an
overlay is never clipped. Set `layer:` to reorder overlapping pins, or to push one
*beneath* the flow.
### `translate` and `rotate` — the universal nudge and turn
**`translate: x y`** shifts a node by (x, y) *after* it is placed. It works on
**every** node — flow children, pinned children, text nodes, the root alike — and is
layout-neutral: siblings don't move, the parent doesn't grow, no size changes. It is
CSS's standalone `translate`, baked into the node's origin (so a standalone SVG needs
no transform variable); the canvas still includes the shifted node.
There is **no numeric coordinate property**. Because the parent's origin is its
center, `pin: center` + `translate: x y` lands a child's center at parent-local
(x, y) — explicit coordinates with no node-size arithmetic.
**`rotate: N`** turns a node N degrees about its bbox center, applied last as an SVG
transform. Like `translate`, it works on **any** node, text included — so a link label
or a stray string can be nudged or turned in place. `pin` (which needs a parent anchor
and takes a child out of the flow) is a **box** job; to pin text, wrap it in a `|block|`.
### Auto-sizing
`width` and `height` default to **`auto`** — the bbox sizes to its content (text or
child nodes) **plus `padding` on each side** (default 20 on a framed box; there is no
separate text padding). Sizing is **border-box**: padding sits *inside* the box, never
added on top, and the two axes are independent. An explicit `width` / `height` is a
**floor** — the box is exactly that size when its content fits, and grows past it (to
`content + padding`) when the content is larger, so a box never clips or spills its
content. A box with no in-flow content — empty, or holding only `pin`ned overlays —
has nothing to grow for: an explicit size stands exactly as written, and an **auto**
one falls to **`2 × padding`** on each axis (the default `padding` 20 gives a 40 × 40
minimum).
**Padding also places the content.** The content area is the box inset by `padding`,
and the content sits within it; symmetric padding centres it, while an asymmetric
`padding: t r b l` offsets it — `padding: 4 4 20 4` lifts the content toward the top,
away from the larger bottom inset, exactly like CSS.
Exceptions: a **text** node sizes to its glyphs (no padding), widened by
`letter-spacing` and given `line-spacing` between `\n` lines; `|icon|` is a square
that grows with its `[ ]` text (a `32` floor) and needs a `symbol`; `|line|` / `|poly|` /
`|image|` / `|path|` require their geometry (`points` / `src` / `path`) and error
without it. `|block|` carries `padding: 0`, so a bare block sizes to its content
exactly.
Text width uses one advance per character (≈ 0.6 em). The default font is monospace,
so this is essentially exact; a proportional `font-family` override makes it
approximate until embedded font metrics land ([SPEC 23](#23-deferred)).
---
## 6. Paint, Stroke & Text
The visual vocabulary shared by every node. These are ordinary properties — the full
list, with value shapes and defaults, is the [Property Ledger](#16-property-ledger--support);
the colour system they draw on is [SPEC 10](#10-colour-variables--expressions). This section
is the *behaviour*.
### Paint
**`fill` paints a body, `color` a label.** `fill` is a closed shape's interior (and,
on text, an alias for its `fill`); `color` sets text colour for a subtree and
cascades through the SVG via native `currentColor` — set it on a container to recolour
every descendant's text that doesn't override. `opacity` (0–1) fades a node whole.
`fill`, `stroke`, and `gap-fill` each accept a **gradient** as well as a flat colour
([SPEC 10](#10-colour-variables--expressions)).
### Stroke
**One stroke role paints a shape's outline and a link's wire alike** — `stroke` the
colour, `stroke-width` the thickness (markers scale with it), `stroke-style` the dash
pattern (`solid` / `dashed` / `dotted`, plus the drafting `center` / `phantom` on
shapes and `|line|`s and `wavy` on links — [SPEC 7](#7-nodes)). There is no parallel
`link-*` family: a `.class` carrying `stroke` dresses whichever wears it, node or link
([SPEC 9](#9-links)). A closed primitive's default outline is `--stroke` at width 2; a
`|group|` softens to width 1.
### Text
The text family — `font-family`, `font-size`, `font-weight`, `font-style`,
`text-transform`, `text-decoration`, `letter-spacing`, `line-spacing`, and `color` —
**inherits**: nearest ancestor wins, like CSS. Set it on a containing box (or the root)
and it cascades down, or set it on a string's own block (`"x" { font-weight: bold }`)
for that one text node. Style globally with `font-size:` etc. in the stylesheet, or
scope it on a container. Body text defaults to `font-size` 15, `font-weight` `normal`;
captions 12 and link labels 11 carry their own baked defaults.
Two kinds of text property, split by whether they touch layout:
- **Baked spacing** — `letter-spacing`, `line-spacing`, and `font-size` — changes
**layout** (the text box grows to fit the wider glyphs or taller block) and compiles
into the glyph and line positions, never emitted as a style. `font-size` can never be
a runtime `var()` — text is measured at compile time. `letter-spacing` / `line-spacing`
default to 0, so text is unaffected until set.
- **Live CSS** — `font-style`, `text-transform`, `text-decoration` — does *not* touch
layout: it rides the class / `<g>` / `.lini` rule and a host page can override it. Set
any in the global block to style the whole scene.
For a global `font-family` / `color`, prefer the `--lini-font-family` /
`--lini-text-color` variables (or a `--theme`) for an **embeddable** diagram — they stay
live for a host page to re-theme, where a global property bakes its value into the
`.lini` rule ([SPEC 10](#10-colour-variables--expressions), [SPEC 17](#17-svg-output)).
---
## 7. Nodes
12 primitives. All accept position ([SPEC 5](#5-the-box-model)) and paint ([SPEC 6](#6-paint-stroke--text));
closed primitives also accept `stack`, `rotate`, `shadow`. Text is **not** a primitive —
it is bare content ([SPEC 3](#3-statements--the-label)); the frameless `|block|` box
([SPEC 8](#8-templates)) is what you reach for when text needs an id, a class, a link, or box
layout.
**Dimensions** use `width` / `height`, each defaulting to `auto` (content + padding,
**border-box** — [SPEC 5](#5-the-box-model)). They are always **bbox dimensions**:
`|oval| { width: 60; height: 40 }` is an ellipse in a 60×40 box; equal dimensions (or an
empty `|oval|`) make a circle.
| `\|block\|` | size (auto) | The base rectangle — frameless (no fill/stroke, `radius: 0`, `padding: 0`), like a `div`. It keeps `stroke-width: 2` (invisible while `stroke: none`), so a styled block gets a sensible border. `\|box\|` frames + rounds it, `\|rect\|` frames it sharp ([SPEC 8](#8-templates)). |
| `\|oval\|` | size (auto) | Bbox ellipse; equal width/height = circle. |
| `\|hex\|` | size (auto) | Regular hex, flat top/bottom. |
| `\|slant\|` | size (auto) | Parallelogram; top edge shifted `tan(skew) × h`. `skew` in degrees, (-89, 89), default 15. |
| `\|cyl\|` | size (auto) | Cylinder; end ellipses ≈ h/10. |
| `\|diamond\|` | size (auto) | Rhombus inscribed in the bbox. |
| `\|poly\|` | `points` | ≥3 points, local (center-origin) coords. Closed. |
| `\|path\|` | `path` | Raw SVG path. **Native top-left coords.** |
| `\|line\|` | `points` | 2+ points. Markers via `marker*:`. |
| `\|icon\|` | `symbol` | A **Phosphor** icon — `symbol:` (or the label) names it; paints two-tone like a box (`fill` body, `stroke` line, counter-scaled `stroke-width`). A square that grows with its `[ ]` text (`32` floor); `\|sign\|` is the larger preset. See [Icons](#icons). |
| `\|image\|` | `src`, `width`, `height` | `<image href="…">`. External URLs only; both dimensions required. `fit` maps it into the box — `auto` (default, letterbox), `contain`, `cover`, or `stretch`. |
| `\|sketch\|` | `draw` | A **pen** that folds to a path — profiles drawn call by call, with named points and edges, mirroring, and view breaks ([SPEC 15.3](#153-the-sketch-pen)). Closed-primitive paint; bbox from the geometry. |
**`radius`** rounds a rectangle's corners — `|box|` defaults to 8, `|block|` / `|rect|`
to 0. It is honoured on the rectangle (and on a multi-point `|line|`'s joins); `radius`
on the non-rect primitives (hex / diamond / slant / poly) is deferred ([SPEC 23](#23-deferred)).
### Visual modifiers (closed primitives)
| `stroke-style` | `solid` / `dashed` / `dotted` / `center` / `phantom` | Stroke pattern. Default `solid`. `center` (dash-dot) and `phantom` (dash-dot-dot) are the drafting line conventions — axes and alternate positions — valid on shapes and `\|line\|`s everywhere ([SPEC 15.7](#157-leaders-notes--line-conventions)); a link's set stays `solid` / `dashed` / `dotted` / `wavy` ([SPEC 9](#9-links)). (`wavy` on closed primitives is deferred — [SPEC 23](#23-deferred).) |
| `stack` | `N` / `dx dy` | Draw an offset duplicate behind the node. Scalar `N` = `N -N`. |
| `rotate` | `N` degrees | Rotate around the bbox center ([SPEC 5](#5-the-box-model)). |
| `shadow` | `N` / `dx dy` / `dx dy blur` / `dx dy blur color` | Drop shadow via SVG `<filter>`. Scalar `N` = offset `N N`, blur `N`; tint defaults to `--lini-shadow-color`. |
### Markers (on `|line|` and links)
| `marker: X` | Both ends. |
| `marker-start: X` | Start end (link source). |
| `marker-end: X` | End end (link target). |
Values: `none`, `arrow`, `dot`, `circle`, `diamond`, **`datum`** (the filled drafting
triangle a drawing's `>-` leader lowers to — [SPEC 15.7](#157-leaders-notes--line-conventions)), and the ER **cardinality set** —
`crow` (the "many" foot), `one` (a bar `|`), `exactly-one` (a double bar `‖`),
`zero-or-one`, `one-or-many`, `zero-or-many` (a bar or `○` paired with the foot). The
compositional operators `-+` / `-<` / `-o+` / `-+<` / `-o<` / `-++` are sugar over this
set ([SPEC 9](#9-links)). `circle` is a larger `dot` — a filled point sized for
hovering or reading (on a chart line it marks a data point; [SPEC 14](#14-charts)). Markers scale
with `stroke-width` (a link's wire and a shape's outline alike), floor 5 px; colour follows
the stroke.
`|line|` is bare by default — write `|line| { marker-end: arrow }` for a one-shot
arrow. For links the operator picks markers (see [SPEC 9](#9-links)). Source order wins:
`marker: arrow; marker-end: dot` → start arrow, end dot.
### Icons
`|icon|` draws a **[Phosphor](https://phosphoricons.com/)** icon (MIT) as inline SVG
paths — themeable, reproducible, and renderer-agnostic (no icon font). The `symbol`
property names it — or, as the [smart label](#the-label), the string does (`|icon| "heart"` is
`|icon| { symbol: heart }`); everything else paints like a box:
```
|icon| "heart" { fill: --rose-wash; stroke: --rose-ink }
|icon#tag| "bell" [ "3" ] // symbol bell, "3" rides as text
```
Setting the symbol twice — a label *and* `{ symbol: … }` — is an error; pick one. A
text label on an icon rides in the `[ ]` (`|icon| "bell" [ "3" ]`).
Phosphor icons are **two-tone** (a soft fill behind a line), so an icon wears Lini's
paint roles like any node: **`fill`** paints the body, **`stroke`** the line,
**`stroke-width`** its weight. The defaults make the duotone read out of the box —
`fill` a soft grey (`--icon-fill`), `stroke` the ink (`--stroke`, matching borders
and wires), `stroke-width` 2. A single-tone line icon is `fill: none`; a hued duotone
is `fill: --teal-wash; stroke: --teal-ink`, exactly like a card.
`stroke-width` is **counter-scaled**: an icon is authored on a 256-unit grid and fit
to its box, and the stroke is divided by that scale (baked at compile time), so its
line weight holds as the icon resizes and matches the diagram's other strokes.
An icon is a **square** that grows uniformly with its `[ ]` text (and `padding`): the
side is a `32` floor (`icon-size`) over the text + padding on either axis, so an
empty icon is 32×32 and a longer label scales the **whole icon up** — symbol and all
— keeping its proportion (the symbol never distorts). For a larger stand-alone icon,
reach for `|sign|` ([SPEC 8](#8-templates)).
**`fit`** controls how the symbol fills that box. By default (`fit: auto`) an icon
keeps Phosphor's authored framing — each glyph sits in the 256-grid with its own
built-in margin, so different glyphs fill the box by different amounts and a row of
mixed icons reads at an even weight. `fit: contain` scales the glyph's *own* bounds
up until they meet the box (filling it — and `|sign|` defaults to it); `cover` scales
until the box is covered (the glyph may overflow); `stretch` fits both axes (may
distort). The counter-scaled `stroke-width` follows the resulting scale, so the line
weight stays constant whichever `fit` you choose.
A missing `symbol` errors like `|poly|` without `points`; an unknown one suggests the
nearest name. Only the icons a diagram uses are embedded (a default-on `icons` feature,
[SPEC 23](#23-deferred)).
---
## 8. Templates
Built-in types — each a bundle over a primitive base, named because the pattern is
common. **Every rectangular template is a bundle over `|block|`**; the non-rect
primitives ([SPEC 7](#7-nodes)) stand on their own. A template's defaults are the low tier of
the cascade ([SPEC 4](#4-selectors-cascade--specificity)) — every value here is overridable.
| `\|box\|` | `\|block\|` | `fill: --fill; stroke: --stroke; stroke-width: 2; radius: 8; padding: 20` | The **default** node — a rounded, framed card. |
| `\|rect\|` | `\|box\|` | `radius: 0` | A sharp-cornered box. |
| `\|group\|` | `\|block\|` | `stroke: --group-stroke; stroke-style: dashed; stroke-width: 1; fill: --group-fill; radius: 8; padding: 20` | Dashed frame for a caption + children. |
| `\|caption\|` | `\|block\|` | `pin: top left; translate: 0 -18; color: --caption-color; font-size: 12; font-weight: --caption-font-weight` | A title, pinned just above the group's top-left corner. |
| `\|footnote\|` | `\|caption\|` | `pin: bottom; translate: 0 17; font-size: 12; color: --footer-color` | A caption flipped to a shape's bottom edge — a centred, muted footnote. |
| `\|badge\|` | `\|block\|` | `pin: top right; translate: 6 -6; radius: 8; padding: 2 6; shadow: 2 3 3; fill: --accent; color: --accent-text; font-size: 11; font-weight: normal` | Corner pill — nudged out over the top-right corner, grows nothing. |
| `\|row\|` | `\|block\|` | `direction: row` | Frameless wrapper — children in a row. |
| `\|column\|` | `\|block\|` | `direction: column` | Frameless wrapper — children in a column. |
| `\|grid\|` | `\|block\|` | `layout: grid` | Frameless grid (needs `columns`). |
| `\|sign\|` | `\|icon\|` | `width: 64; height: 64; padding: 4; stroke-width: 2; fit: contain` | A larger icon as a stand-alone node, with room for a short label; `fit: contain` fills the box (unlike a bare `\|icon\|`). |
| `\|table\|` | `\|group\|` | `layout: grid; align: stretch; justify: stretch; gap: 1; gap-fill: --stroke; padding: 0; fill: none; stroke: --stroke; stroke-width: 2; stroke-style: solid; font-size: 14; font-weight: normal; scale: 1` | Ruled grid (see below). |
| `\|cell\|` | `\|block\|` | `padding: 4 8` | A **table cell** — a frameless `\|block\|` carrying the text-to-gutter inset. Body cells wrap in it; `\|header\|` / `\|footer\|` build on it. Style all cells with `\|cell\| { … }` or, per table, `\|table\| \|cell\| { … }`. |
| `\|header\|` | `\|cell\|` | `fill: --header-fill; font-weight: bold` | A **header** cell — a filled, bold band (a `\|table\|`'s first row; an `\|entity\|`'s title spans them). |
| `\|footer\|` | `\|cell\|` | `color: --footer-color` | A **footer** cell — muted text; opt-in on the last row. |
| `\|entity\|` | `\|table\|` | `columns: auto auto` | An ER / database **entity** — a titled field list, rows left-aligned (see below). |
| `\|note\|` | `\|block\|` | `fill: --fill; stroke: --stroke; padding: 20; scale: 1` | A **note** — the folded-corner callout card, one type in every layout (see below). |
| `\|balloon\|` | `\|oval\|` | `width: 16; fill: --fill; stroke: --stroke; font-size: 11; scale: 1` | An item **balloon** — the numbered circle an assembly leaders to a part ([SPEC 15.8](#158-assemblies-views-sheets--titles)). |
| `\|drawing\|` | `\|block\|` | `layout: drawing; padding: 0; scale: 4` | An engineering **drawing** — geometry on a datum, measured annotations ([SPEC 15](#15-drawing)). |
| `\|hole\|` | `\|oval\|` | `fill: --bg; stroke: --stroke-dark` — `width:` **required**, the diameter | A round **hole** — punches by paint order, draws its own centre marks ([SPEC 15.4](#154-features-holes--patterns)). |
| `\|centerline\|` | `\|line\|` | `stroke-style: center; stroke: --stroke-light; stroke-width: 1; fill: none` — needs `points:` | The dash-dot axis / symmetry line ([SPEC 15.7](#157-leaders-notes--line-conventions)). |
| `\|pitch-circle\|` | `\|oval\|` | `stroke-style: center; stroke: --stroke-light; stroke-width: 1; fill: none` — `width:` **required**, the diameter | The dash-dot bolt circle; round, so a `(o)` reads its PCD ([SPEC 15.7](#157-leaders-notes--line-conventions)). |
| `\|breakline\|` | `\|line\|` | `stroke: --stroke-light; stroke-width: 1; fill: none` — needs `points:` | A break cut's edge — the thin jogged line a `break:` generates ([SPEC 15.3](#153-the-sketch-pen)); manual use is free. |
| `\|hidden\|` | `\|sketch\|` | `stroke-style: dashed; stroke: --stroke-dark; stroke-width: 1; fill: none` — needs `draw:` | **Hidden edges** — interior geometry on its own dashed child, per the one-node-one-stroke-style law ([SPEC 15.7](#157-leaders-notes--line-conventions)). |
| `\|shoulder\|` | `\|line\|` | `stroke: --stroke-dark; stroke-width: 2; fill: none` — needs `points:` | A turned part's **shoulder line** — the geometry-weight edge a `revolve:` generates at every sharp diameter change ([SPEC 15.3](#153-the-sketch-pen)); manual use is free. |
| `\|page\|` | `\|block\|` | `layout: flow; scale: 4; fill: --bg` — `sheet: a4` unless sized | An ISO 5457 drawing **sheet** — mm dimensions via `sheet:`, `scale:` px per mm; frame, zones, and centring marks as generated chrome ([SPEC 15.8](#158-assemblies-views-sheets--titles)). |
| `\|title-block\|` | `\|table\|` | `font-size: 11; stroke-width: 1` | The ISO 7200 **title block** — a table the `\|page\|` seats flush inside its frame's bottom-right corner ([SPEC 15.8](#158-assemblies-views-sheets--titles)). |
| `\|frame\|` | `\|rect\|` | `fill: none; stroke: --stroke; stroke-width: 2` | A sheet's **frame** — the thick border a `\|page\|` generates at the ISO margins ([SPEC 15.8](#158-assemblies-views-sheets--titles)). |
| `\|zone\|` | `\|block\|` | `font-size: 9; color: --stroke-light` | A **zone reference** label (1, 2… / A, B…) a `\|page\|` generates in the margin band ([SPEC 15.8](#158-assemblies-views-sheets--titles)). |
| `\|tick\|` | `\|line\|` | `stroke: --stroke; stroke-width: 1; fill: none` — needs `points:` | A zone **divider** / **centring mark** a `\|page\|` generates ([SPEC 15.8](#158-assemblies-views-sheets--titles)). |
The bare `|block|` is the base everything rectangular builds on — frameless, yet a real
box (id, class, children, wirable, positionable): what you reach for to wrap text that
needs box behaviour.
**Captions.** A `|caption|` is a small `|block|` **pinned** just above the group's
top-left corner; a `|footnote|` is the same flipped to the bottom. Both are out-of-flow
overlays, so they never push the content, and their place is fixed by the template,
not by where they sit among the children. A group's **label is its caption** ([SPEC 3](#the-label)),
so the two forms are equal:
```
|box#b| "Network"
|footnote| "synced"
]
|group#panel| [ // the explicit form
|caption| "Settings"
…
]
```
Style every caption globally with `|caption| { font-size: 16; font-weight: bold }` —
that targets captions without touching body text. Because a caption is pinned (not in
flow), a group laid out as a `row` carries its title just the same.
**Notes.** A `|note|` is the callout card — a filled block with a folded top-right
corner. It is **one type in every layout**: in a `sequence` it binds to lifelines with
`over:` / `left:` / `right:` ([SPEC 13](#13-sequence)); in a `drawing` it places at the
datum, usually wired by a leader ([SPEC 15.7](#157-leaders-notes--line-conventions)); in
flow / grid it is an ordinary padded card. Built-in scoped rules — `|sequence| |note|`
and `|drawing| |note|`, each `{ padding: 6 10; font-size: 13 }` — keep it compact where
convention expects, exactly as `|table|` insets its `|cell|`s; override them like any rule.
**Tables.** A `|table|` is sugar — a `group` that is a grid with `gap: 1` and
`gap-fill: --stroke`, so the 1px gaps between cells paint as hairline rules
([SPEC 12](#12-flow--grid)). Each body cell wraps in a `|cell|`, the type that
carries the text-to-gutter inset (`padding: 4 8`); `|header|` / `|footer|` build on
`|cell|`, so every cell — but not the caption, a plain `|block|` — is inset. Style all
cells with `|cell| { … }`, or per table with `|table| |cell| { … }`, exactly as you
style headers with `|table| |header| { … }`. The table sets `align: stretch;
justify: stretch`, so **every cell fills its track** — backgrounds fill and text has
room. The outer frame is the group border, the inner rules the gap paint
([SPEC 11](#11-the-layout-model)); no edge is doubled. A table's label is its caption.
**Column alignment.** `align` (↔) / `justify` (↕) on the table read per column
([SPEC 12](#12-flow--grid)) and align the *cells' text*: since the cells already fill, the
table's own `align`/`justify` are carried onto each cell — a `start`/`end` column's cells
wear a `.lini-align-*` / `.lini-justify-*` class — and a filled cell places its text at
that edge (`center` is the default). So `align: start center end` reads three columns
left / centre / right, header band and body alike.
A table's **first row becomes its header** — each cell wrapped as a `|header|`, a filled
bold band; `|table| |header| { font-weight: normal; fill: none }` reverts it. A **footer**
is opt-in: wrap a last-row cell in `|footer|`. Every cell is a box now — header/footer
carry a fill; a body cell is a frameless `|block|` wrapping its text, so the padding rule
and the column's alignment reach it ([SPEC 17](#17-svg-output)).
```
} [
"Fruit" "Quantity" "Notes" // the header row — filled + bold
"Apple" "12" "fresh"
"Mango" "3" "ripe"
]
```
`fmt` knows the column count and pads the cells into aligned columns, so the flat form
reads like the table it is. A cell that must be placed or linked is a **box** child
(`|cell| "X"` for a padded cell, or `|box| { cell: 2 1; … }`); a cell that just needs a
colour or weight can take its own style block (`"Apple" { color: --red-ink }`).
**Entities.** An `|entity|` is sugar over `|table|` (two auto columns) for an ER /
database card: its **label is its title** — a `|header|` spanning every column, centred
over left-aligned `"field" "type"` rows (an entity's field rows read left by default; the
title keeps its centred, full-span band). Add a column for a **key** gutter —
`{ columns: auto auto auto }` gives `"PK"/"FK" "field" "type"`. In an entity (not a plain
table) a `|header|` / `|footer|` cell spans the full width.
```
Relationships are ordinary links ([SPEC 9](#9-links)): `users -< orders` is one-to-many, `a >-< b`
many-to-many, landing on the entity edge; the full cardinality set composes from `[min][max]`
end-markers (`-o<` zero-or-many, `-+<` one-or-many, `-o+` zero-or-one, [SPEC 9](#9-links)). To anchor a wire to one **field**, give that cell an
id (`|block#user_id| "user_id"`) and link the path (`orders.user_id -< users.id`). Keys are
plain content (`"id" { font-weight: bold }`); an entity adds no grammar.
Extend any template: `|panel::group| { stroke: --accent }`. Common nodes need no
template:
| Circle | `\|oval\| { width: 40 }` |
| Database | `\|cyl\|` |
| Arrow | `\|line\| { marker-end: arrow; points: 0 0, 50 0 }` |
---
## 9. Links
A link connects scene-node ids with an operator (`a -> b`). Like every node it has a
`{ }` **style** and a `[ ]` of **content** — its content is its **labels** (text),
placed along the route by `along:`. It is never written as a `|link|` instance; the
operator draws it.
A link is **styled like a node**: its type is `|-|` — a line in the identity capsule,
the one selector that matches every link — so `stroke` is its wire and `color` /
`font-*` its labels, the ordinary vocabulary ([SPEC 6](#6-paint-stroke--text)) with no
parallel family. Only **`clearance`** and **`routing`** stay scene config — geometry,
not paint — set on a container's `{ }` and cascading to its links.
### Operators
A link op is `[start_marker?][line][end_marker?]`, no spaces:
| Line | `-` solid · `--` dashed · `---` dotted · `~` wavy |
| Start markers | `<` arrow · `>` crow · `*` dot · `<>` diamond · `+`/`o` ER cardinality (below) |
| End markers | `>` arrow · `<` crow · `*` dot · `<>` diamond · `+`/`o` ER cardinality (below) |
The line grows more broken as it lengthens — solid `-`, dashed `--`, dotted `---`.
The same marker glyph differs by position (`<` is arrow at the start, crow at the
end).
| `->` `<-` `<->` | arrow combinations, solid |
| `-*` `*-` `*-*` | dot combinations |
| `-<>` `<>-<>` | diamond |
| `-<` `-+` `-o<` `-+<` `-o+` `-++` `>-<` | ER cardinality (crow's-foot, below) |
| `-->` `--->` `~>` | dashed / dotted / wavy |
| `-` `--` `---` `~` | no markers (each line style) |
If the operator carries no markers, there are none on both ends. Explicit `marker:` /
`marker-start:` / `marker-end:` override the operator (source order wins). The
operator's line part sets the link's `stroke-style` (`--` ⇒ `dashed`, `---` ⇒ `dotted`,
`~` ⇒ `wavy`); an explicit `stroke-style:` overrides it.
**ER cardinality — a crow's-foot marker, composed.** A cardinality marker reads
`[min][max]`: the **min** ring `o` (zero) or bar `+` (one) hugs the line, the **max**
bar `+` (one) or crow (many — `<` at the end, `>` at the start) sits outermost. **Either
end takes one; the two sides mirror** — `a +-< b` is one-to-many, `a >o-o< b` zero-or-many
both ways. The six relations, shown end-side:
| `-+` | one |
| `-<` | many |
| `-o+` | zero-or-one |
| `-+<` | one-or-many |
| `-o<` | zero-or-many |
| `-++` | exactly one |
A lone `-o` (no max) errors; a hollow *endpoint* is `marker-end: circle` ([SPEC 7](#7-nodes)).
The ops are sugar over the `marker:` set (`one`, `exactly-one`, `zero-or-one`,
`one-or-many`, `zero-or-many`, `crow` — [SPEC 7](#7-nodes)); `marker*:` overrides.
### Syntax
```
endpoints op endpoints [op endpoints …] [ "label" ] [ .class… ] [ { style } ] [ [ labels ] ]
```
The tail is the **node tail** (`"label" .class { style } [ … ]`); only the head differs
— endpoints + operators, versus bars — and a link's `[ ]` holds only labels (text),
where a node's holds children.
`endpoints` is one or more endpoints joined by `&`:
```
a -> b // 1 link
a -> b -> c // chain: 2 links
a -> b & c // fan-out: a→b, a→c
a & b -> c // fan-in
a & b -> c & d // cartesian: 4 links
a -> b -> c & d // chain + fan
```
Mixing operators in one chain is a parse error.
A link's **class follows** its endpoints (`a -> b .loud`), exactly as a node's
follows its identity (`|box| .loud`) — one `.class` slot, after the head, on both; a
class never lives in the bars. On a chain or fan, the label, class, and `{ }` apply to
every link the statement expands to.
### Styling
**`stroke` is the wire; `color` / `font-*` the labels** — the same `stroke` paints a
node's outline and a link's wire ([SPEC 6](#6-paint-stroke--text)), so a class carrying it
dresses either:
| `stroke` | colour | `--stroke` | The wire's colour. |
| `stroke-width` | number | 2 | Wire thickness; markers scale with it. |
| `stroke-style` | `solid` / `dashed` / `dotted` / `wavy` | from the operator | The dash pattern; usually set by the op (`-->` ⇒ dashed), overridable here. |
| `color` · every `font-*` · `letter-spacing` · … | — | inherits / baked | The labels ([Labels](#labels)). |
`|-| { … }` styles every link; a descendant (`#g |-|`, `|table| |-|`) or a worn class
scopes it, exactly as `|box|` / `#g |box|` / `.hot` scope a node; a link's own `{ }`
overrides — the same cascade a node walks ([SPEC 4](#4-selectors-cascade--specificity)):
```
{
.flow { stroke: --teal } // a worn class — nodes or links
clearance: 12; routing: orthogonal // scene config, cascades to links
}
a -> b .flow "hi" { stroke: red; stroke-style: dashed } // one link overrides
```
`|-|` is **selector-only**: a link is drawn by an operator, so `|-|` never appears as an
instance ([SPEC 20](#20-errors)). `clearance` (default 16) and `routing` (default
`orthogonal`) are **scene config** — geometry, not paint — set on a container's `{ }`,
cascading to that scope's links, nearest winning; `marker*` come from the operator and
override per link.
### Labels
A link's label is **text**, placed along the route by `along:` — the link's track
rule, exactly as `columns:` is a grid's. One label trails the head (`a -> b
"watches"`); two or more, or a styled one, ride the `[ ]`:
| `along` | A list of `0..1` fractions along the whole drawn route, one per label (`along: 0.2 0.5 0.8`). Omitted → auto-distribute across the hops, so one label avoids junctions and several spread out. |
```
a -> b "watches" // the common case — one label, auto-placed
a -> b "watches" .loud { stroke: red } // + a class and wire colour
a -> b { along: 0.3 0.7 } [ "near a" "near b" ] // two labels
a -> b [ "watches" { translate: 0 -6 } ] // a styled / nudged label
```
Each label is an ordinary **styleable text leaf** ([SPEC 3](#3-statements--the-label)): give it its
own `{ }` in the `[ ]` to nudge or turn it. The head label takes no style — the `{ }`
after a link's head is the *link's* — so a styled label rides the `[ ]`, exactly as a
node's does. A label is an obstacle to nothing, and may slide along the link to keep
clear of nodes and other labels; the link never moves for it. Link labels default to
`font-size: 11`, `font-weight: normal`, and are tinted by the link's `color` — a link's
text props cascade to its labels; set them via `|-| { font-size: 14; color: --blue }`
to restyle every link's labels at once, on one link's `{ }` to restyle its labels, or
on a label's own `{ }` to restyle one.
### Endpoints & scope
```
endpoint = ident { "." ident } [ ":" side ]
A path walks with `.` into children; a final `:side` forces a side. Every link
resolves in a **scope** — the scene root for top-level links, the container's body for
links written inside one. The first segment names a node in the scope, each further
segment a child of the previous. **There is no search.** A single bare id not in the
scope auto-creates a box there ([Implicit nodes](#implicit-nodes)); a **multi-segment
path** that does not resolve is an error, and the error suggests full paths of
same-named nodes —
`link endpoint 'kitchen.bowl' not found at scene root; did you mean 'kitchen.counter.bowl'?`
| `cat` | root node `cat` |
| `kitchen.counter.bowl` | exactly that path |
| `kitchen.counter.bowl:left` | the same node, left side forced |
Bodies are **sealed**: a body link connects nodes of its own subtree only.
Cross-container links are written at the lowest level where both ends are visible —
usually the root. Without a side the router picks edges by geometry; with a `:side`,
that edge is forced.
An **anonymous** container opens no scope: it is **scope-transparent** — its
children belong to its parent's scope (ids stay unique across it), a dot-path
never names it, and its own `[ ]` links resolve in the parent's scope. Name a
container to give it a scope of its own — a scope-owning body (a `|drawing|`'s
mates, a `|sequence|`'s messages) therefore wants an id. A sequence frame is
transparent the same way ([SPEC 13](#13-sequence)).
### Internal links in a body
A container's (or define's) `[ ]` may link its own children — children and links read in
**source order**, so a wire usually trails the boxes it joins but may also sit among them
(a `layout: sequence` ([SPEC 13](#13-sequence)) relies on this — its frames interleave with its
messages). In a define, ids are local and materialize per instance — the same sealed-body
rule. From outside, the dot-path navigates in:
```
{
} [
|box#inlet| "Inlet"
|box#outlet| "Outlet"
inlet -> outlet "flows"
]
}
garden.outlet -> kitchen.inlet "carries"
```
### Routing
Links route **orthogonally** by default — horizontal and vertical runs through the
free space between nodes, corners rounded. The router picks entry/exit sides unless an
explicit `:side` forces one. `clearance` (default 16) is the minimum gap every link
keeps from nodes and from other links.
`routing` selects the strategy for a scope and cascades like `clearance`: `orthogonal`
(the default) routes by the contract below; `straight` draws each link as one segment
between the bodies, trimmed to their boundaries — it avoids nothing and reports
nothing; `curved` is named but deferred ([SPEC 23](#23-deferred)). It pairs with `layout` —
`layout` places the nodes, `routing` routes the links between them — so a group can
route its internals one way while the root routes another. Which subsystem realises a
scope's links is the scope's **wiring strategy** ([SPEC 11](#11-the-layout-model)): the
orthogonal (or `straight`) router for `flow` / `grid`, layout-time lowering for
`sequence`, `chart` / `pie`, and `drawing`.
The full routing contract — clearance, spacing, crossings, fan-out, self-loops — lives
in [`ROUTING.md`](ROUTING.md), the source of truth for routing.
---
## 10. Colour, Variables & Expressions
CSS variables theme the **visual** layer — colours and the font family. Everything
that affects layout — sizes, gaps, padding, and font *size* — is a baked constant, so
a standalone SVG never depends on host CSS. This section also holds the **expression
engine** ([SPEC 10.7](#107-expressions--functions)), the one place operators appear.
### 10.1 Visual variables (live, themeable)
Each colour is a `light-dark(LIGHT, DARK)` value, so one SVG carries both modes:
```
--lini-bg light-dark(white, #1b1b1f) the scene background
--lini-fg light-dark(black, #e8e8ea)
--lini-fill light-dark(white, #26262b)
--lini-stroke light-dark(#444, #9aa0a6)
--lini-stroke-dark light-dark(black, white) the primary drafting tone — pen geometry, dimension/leader linework, and their heads read full black on white (the ISO print look)
--lini-stroke-light light-dark(#0000008b, #ffffffa3) the secondary line tone — drafting's thin support lines (centerlines, break lines, extension lines): full black/white at reduced alpha, so a support line crossing dark geometry blends toward it instead of greying it
--lini-accent light-dark(#0a84ff, #4aa3ff)
--lini-accent-text white text on an accent fill (e.g. a badge)
--lini-muted light-dark(#888, #9aa0a6)
--lini-danger light-dark(crimson, #ff6b6b)
--lini-warn light-dark(orange, #ffb454)
--lini-stray light-dark(crimson, #ff6b6b) the stray-link fallback (ROUTING.md, Impossible layouts)
--lini-group-stroke light-dark(rgba(0,0,0,.4), rgba(255,255,255,.4))
--lini-group-fill light-dark(rgba(0,0,0,.03), rgba(255,255,255,.05))
--lini-header-fill light-dark(rgba(0,0,0,.06), rgba(255,255,255,.08)) the table / entity header band
--lini-icon-fill light-dark(rgba(0,0,0,.16), rgba(255,255,255,.18)) the soft body behind a duotone icon
--lini-caption-color light-dark(rgba(0,0,0,.5), rgba(255,255,255,.55))
--lini-footer-color light-dark(rgba(0,0,0,.5), rgba(255,255,255,.55))
--lini-font-family ui-monospace, "SF Mono", "Cascadia Code", "JetBrains Mono", Menlo, Consolas, "Liberation Mono", monospace
--lini-font-weight normal
--lini-caption-font-weight normal
--lini-link-font-weight normal
--lini-text-color var(--lini-fg)
--lini-shadow-color light-dark(rgba(0,0,0,.2), rgba(0,0,0,.5))
```
`--lini-bg` paints the scene background (the canvas rect), so the diagram is
self-contained in either mode. The default font is a **monospace** stack: it reads
crisp, and a fixed glyph advance keeps text-width estimation accurate without embedded
font metrics ([SPEC 23](#23-deferred)). Body text, captions, and link labels are all
`normal` weight by default.
**Dark/light is automatic.** The compiler emits `color-scheme: light dark` on `.lini`,
so `light-dark()` follows the viewer's OS (`prefers-color-scheme`) — no script, no
`@media`. A `data-theme="dark"` / `"light"` on the SVG or any ancestor forces a mode
(it flips `color-scheme`, and its higher specificity beats the OS). All defaults sit in
`@layer lini.defaults`, so unlayered host CSS still wins with no `!important`.
`--bake-vars` freezes the light arm into literals for renderers without `light-dark()`
([SPEC 10.6](#106---bake-vars)).
### 10.2 The colour palette
Beyond the role variables, Lini ships a **named-hue palette** — pretty by default,
themeable, and dark/light-aware like everything else. Eleven hues, each a
`light-dark()` pair:
```
red rose orange amber lime green teal sky blue purple gray
```
Every hue carries **five tiers**, named for the job they do — not their lightness,
which would invert in dark mode:
| wash | `--teal-wash` | palest — card and section backgrounds (a faint tint; a deep, muted surface in dark mode) |
| soft | `--teal-soft` | a gentle, lighter pastel fill |
| base | `--teal` | the everyday pastel — **the bare name is the easy path** |
| deep | `--teal-deep` | the strong tone — borders and strokes |
| ink | `--teal-ink` | deepest and most saturated — text and emphasis (the high-contrast tone in dark mode) |
`fill: --teal` lands a friendly pastel; the job-names hold across the dark flip, so
`--teal-wash` is always the faint surface and `--teal-ink` always the high-contrast
detail.
```
```
The tiers are generated from one **OKLCH** seed per hue, so the ramp is perceptually
even and the eleven read as a family. The same space is open to you — `fill: oklch(0.7,
0.14, 200)` picks any colour directly ([SPEC 2](#2-lexical-syntax)). Names are conventional
— every one is an ordinary colour word, so `--blue`, `--red`, `--green` are all there —
with aliases for muscle memory: `--yellow → --amber`, `--pink → --rose`, `--indigo →
--purple`, `--cyan → --teal`. `red` stays clear for **danger**; `rose` is the warm pink
you decorate with (its `wash` / `soft` tiers are your pinks), `green` is tuned to an
emerald, and `lime` is the lemony one.
The palette is **tree-shaken** ([SPEC 17](#17-svg-output)): only the `--lini-*` variables a
diagram references are emitted, so the full palette costs a three-box diagram nothing.
### 10.3 Gradients
`fill`, `stroke` (a shape's outline or a link's wire), and `gap-fill` accept a **gradient** in place of a flat colour. Stops are
ordinary colours — palette `--name`s flip dark/light and bake, a raw `#hex` is a fixed
literal.
| `gradient(--rose, --sky)` | two stops, auto-angled 135° — any two hues blend cleanly |
| `gradient(--rose, --amber, --sky)` | three or more evenly-spaced stops |
| `linear-gradient(135, --rose, --sky)` | an explicit angle in degrees — the control gate |
| `radial-gradient(--rose, --sky)` | a radial blend from the centre out |
```
```
The angle is the only "more syntax": `gradient(…)` is angle-less and always lands on a
flattering 135°. OKLCH stops keep the midpoint clean
rather than muddy.
Each distinct gradient is emitted once as a `<linearGradient>` / `<radialGradient>` in
`<defs>` and referenced by `url(#…)` — deduplicated and shared like the drop-shadow
`<filter>`s ([SPEC 17](#17-svg-output)). `objectBoundingBox` units fit one definition to
any node at any size. The stops being palette vars, a gradient themes, flips, and bakes
like any other paint; gradient-on-text is deferred ([SPEC 23](#23-deferred)).
**Hatches.** `hatch()` is a paint function beside `gradient()`, valid on **`fill`**
only — the drafting section-line texture, usable in any layout:
| `hatch(45)` | section lines at 45°, pitch 6 |
| `hatch(45, 6)` | explicit pitch (sheet-space px — hatch never scales, [SPEC 15.1](#151-the-container-the-datum--the-scale)) |
| `hatch(45, 6, --gray-deep)` | explicit line colour (default `--stroke`) |
| `hatch(45 -45, 6)` | a space-group of angles — cross-hatch |
Angles use the drawing bearing (0 = up, clockwise — [SPEC 15.3](#153-the-sketch-pen)).
Each distinct hatch emits one `<pattern>` in `<defs>`, deduplicated like gradients; the
colour is an ordinary paint, so hatching themes, flips dark/light, and bakes. Hatch
line width is fixed (0.75) — a texture, not a stroke. `hatch()` on `stroke` is an
error — a stroke takes a colour or gradient.
### 10.4 `--name` references
`--name` is the **visual-variable namespace, and only that.** `--name: value;`
declares one (a built-in `--lini-*` name keeps its meaning; a new name is yours), and
`--name` in a value references it, emitting live `var(--lini-name)`:
```
{
--brand: #ff6600;
}
Alias a host var from CSS: `.lini { --lini-accent: var(--my-brand-blue); }`.
Layout values — sizes, gaps, padding, `font-size`, `clearance` — are **not** `--name`
variables: they bake (a runtime `var()` can't be measured at compile time). Set them
with a literal, a rule (`gap: 30;`, `|box| { radius: 4 }`), or a `(…)` expression /
binding ([SPEC 10.7](#107-expressions--functions)).
### 10.5 Layout constants (baked)
Baked compile-time defaults — override per-node, on the root, in rules, or in an
instance / link block:
```
font-size 15 link-font-size 11 caption-font-size 12
stroke-width 2 radius 8 gap 20 padding 20
clearance 16 icon-size 32 link-width 2 icon stroke-width 2
```
`font-size` is body text; link labels (11) and captions (12) carry their own baked
defaults — a global `font-size:` sets body text and cascades, `|-|` or a link sets link
labels, `|caption| { font-size: … }` sets captions. `radius` rounds a `|box|` by default;
`|block|` / `|rect|` are `0`.
Padding defaults to 20 — including the root, whose padding frames the whole scene (the
SVG margin) — with `|block|` / `|row|` / `|column|` at 0 and a `|table|` at `4 8` (its
cell inset). It doubles as the minimum size of an empty box (`2 × padding`; see
[Auto-sizing](#5-the-box-model)). **Every baked default — these constants and
the template bundles — lives in one place**, so the whole look is tuned from a single
file.
The drawing chrome ([SPEC 15](#15-drawing)) — sheet-space, never scaled:
```
dim-offset 18 dim-pitch 16 dim-ext-gap 3 dim-ext-overshoot 3
dim-arrow 12 × 4 datum-triangle 11 note-offset 14 note-landing 8
hatch-pitch 6 hatch line-width 0.75 break-gap 12 tol-stack 0.7
center-mark-overhang 4 drawing link stroke-width 1 drawing link font-size 12
```
### 10.6 `--bake-vars`
Class rules and inline `style=` work everywhere, but CSS *variables* don't — resvg and
librsvg fail `var()` in every position (browsers, even `<img>`-embedded, are fine).
`--bake-vars` keeps the rules but inlines every `var(--lini-name)` as its literal: no
runtime theming, but a self-contained SVG that renders anywhere.
### 10.7 Expressions & functions
A **parenthesized expression** `(…)` holds compile-time math — folded to a literal (a
number, or a point `(x, y)` for geometry) when the diagram compiles. Parentheses are the
**only place operators appear**: outside them `-` is a link or a number's sign, `<` / `>`
are markers, `//` a comment, so the parens are what let `*` mean "times". A value stays
paren-free until an operator does:
```
{ scale(n) = (100 * 1.2^n); } // a binding (below)
width: scale(3); // a call — no operator, no group
padding: (8 * 2); // an operator → a group (= 16)
}
```
**A call's own parens count**, so an operator inside a call's arguments needs no inner
group — this is what makes math usable inline everywhere. A signed number is a sign, not
an operator, so `-2` stays bare (`translate: -35 20`); to subtract, group it:
```
fn: ramp(1) // a call — bare
fn: (x * 2) // an operator → a group
draw: move(-2, 5) up(8) // calls and signed numbers — bare
draw: right(w / 2) // an operator in a call's own parens — no group
```
Inside a group the language is small and total:
- **Operators** `+ - * / ^` (`^` power, right-associative), unary `-`, grouping `( )`,
comparisons `< <= > >= == !=`, the ternary `cond ? a : b`.
- **Functions** — the math library `exp ln log sqrt abs sin cos tan min max clamp floor
round pow`, and any you define (below); each returns a number or a point, called
`name(args)`. (Colour / track builders like `rgb` / `repeat` make typed values, so
they live in value position, never inside math.)
- **Constants** `pi`, `e`; **scientific notation** `1e6`, `1.32e-6`; the sample
parameters `u` (geometry, below) and chart `x`; and your **bound names**, read bare
(below). A bare name resolves: locals → the ambient (`u` / `x`) → `pi` / `e` → your
bindings.
- **Locals** — `name = expr;` binds for the rest of the group; the **final expression is
the value** (no keyword, no `return`). `=` binds, `==` compares. A top-level `,` makes
the value a **point**. Values are numbers and points — no strings, no loops.
```
(r = 40; n = 6; 2 * pi * r / n) // r, n are locals; the last line is the value
```
**Bindings** are written in the stylesheet with `=` — a name bound to a value, for reuse
in any expression. A **scalar** is `name = value`; a **function** adds a parameter list,
`name(params) = value`. The value is **bare** when it is a literal, a name, or a call,
and a **group** when it holds an operator, locals, or a point. `=` binds and reads
compile-time (baked), where `:` sets a live property — the two never meet:
```
{
my_radius = 5; // a scalar — read bare as `my_radius`
scale(n) = (100 * 1.2^n); // a function
wave(a, f) = (u*300, a*sin(2*pi*f*u)); // a function returning a point
}
Call a binding anywhere a value goes — bare like `rgb(…)` / `repeat(…)`, or inside a
group; only an operator forces the group, never the call, and a computed argument rides
the call's own parens:
```
**Geometry.** `points:` (on `|line|` / `|poly|`) may be a **parametric expression in
`u`** — `u` sweeps `0 → 1`, sampled at `samples:` points into a vertex list, drawing
curves, waves, and spirals procedurally:
```
draw: move(-80, 0)
up(14) right(50):neck fillet(3):r1 up(8) right(60):mid fillet(3) down(8) right(50) down(14);
revolve: x-axis; // a turned part: half → whole, axis + edge lines
}
body:left (-) body:right { side: bottom } // → 160 mm
body:neck (o) { side: left; tol: h6 } // → ⌀28 h6 — the surface, doubled about the axis
body:r1 (o) // → R3 — the fillet knows its radius
```
### 15.7 Leaders, notes & line conventions
A **callout** is a one-ended link, written tip-first: the glyph hugs the feature, the
line runs toward the text — which is formally the link's **label**, so everything core
says about labels (the `[ ]` form, styling, one inline label) applies verbatim:
| `<-` | arrow | an edge or outline |
| `*-` | dot | a leader landing **within** an outline — a face, a region |
| `>-` | **datum** triangle | a datum feature (`>-` is the crow op elsewhere — the scope reinterprets it, as a sequence reinterprets `->`) |
```
bolt <- "THRU" // arrow lands on the hole's rim
face *- "Ra 1.6" // a dot — a surface note
body:seat >- "A" // datum A on that face
bolt <- [ "R3 TYP" { translate: 30 -24 } ] // a styled / nudged text — the core form
```
- A callout has **one** tip, so the singular `marker:` overrides it; the marker set
gains **`datum`** ([SPEC 7](#7-nodes)). One arrowhead style per sheet (ISO 129):
a word leader's `<-` tips with the **same drafting-slender arrow** as every
dimension; `*-`'s dot and the datum triangle keep their own shapes. A one-ended callout with no text is an
error; a one-ended `->` / `-*` errors the other way — a leader points *back* at its
feature. A label-terminated statement is single-hop; fan leaders are deferred
([SPEC 23](#23-deferred)).
- **Text placement.** The text auto-places **outward**: a **directed** feature's
leader leaves straight off its face — along the surface normal — while a point
feature's runs along the ray from the drawing's datum through it; either way just
past the geometry union (`note-offset`), horizontal — and the leader ends in a
short horizontal **landing** (`note-landing`) before it, the drafting elbow.
`side:` picks the direction instead (a side or a corner); a styled label's
`translate` nudges from there. The tip ray-casts onto the drawn outline
([15.2](#152-anchors)).
- **The leader makes the note.** A callout's text lowers to a bare leaf — drafting
callouts are unboxed. A **boxed** note is the `|note|` template
([SPEC 8](#8-templates)) wired with an ordinary two-ended link; a **balloon** is
`|balloon|` plus a leader (`b1 -* nozzle`); bare `"…"` stays plain sheet text
("SECTION A-A"). Any other **two-ended** op between two nodes draws a straight
annotation line, markers per the op — a flow direction, an exploded-view path.
**Line & material conventions.** `hatch()` fills section cuts
([SPEC 10.3](#103-gradients)); `stroke-style: center` / `phantom` are the drafting
dash conventions and `dashed` the hidden-edge one, each on its own child — one node
has one stroke style ([SPEC 7](#7-nodes)). The **`|hidden|`** template
([SPEC 8](#8-templates)) is that child ready-made — a dashed, unfilled pen profile
for interior geometry (a socket, a bore): a feature in the part's `[ ]`, rigid under
mates, riding `break:`, its `:segment`s dimensionable like any sketch's. Two chrome types carry the centerline
pattern ([SPEC 8](#8-templates)): `|centerline|` (a `|line|` — an axis, a symmetry
line, a spoke) and `|pitch-circle|` (an `|oval|`, `width:` its diameter — the bolt
circle; being round, `bc (o)` reads its PCD). A manual `|pitch-circle|` covers what
`pattern:` can't — unequally spaced holes still share one drawn circle.
**Auto chrome — one mechanism, seven producers.** The lines drafting always draws are
**generated children**, so the cascade styles or removes them with no dedicated knobs
(`|sketch| |centerline| { stroke: none }`):
| a **fused** `mirror:` ([15.3](#153-the-sketch-pen)) | the axis `\|centerline\|`, overhanging the profile |
| a `revolve:` ([15.3](#153-the-sketch-pen)) | the axis `\|centerline\|` + the `\|shoulder\|` edge lines at every sharp diameter change |
| a `thread:` ([15.3](#153-the-sketch-pen), [15.4](#154-features-holes--patterns)) | the thin minor line + the thread-end line; on a round view, the ¾ thread arc |
| `pattern: radial` ([15.4](#154-features-holes--patterns)) | the `\|pitch-circle\|` through the copies |
| a `\|hole\|` | its centre-mark crosshair |
| a `break:` ([15.3](#153-the-sketch-pen)) | the `\|breakline\|` pair — thin, sharply jogged mid-span |
| a `\|page\|` ([15.8](#158-assemblies-views-sheets--titles)) | the sheet chrome — the `\|frame\|`, the `\|zone\|` references, the `\|tick\|` dividers and centring marks |
### 15.8 Assemblies, views, sheets & titles
There is no `|assembly|` type: **an assembly is a drawing whose children mate** — and
drawings **nest**. A child `|drawing|` is one rigid body from outside (the core
sealed-body law): its internal mates, dims, and features stay in its `[ ]`, its
geometry bbox is its parts' union, and it grounds, mates, and anchors like any part.
Build sub-assemblies in isolation, then seat them — the same vocabulary at every
level; reach in where both ends are visible (`motor.shaft:right || pump.rotor:left`).
A project that wants the word writes `|assembly::drawing| { }` — a define, not a
language feature. Item balloons are `|balloon|` + a leader; the parts list is a core
`|table|` beside the drawing; auto-numbering and auto-BOM are deferred
([SPEC 23](#23-deferred)).
A multi-view sheet is ordinary layout: drawings in a `|row|` / `|grid|`, each view its
own scope and `scale:` (a 2 : 1 detail still dims true,
[15.1](#151-the-container-the-datum--the-scale)). There is no `|view|` type and no
projection engine; views **share their axes with `align: origin`**
([SPEC 12](#12-flow--grid)) — a drawing's origin is its datum, so a row of views
lines up datum-to-datum however their dimensions stack, and a grid with
`align: origin; justify: origin` is the first- / third-angle arrangement.
Projection *lines* between views stay deferred ([SPEC 23](#23-deferred)).
**A drawing's smart label is its title, placed *below*** — it lowers to a
`|footnote|` (the bottom-centred caption template), because drafting titles sit
under the view: `|drawing| "SECTION A-A"`; style every title with
`|drawing| |footnote| { … }`.
**The sheet.** `|page|` gives the multi-view story its walls: the trimmed ISO 5457
sheet as a **template container**, not a layout — inside its frame it is an ordinary
container (default `flow`; `layout:` / `columns:` / `direction:` free), hosting
drawings, tables, and notes as normal children in **sheet space** (a page is never a
drawing scope). **`sheet:`** names the trimmed size — `sheet: a3`,
`sheet: a4 landscape` — pure sugar for `width` / `height` **in millimetres** (the
orientation keyword swaps the pair; ISO defaults — A4 and A5 portrait, A3–A0
landscape; a bare `|page|` is `a4`), so an explicit `width:` / `height:` overrides
through the ordinary slot and a custom sheet still derives its zones. The
**ANSI/ASME Y14.1 letters** ride the same sugar in their own millimetres —
`sheet: b` (`a`…`e`; `a` portrait, `b`–`e` landscape) — nothing else differs. The page's
`scale:` is **pixels per millimetre**, default 4 — a `|drawing|`'s own default, so a
default drawing on a default page draws **1 : 1 true**, and the drafting ratio is the
drawing's scale over the page's (a 2 : 1 detail is `scale: 8`).
The ISO furniture is generated chrome ([15.7](#157-leaders-notes--line-conventions)):
the thick `|frame|` at the margins (a 20 mm filing edge on the left, 10 mm
elsewhere); the **zone grid** — divisions of ≈ 50 mm, rounded to the nearest even
count per edge (A4 4 × 6, A3 8 × 6, A0 24 × 16) — numbered `1…` left-to-right along
top and bottom and lettered `A…` top-to-bottom along both sides, drawn as `|zone|`
labels and `|tick|` dividers in the **reference band**: the 10 mm beside the frame
on every side, so the references read alike all round and the filing margin's
extra 10 mm stays truly empty; and the four centring marks — each crossing the
frame at an edge's midpoint (the middle divider, which would coincide, is not
drawn; the left mark starts at its band, keeping the filing strip empty). The
content area is the frame inset by 5 mm (`padding:` adds to it). A
**`|title-block|`** child (ISO 7200 — a `|table|`, [SPEC 8](#8-templates)) is seated
by **type**, flush inside the frame's bottom-right corner; its fields are ordinary
cells. A file whose drawn content is only pages **hugs them** — the paper is the
margin, so the root's `padding` defaults to 0 (your own `{ padding: … }` still
wins) and the sheet runs edge to edge of the SVG.
```
|drawing#detail| "DETAIL A" { scale: 8 } [ … ] // a 2 : 1 view
|title-block| { columns: 60 auto } [
"Title" "Socket cap screw"
"Scale" "1:1"
]
]
```
### 15.9 Lowering
`layout: drawing` resolves in the **layout** phase ([SPEC 18](#18-compile-pipeline)) —
geometry must exist before it can be measured:
1. **Geometry** per child, bottom-up: fold `draw:` to a path (corner modifiers applied
cyclically through `close()`), collect its `:segment`s, apply `mirror:` /
`revolve:` (+ the edge lines and `thread:` dressing), expand `pattern:`, build
`break:`'s view map; nested drawings lower first, becoming rigid subtrees. Compute
each node's geometry bbox (stroke excluded) and paint bbox (core).
2. **Place** children: origins on the datum, `translate:` applied.
3. **Mates**: walk from the ground; rotate first, seat, the child's own translate
after; flag cycles and over-constraints.
4. **Measure** every annotation's anchors against the seated, unbroken geometry;
compose the texts (glyph + number / label + `tol:` + count + `unit:`).
5. **Annotate**: assign dims to sides and pack the rows in source order; auto-place
callout texts outward; ray-cast leader tips; land the elbow.
6. **Lower** to primitives at baked coordinates: sketch → `|path|`; hole → `|oval|` +
centre marks; the auto chrome → generated children; dim → extension `|line|`s + a
marker-tipped dimension `|line|` + text; an angle → its arc `|path|` + text;
leader → `|line|` + marker + text; hatch → one deduplicated `<defs>` `<pattern>`.
7. **Scale** geometry per the effective per-node scale; chrome stays sheet-space. Emit
geometry in source order and annotations **above** all of it (the drawing's one
draw-order override, like a chart's semantic order; `layer:` still wins).
The output is an ordinary primitive subtree — theming, the palette, gradients,
`--bake-vars`, `fmt`, and byte-for-byte determinism apply with no drawing-specific
render code. The **parser is scope-blind**: the ops and forms parse everywhere and
*mean* drawing only in a drawing scope — elsewhere they error at resolve
([SPEC 20](#20-errors)).
### 15.10 Properties
New properties, and core ones reused with their core meaning; paint, text, and marker
properties are the core ones.
| `scale` | any node | number > 0 | px per drawing unit; nearest-wins; a `\|drawing\|` defaults to 4, other nodes to 1; position scales by the parent, shape by self ([15.1](#151-the-container-the-datum--the-scale)) |
| `unit` | `\|drawing\|` | quoted string | suffix on auto-measured values only |
| `draw` | `\|sketch\|` | pen calls + `:segment`s | **required** ([15.3](#153-the-sketch-pen)) |
| `mirror` | `\|sketch\|` | list of `x-axis` / `y-axis` / bearing | reflect + union, left to right |
| `revolve` | `\|sketch\|` | `x-axis` / `y-axis` | a solid of revolution — fused fold + the `\|shoulder\|` lines; exclusive with `mirror:` ([15.3](#153-the-sketch-pen)) |
| `thread` | `\|sketch\|` · `\|hole\|` / round geometry | `seg pitch` groups · `pitch` | ISO 6410 dressing — minor + thread-end lines; the ¾ arc ([15.3](#153-the-sketch-pen), [15.4](#154-features-holes--patterns)) |
| `sheet` | `\|page\|` | `a5…a0` / ANSI `a…e` `[portrait \| landscape]` | trimmed-size sugar → `width` / `height` in mm ([15.8](#158-assemblies-views-sheets--titles)) |
| `break` | `\|sketch\|` | `a b [axis]` groups | cut the view between stations; longer axis default ([15.3](#153-the-sketch-pen)) |
| `pattern` | any node | `grid(c, r, dx, dy)` / `radial(n, r)` | replicate about its position ([15.4](#154-features-holes--patterns)) |
| `width` | `\|hole\|` `\|pitch-circle\|` | number | **required** — the diameter |
| `tol` | a dimension | `t` / `+u -l` / fit ident | tolerance text ([15.6](#156-dimensions)) |
| `side` | a dimension / callout | side — or a corner (callouts, diametral dims) | stack side / text direction |
| `gap` | a dimension / a mate | number | dim: its offset from the geometry. Mate: separation along the normal — **may be negative** |
`fill` accepts `hatch()` ([SPEC 10.3](#103-gradients)); `stroke-style` has `center` /
`phantom` and `marker` has `datum` ([SPEC 7](#7-nodes)). `routing`, `clearance`,
`along`, and the container `gap` / `align` / `justify` have no role in a drawing scope.
---
# Part III — Reference
Canonical, dense lookup. The narrative ([Parts I–II](#part-i--core)) teaches once; this
part is the authoritative tables — every property, the output, the pipeline, the grammar,
the errors — and never repeats the prose.
---
## 16. Property Ledger & Support
Every property is `name: value;` — dash-case, positional, space-separated values
([SPEC 3](#3-statements--the-label)). This section is the one place that answers **which
property works where.**
**A property applies everywhere by default; the exceptions are marked.** An exception is
always one of two kinds: **type-owned** — a property a primitive requires or reads
(`points` on `|line|`, `symbol` on `|icon|`, `skew` on `|slant|`) — or **layout-owned** — a
property an engine interprets (`cell` on a grid, `over` on a sequence, `data` on a chart).
An **unknown or misspelled** property is **silently ignored** — the engine reads properties
by name and never rejects an unrecognised one (an unknown-property warning is deferred —
[SPEC 23](#23-deferred)). A **wrong-context** property is usually ignored too (`cell:` off a grid
simply has no effect), but a handful of **hard gates** do error: the sequence-placement props
(`over` / `left` / `right` / `activation`) off a sequence, a box property on bare text, a grid
without `columns`, a layout's own type names used outside it, and the drawing statements —
the measuring ops, `||`, `tol:` — outside a `layout: drawing` ([SPEC 20](#20-errors)).
**State marks** used below: **✓** built and honoured · **⌛** meaningful but not built, a
candidate ([SPEC 23](#23-deferred)) · **—** not applicable.
### The container × layout matrix
The high-signal grid: which **container / layout** property each engine honours. (Paint,
text, and box-model properties are universal to every node — the tables that follow.)
| `direction` | ✓ `row`/`column` | — | — | ✓ `+radial` | — | — |
| `gap` | ✓ spacing | ✓ spacing | ✓ pitch / spacing | ✓ plot gutter | ✓ plot gutter | — (dims / mates read their own — [SPEC 15](#15-drawing)) |
| `gap-fill` | ✓ | ✓ | ✓ᵇ | — | — | — |
| `padding` | ✓ | ✓ | ✓ᵇ | — | — | ✓ frames the sheet |
| `align` / `justify` | ✓ | ✓ per-column | ✓ᵇ | — | — | — |
| `width` / `height` | ✓ (slack) | ✓ (slack) | — content-sized | ✓ box size | ✓ box size | ✓ a floor |
| `columns` / `rows` / `cell` / `span` | — | ✓ | — | — (`span`→band) | — | — |
| container paint (`fill` `stroke` `radius` `shadow` `opacity` `href`) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
**✓ᵇ** — honoured on the participant / frame **boxes' own content** (they are ordinary
boxes), but *not* by the sequence engine's placement of them on the time axis
([SPEC 11](#11-the-layout-model)). A `chart` / `pie` consumes its children into marks, so that
case does not arise — hence `—`.
### Universal properties
Honoured on every drawn node, in every layout (a box; text takes the marked subset).
**Paint & stroke** ([SPEC 6](#6-paint-stroke--text), colour [SPEC 10](#10-colour-variables--expressions)):
| `fill` | colour · `none` · gradient · `auto` | `--fill` (box) · `none` (block/line) · `--icon-fill` (icon) · `currentColor` (text) · `--bg` (root) |
| `color` | colour | inherits (`--text-color`) — text colour for the subtree |
| `opacity` | `0..1` | 1 |
| `stroke` | colour · `none` · gradient | `--stroke` (`--group-stroke` on group) |
| `stroke-width` | number | 2 (group / frame 1) |
| `stroke-style` | `solid`·`dashed`·`dotted`·`wavy`·`center`·`phantom` | `solid` — `wavy` on links today (closed prims ⌛); `center` / `phantom` on shapes and `\|line\|`s ([SPEC 15.7](#157-leaders-notes--line-conventions)) |
| `radius` | number | 0 (block/rect) · 8 (box/group) — rect + polyline join; non-rect ⌛ |
| `shadow` | `N` · `dx dy` · `dx dy blur` · `dx dy blur color` | off — tint `--shadow-color` |
**Text** — all **inherit** ([SPEC 6](#6-paint-stroke--text)); text-valid on a bare string:
| `font-family` | ident · string · `--var` | `--font-family` | live |
| `font-size` | number | 15 (link 11, caption 12) | baked |
| `font-weight` | `normal` · `bold` | `normal` | live (numeric ⌛) |
| `font-style` | `normal` · `italic` · `oblique` | `normal` | live |
| `text-transform` | `uppercase` · `lowercase` · `capitalize` · `none` | `none` | live |
| `text-decoration` | `underline` · `overline` · `line-through` · `none` | `none` | live |
| `letter-spacing` | number | 0 | baked |
| `line-spacing` | number | 0 | baked |
**Box model & placement** ([SPEC 5](#5-the-box-model)):
| `width` · `height` | number · `auto` | `auto` | border-box; a **floor**. `\|image\|` needs both. |
| `padding` | `N` · `v h` · `t r b l` | 0 (block) · 20 (box) | inner padding; places content. Longhands `padding-top`/… accepted. |
| `pin` | `none` · `center` · edge · corner | `none` | out-of-flow anchor; a **box** property (not text). |
| `translate` | `x y` | — | post-placement nudge; **any** node incl. text. |
| `rotate` | degrees | 0 | turn about bbox centre; **any** node incl. text. |
| `layer` | integer | 0 (flow) · 1 (pinned) | paint order; ties → source order. |
| `scale` | number > 0 | 1 (`\|drawing\|` / `\|page\|` 4) | px per drawing unit — nearest-wins; position scales by the parent, shape by self ([SPEC 15.1](#151-the-container-the-datum--the-scale)). |
| `pattern` | `grid(…)` · `radial(…)` | — | replicate about the node's position ([SPEC 15.4](#154-features-holes--patterns)). |
**Media & accessibility** — any node (`href` also a link):
| `href` | quoted URL | wraps the node / link in `<a href>` — clickable. |
| `title` | quoted string | emits a `<title>` child (tooltip + screen-reader name). |
### Type-owned properties
Read on the listed primitive; required where noted ([SPEC 7](#7-nodes)).
| `points` | `\|line\|` `\|poly\|` | `x y, …` · parametric `u` expr | vertex list; **required**. |
| `samples` | `\|line\|` `\|poly\|`, chart `fn:` | integer | sample count (geometry; chart default 24). |
| `path` | `\|path\|` | quoted SVG path | **required**; native top-left coords. |
| `src` | `\|image\|` | quoted URL | **required**. |
| `symbol` | `\|icon\|` | ident | Phosphor name; **required** (or via the label). |
| `fit` | `\|icon\|` `\|image\|` | `auto` · `contain` · `cover` · `stretch` | maps content into the box (size unchanged); `auto` default, `\|sign\|` `contain`. |
| `skew` | `\|slant\|` | degrees `(-89,89)` | 15. |
| `stack` | closed primitives | `N` · `dx dy` | offset duplicate behind. |
| `marker` · `marker-start` · `marker-end` | `\|line\|`, links | see [SPEC 7](#7-nodes) | endpoint / vertex glyphs; from the operator on a link. |
| `draw` | `\|sketch\|` | pen calls + `:segment`s | **required** ([SPEC 15.3](#153-the-sketch-pen)). |
| `mirror` | `\|sketch\|` | `x-axis` / `y-axis` / bearing list | reflect + union ([SPEC 15.3](#153-the-sketch-pen)). |
| `revolve` | `\|sketch\|` | `x-axis` / `y-axis` | solid of revolution — fused fold + `\|shoulder\|` lines ([SPEC 15.3](#153-the-sketch-pen)). |
| `thread` | `\|sketch\|` `\|hole\|` round geometry | `seg pitch, …` · `pitch` | ISO 6410 thread dressing ([SPEC 15.3](#153-the-sketch-pen), [SPEC 15.4](#154-features-holes--patterns)). |
| `sheet` | `\|page\|` | `a5…a0` / ANSI `a…e` `[portrait \| landscape]` | trimmed-size sugar → `width` / `height` in mm ([SPEC 15.8](#158-assemblies-views-sheets--titles)). |
| `break` | `\|sketch\|` | `a b [axis]` groups | cut the view between stations ([SPEC 15.3](#153-the-sketch-pen)). |
### Grid, chart, pie, sequence & drawing properties
Layout-owned — an error only where a hard gate exists ([SPEC 20](#20-errors)); otherwise inert
out of scope.
| `layout` | any container | `flow`·`grid`·`sequence`·`chart`·`pie`·`drawing` | `flow` | [SPEC 11](#11-the-layout-model) |
| `direction` | flow, chart | `row`·`column`·`radial` | `column` | [SPEC 11](#11-the-layout-model) |
| `gap` · `gap-fill` · `align` · `justify` · `padding` | flow, grid | — | see matrix | [SPEC 11](#11-the-layout-model), [SPEC 12](#12-flow--grid) |
| `columns` · `rows` | grid | track list | — (`columns` required) | [SPEC 12](#12-flow--grid) |
| `cell` · `span` | grid box child | `col row` / `cols rows` | `— / 1 1` | [SPEC 12](#12-flow--grid) |
| `data` · `fn` | chart series | list / pairs / `(…)` expr | — | [SPEC 14.3](#143-data--formulas) |
| `tags` | chart series | quoted-string list | — | [SPEC 14.3](#143-data--formulas) |
| `curve` | `\|line\|` `\|area\|` | `linear`·`smooth`·`step` | `linear` | [SPEC 14.2](#142-series) |
| `baseline` | `\|area\|` | number | axis zero | [SPEC 14.2](#142-series) |
| `axis` | series, `\|mark\|`, `\|band\|` | an `\|axis\|` id | — | [SPEC 14.4](#144-axes-scales--domain) |
| `bars` · `categories` · `samples` | `\|chart\|` | see [SPEC 14.1](#141-the-chart-plane) | `grouped` · indices · 24 | [SPEC 14](#14-charts) |
| `hole` | `\|pie\|` | `0` ≤ n < `1` | 0 | [SPEC 14.7](#147-direction-radial--pie) |
| `legend` · `tooltip` | `\|chart\|` `\|pie\|`, series (`tooltip`) | see [SPEC 14](#14-charts) | auto · auto | [SPEC 14](#14-charts) |
| `value` | `\|slice\|` `\|bubble\|` | number ≥ 0 | — | [SPEC 14](#14-charts) |
| `at` | `\|mark\|` `\|bubble\|` | `V` / `X Y` | — | [SPEC 14.5](#145-bands--annotations) |
| `side` · `range` · `scale` · `step` · `ticks` · `unit` · `gridlines` | `\|axis\|` | see [SPEC 14.4](#144-axes-scales--domain) | — | [SPEC 14.4](#144-axes-scales--domain) |
| `over` · `left` · `right` | sequence `\|note\|` | id(s) | — | [SPEC 13](#13-sequence) |
| `activation` | `\|sequence\|` | `auto` · `none` | `auto` | [SPEC 13](#13-sequence) |
| `scale` (homonym: an `\|axis\|`'s is `linear`·`log`) | any node | number > 0 | 1 | [SPEC 15.1](#151-the-container-the-datum--the-scale) |
| `unit` | `\|drawing\|`, `\|axis\|` | quoted string | — | [SPEC 15.1](#151-the-container-the-datum--the-scale), [SPEC 14.4](#144-axes-scales--domain) |
| `tol` | a dimension | `t` / `+u -l` / fit ident | — | [SPEC 15.6](#156-dimensions) |
| `side` | a dimension / callout (also `\|axis\|`, above) | side · corner | by axis | [SPEC 15.6](#156-dimensions) |
| `gap` | a dimension / a mate | number (a mate's may be < 0) | — | [SPEC 15.5](#155-mates), [SPEC 15.6](#156-dimensions) |
### Link properties
A link is styled like a node ([SPEC 9](#9-links)) — its wire takes `stroke*`, its labels the
text props. Its own properties:
| `clearance` | number | 16 | min gap from nodes and links. **Scene config** — cascades. |
| `routing` | `orthogonal` · `straight` | `orthogonal` | wiring strategy; scene config, cascades. `curved` ⌛. |
| `along` | fraction list | auto | label positions along the route. |
| `marker` · `marker-start` · `marker-end` | marker | from the operator | endpoint glyphs ([SPEC 7](#7-nodes)). |
---
## 17. SVG Output
```svg
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="X Y W H" width="W" height="H" class="lini">
<style>
@layer lini.defaults {
:root, .lini { color-scheme: light dark; }
.lini[data-theme="dark"], [data-theme="dark"] .lini { color-scheme: dark; }
.lini[data-theme="light"], [data-theme="light"] .lini { color-scheme: light; }
}
.lini { font-family: var(--lini-font-family); font-size: 15px; font-weight: var(--lini-font-weight); color: var(--lini-text-color); }
.lini .lini-canvas { fill: var(--lini-bg); }
.lini .lini-box { fill: var(--lini-fill); stroke: var(--lini-stroke); stroke-width: 2; }
.lini .lini-style-hot { stroke-width: 3; }
.lini .lini-link { stroke: var(--lini-stroke); stroke-width: 2; fill: none; }
</style>
<defs><!-- filters, gradients, clipPaths --></defs>
<rect class="lini-canvas" .../> <!-- the scene background (--lini-bg) -->
<g class="lini-scene"> <!-- scene tree --> </g>
<g class="lini-links"> <!-- links --> </g>
</svg>
```
`viewBox` auto-sizes to content + the scene's `padding` (20 px by default) on every
side. The `lini-canvas` backing rect paints the scene background (`--lini-bg`) over the
viewBox; a root `fill:` overrides it (`none` = transparent).
**Paint compiles to CSS; geometry bakes.** Node and link paint defaults — and every
rule — are stated once as class rules; only the classes actually used are emitted — and
likewise only the `--lini-*` variables actually referenced, so the built-in palette
([SPEC 10.2](#102-the-colour-palette)) adds nothing unless a diagram uses it. A node whose
resolved paint differs from those rules carries the difference as an inline `style="…"`
(inline beats class, mirroring the [cascade](#4-selectors-cascade--specificity)). Geometry —
sizes, positions (`pin` and `translate` fold into the baked origin), radii, points, paths,
transforms — is always baked into attributes. Inherited text properties state on `.lini`
and cascade natively; a node's own text property emits on its `<g>` (or directly on the
`<text>`) and inherits to its subtree.
**Box:**
```svg
<g class="lini-node lini-{type} lini-{base} lini-style-{class}"
data-id="ID" transform="translate(X,Y)">
<title>…</title>
</g>
```
Auto-classes: `lini-node` (every box); `lini-{name}` (the type and every type it
inherits, down to `lini-block`); `lini-style-{name}` (per worn class). With rotation,
the transform becomes `translate(X,Y) rotate(N)`.
**Text** emits a bare `<text class="lini-text">…</text>` at its placed position — no
wrapping `<g>`. A table's cells are `|block|`s wrapping their text, so each renders as a
`<g class="lini-block …"><text>…</text></g>`; the header and any `|footer|` cells carry
a fill, a body cell is frameless ([SPEC 8](#8-templates)). Text's font and colour come by
inheritance from the enclosing `<g>`; a string's own style block emits as a `style="…"`
(and `translate` / `rotate` as a `transform`) on the `<text>` itself.
**Link:**
```svg
<g class="lini-link lini-style-{class}" data-from="A" data-to="B">
<path d="…" fill="none" stroke="…"/>
<polygon class="lini-marker lini-marker-arrow" …/>
<text class="lini-text" …>label</text>
</g>
```
Host CSS may restyle any `lini-`-prefixed class; layout is computed at compile time, so
runtime restyling (a fatter `stroke-width`) restyles without re-layout. A chart's or
sequence's lowered primitives ([SPEC 18](#18-compile-pipeline)) emit exactly like the boxes,
text, and lines above — a chart's tooltip card is a `<g class="lini-chart-tip">`, a
reserved styling hook. A drawing's dimension anatomy ([SPEC 15.6](#156-dimensions))
lowers with its own hooks, paint stated once per class: `lini-dim-line` (dimension /
leader linework), `lini-ext-line` (extension lines, `--lini-stroke-light`), and the
slender arrowheads as `lini-marker lini-marker-dim`.
---
## 18. Compile Pipeline
A reference pipeline; implementations may differ if the observable output matches.
**Parse.** Lex to tokens, then a single recursive-descent pass to the AST. The
bracket-and-bars vocabulary (`|…|` identity, `{ }` style, `[ ]` content) resolves every
statement with one token of lookahead — no type-set prescan ([SPEC 21](#21-grammar)).
**Desugar.** Lower all surface sugar to primitives + classes — the engine's true input.
Each template/define instance becomes its base primitive wearing a `.lini-*` class
chain (derived→base→primitive, down to `block` for every rectangular type); a type's
defaults and any `|type| { }` element rule fold into a generated `.lini-<type> { … }`
class; a `|table| |box| { }` descendant rule rewrites to `.lini-table .lini-box { }`, and
`|-|` (the link type) to `.lini-link` — the class every link wears; define bodies inline
per instance; the scene defaults (`layout`, `padding`, `gap`, `font-size`, `clearance`,
`routing`) settle on the root; the per-type smart label (text / caption / symbol / link
label / chart title …), auto-`along:`, and link auto-create (an undeclared endpoint `x` →
`|box#x| "x"`) become explicit. The pass is idempotent; type-system errors (cycle,
depth > 16, a define shadowing a built-in) surface here.
**Resolve** (top-to-bottom):
1. *Variables, functions & rules:* merge visual-var defaults ← `--theme` ←
`--name: value`; build the function table; compile the stylesheet's class / id /
element / descendant rules. Parenthesized expressions and function calls fold to literal
numbers / points ([SPEC 10.7](#107-expressions--functions)).
2. *Scene tree:* each box is a primitive wearing `.lini-*` (type) and user classes;
layer properties per the [cascade](#4-selectors-cascade--specificity) — the worn
`.lini-*` classes as the type tier, then descendant, class, id rules, and the instance
block; lift internal links; build the path index.
3. *Links:* resolve endpoints by scoped path walk with suggestion errors; merge link
properties through the node cascade — the baked base plus the scope's `clearance` /
`routing`, the `|-|` element rule, descendant `|…| |-|` and class rules, then the
link's own block; cartesian-expand fan groups into one resolved link per pair; the
operator's line sets `stroke-style` unless overridden.
**Layout** (bottom-up): leaf bbox from `width`/`height` or defaults (text → its glyphs;
box → content + `padding`; + half-`stroke-width` per side); arrange flow children per
`layout` / `direction` honouring `align`/`justify`/`stretch`/`evenly` when there is slack; pin
out-of-flow children to their parent anchor (the parent never grows for them); compute
gutters; apply `padding`; apply each node's `translate`; `rotate` last. A **layout-owning**
container — `sequence` ([SPEC 13](#13-sequence)), `chart` / `pie` ([SPEC 14](#14-charts)), and
`drawing` ([SPEC 15](#15-drawing)) — instead
reads its whole subtree here and lowers it to primitives: a sequence places participants
and walks its scope's messages / frames / notes in source order, emitting lifelines, arrows,
frames, and notes — **consuming those messages**, so the router never sees them; a chart
fixes its shared scale and lowers series / axes / bands / annotations at baked pixels; a
drawing folds its geometry, seats its mates, measures, and lowers its annotations the same
way ([SPEC 15.9](#159-lowering)).
**Route links.** Per [`ROUTING.md`](ROUTING.md) — orthogonal, clearance-respecting,
deterministic — over every link **except** those a `sequence` or `drawing` scope already drew.
Place markers (sized `max(5, stroke-width × 4)`, tip on the endpoint) and link labels at
their `along:` fractions (auto-distributed when unset).
**Render.** Depth-first emit SVG per [SPEC 17](#17-svg-output): a box is a `<g>`, a string is a
`<text>`. A lowered chart / sequence subtree renders as ordinary primitives — so theming,
palette, gradients, `--bake-vars`, `fmt`, and byte-for-byte determinism apply with no
layout-specific render code.
---
## 19. CLI
```
lini [options] <input.lini>
lini fmt [--check] [--stdout] <input.lini>
lini desugar <input.lini>
lini serve [--port N] [--bake-vars] [PATH]
lini theme [NAME]
```
| `-o FILE` | Output path (default stdout). |
| `--format svg\|html` | `svg` (default) or HTML wrapper. |
| `--check` | Parse + resolve only — layout/render errors still surface on a full compile. |
| `--theme NAME\|FILE\|A/B` | A built-in theme (`dark`, `high-contrast`, …), a CSS file of `--lini-*` overrides, or a light/dark pair (`light/dark`). |
| `--no-warn` / `--strict` | Silence warnings / treat them as errors. |
| `--bake-vars` | Inline `var()`s as literals (for non-browser renderers — [SPEC 10.6](#106---bake-vars)). |
| `--watch` | Recompile on every input change (requires `-o`). |
| `-h`, `-V` | Help / version. |
`lini -` reads stdin (filename `<stdin>` in errors). **`lini serve`** runs a local live
preview (default port 7700): a `.lini` file live-reloads that one file; a directory (or
no path → the current directory) opens the **playground** — pick, edit, and render any
`.lini` file beneath it in the browser. **`lini theme`** lists the built-in themes;
**`lini theme NAME`** prints one as a `--lini-*` CSS file — a ready starting point for
your own (`light-dark()` colours, the font commented out).
**`lini fmt`** reformats to canonical style — 2-space indent, `key: value;`
declarations grouped on one line, a style-only node collapsed onto its head line when it
fits (`|box#api| { fill: red }`), a lone label trailing the head (`|box#api| "API"`),
children one per line in `[ ]`, table cells padded into aligned columns, a `draw:`
value broken before each `move()` and wrapped between calls at the column limit
(continuations indented, so a profile reads as its subpaths), comments and
blank lines preserved. `--check` exits 1 if it would change anything; `--stdout` writes
instead of rewriting.
**`lini desugar`** prints the file fully **lowered to primitives** — the Desugar pass
([SPEC 18](#18-compile-pipeline)) that is the engine's true input — so the lowered form
re-renders byte-identically. A chart's or sequence's *type* desugars here (a `|chart|`
is a `|block|` wearing `.lini-chart`); its geometric primitive subtree is a layout-phase
artefact ([SPEC 18](#18-compile-pipeline)), like a routed link's geometry. A
teaching/debugging view; prints to stdout, never rewrites, comments not preserved.
Exit codes: 0 success · 1 parse/resolution error or `--check` reformat needed · 2 I/O ·
3 invalid CLI.
---
## 20. Errors
Format: `filename:line:col: error: <message>` (LSP-compatible), compile-time, with a span.
**Identity, cascade & statements**
| Duplicate id | `duplicate id 'X' (previously at L:C)` |
| Unknown type / class | `unknown type 'X'` / `unknown class '.X'` |
| Inheritance cycle / depth | `cycle in 'X → … → X'` / `'X' exceeds max inheritance depth (16)` |
| Define shadows builtin | `'X' shadows a built-in type` |
| Empty bars | `'\| \|' needs a type or an '#id'` |
| Invalid id | `'#123' is not a valid id — an id starts with a letter or '_'` |
| Class inside the bars | `a class follows the bars — write '\|box\| .hot', not '\|box.hot\|'` |
| Symbol set twice | `an icon's symbol is its label or 'symbol:', not both` |
| Text carries children | `text content takes no '[ ]' — wrap it in '\|block\|' to give it children` |
| Box property on text | `'pin' needs a box — wrap the text in '\|block\|'` |
| Declaration outside a block | `a declaration belongs in a '{ }' block` |
| Bare node on the canvas | `a node leads with bars — write '\|box#X\|' (a bare name is a link endpoint)` |
| Bare type in the stylesheet | `a type only appears in bars — write '\|box\| { }' to style every box` |
| Missing declaration ';' | `a declaration ends with ';'` |
| Style block holds non-decl | `a '{ }' style block holds only declarations` |
| `[ ]` holds a declaration | `declarations go in '{ }', not '[ ]'` |
| Styled head label | `a head label takes no '{ }' — put the text in a '[ ]' to style it: [ "…" { … } ]` |
| Two head labels | `one inline label — put two or more in a '[ ]'` |
| Label after a class | `a label comes before classes — write '\|box\| "X" .hot'` |
| Stylesheet after canvas | `the stylesheet '{ }' must come first, before any instance` |
| Glued compound in a rule | `a selector unit can't glue a type and a class — space them (descendant) or style '.hot'` |
| Spaced class chain | `classes glue into a chain — write '.hot.loud', no space` |
**Links & routing**
| Unknown endpoint (path) | `link endpoint 'X' not found at <scope>` + `; did you mean 'A', 'B'?` |
| Auto-create shadows a node | `endpoint 'X' auto-created at <scope> — a node 'X' also exists at 'A.B.X'` (warning) |
| Chain mixes operators | `link chain mixes operators 'X' and 'Y'` |
| Chain < 2 nodes | `link requires at least two endpoints` |
| Bare `o` marker | `'-o' needs a max glyph — write '-o<', '-o+', or 'marker-end: circle'` |
| Missing required property | `'\|line\|' requires 'points'` |
| `->` in the stylesheet | `'->' draws a link on the canvas — style every link with '\|-\| { stroke: … }' in a '{ }' block` |
| `\|-\|` / `\|link\|` as an instance | `a link is drawn by an operator — '\|-\|' only styles links (write 'a -> b')` / `links are drawn by operators, not the '\|link\|' type` |
| `\|node\|` as instance | `'node' is the umbrella concept — write '\|block\|' for the bare box` |
| Deferred routing | `routing: 'orthogonal' and 'straight' are built; 'curved' is deferred (SPEC 23)` |
| Unknown side | `':X' is not a side — use top, bottom, left, or right` |
| Link labels split | `keep a link's labels together — write 'a -> b [ "x" "y" ]'` (warning) |
**Values, colour & expressions**
| Invalid / out-of-range color | `invalid color 'XYZ'` / `rgb(300,0,0): component out of range` |
| Invalid `oklch()` | `oklch expects (L, C, H) or (L, C, H, A) — L and A in 0..1, C ≥ 0, H in degrees` |
| Gradient with < 2 stops | `gradient() needs at least two colour stops` |
| `linear-gradient` without an angle | `linear-gradient needs an angle first, then ≥ 2 colour stops` |
| Single-quoted string | `single quotes are not strings — use "…"` |
| Unquoted text value | `'title' takes a quoted string — write title: "…"` |
| Invalid `pin` value | `'pin' expects none, center, an edge (top/bottom/left/right), or a corner (e.g. 'top right')` |
| Negative container `gap` | `a container's 'gap' must be ≥ 0` — a **mate's** `gap:` may go negative ([SPEC 15.5](#155-mates)) |
| `skew` out of range | `skew: N must be in (-89, 89)` |
| Unknown name in an expression | `unknown name 'foo' in an expression` |
| Function arity | `'scale' takes 1 argument, got 2` |
| Spaced call paren | `a call's '(' glues to its name — write 'rgb(…)'` |
| `hatch()` off `fill` | `'hatch' is a fill — 'stroke' takes a colour or gradient` |
**Layout — grid**
| Missing `columns` | `'layout: grid' requires 'columns'` |
| Empty / bad track | `'columns' needs at least one track` / `a track is a size, 'auto', or repeat(…)` |
| Grid out of range | `cell: 5 _ exceeds columns=3` |
**Layout — sequence**
| Sequence node outside a sequence | `'\|loop\|' belongs in a 'layout: sequence'` (same for `\|opt\|` / `\|alt\|`; a `\|note\|` is core — [SPEC 8](#8-templates)) |
| `\|else\|` outside an `\|alt\|` | `'\|else\|' separates an '\|alt\|' — write it inside one` |
| `\|note\|` in a sequence, no placement | `a sequence '\|note\|' needs 'over:', 'left:', or 'right:'` |
| Sequence property off a sequence | `'over' is valid only in a 'layout: sequence'` (same for `left` / `right` / `activation`) |
**Layout — chart & pie**
| Series / axis / band / mark outside a chart | `'\|bars\|' is a chart series — it belongs in a 'layout: chart'` |
| `\|slice\|` outside a pie | `'\|slice\|' belongs in a 'layout: pie'` |
| Pie given an axis or series | `a pie's children are '\|slice\|' only` |
| Empty chart / pie | `a chart needs at least one series` / `a pie needs at least one '\|slice\|'` |
| Series with both / neither `data:` `fn:` | `a series takes 'data' or 'fn', not both` / `a series needs 'data' or 'fn'` |
| `arrow` / `crow` marker on a series | `'marker: arrow' has no centred form on a chart — use dot, circle, or diamond` |
| `fn:` list ≠ band count | `'fn' has N formulas but the chart has M bands` |
| Data ≠ categories count | `series data has N values but the chart has M categories` |
| `tags:` count ≠ data count / on `fn:` | `'tags' has N labels but the series has M data points` / `'tags' needs explicit 'data'` |
| `categories:` + an axis text | `set 'categories' or an axis's tick text, not both` |
| `\|mark\|` without `axis:` / bad `at:` | `a '\|mark\|' needs 'axis:'` / `'at' takes one value (a line) or two (a point)` |
| `\|bubble\|` missing `at:` / `value:` | `a '\|bubble\|' needs 'at:' (x y) and 'value:'` |
| Unknown `axis:` id | `axis 'X' not found` + `; did you mean 'Y'?` |
| `range:` bad / equal ends | `'range' takes two ends: 'a b', 'a auto', or 'auto b'` / `'range' needs distinct ends` |
| `scale: log` over a non-positive domain | `a 'scale: log' axis needs a domain above 0` |
| `side:` in `direction: radial` | `'side' has no meaning in a radial chart — it has one radius axis` |
| `hole:` out of range | `'hole' is a fraction 0..1` |
| Negative slice value / pie total zero | `a '\|slice\|' value must be ≥ 0` / `a pie's slice values sum to zero` |
**Layout — drawing** ([SPEC 15](#15-drawing))
| `\|sketch\|` without `draw:` | `'\|sketch\|' requires 'draw'` |
| `\|hole\|` / `\|pitch-circle\|` without `width:` | `'\|hole\|' requires 'width' — its diameter` |
| Unknown pen call / arity | `unknown draw call 'X'` / `'arc' takes (dx, dy, r) or (r, deg)` |
| `fillet` / `chamfer` off a corner | `'fillet' modifies the corner between two segments` |
| Floating `:segment` | `a ':segment' glues to its call — name a station with point():v` |
| Bare `point()` | `'point()' names the pen's position — attach a ':segment'` |
| Arc radius too small | `arc radius N is smaller than half the chord` |
| Bad `mirror:` item | `'mirror' takes x-axis, y-axis, or a bearing` |
| Bad `break:` group | `'break' takes two stations 'a b' — a < b — and an optional x-axis / y-axis` |
| `break:` off a sketch | `'break' cuts a '\|sketch\|' — draw the profile with the pen` |
| `break:` station off the profile | `'break' at N misses the profile` |
| Overlapping `break:` groups | `'break' spans overlap — merge them` |
| `break:` through a cubic | `a 'break' can't cut a 'curve()' — move the stations` ([SPEC 23](#23-deferred)) |
| Drawing statement outside a drawing | `'(-)' draws a dimension — it belongs in a 'layout: drawing'` (same for `(o)`, `(<)`, `\|\|`, corner anchors, `tol:`, …) |
| Unknown endpoint | `dimension endpoint 'X' not found at <scope>` + suggestions — **never auto-created** |
| Corner order | `':right-top' is not an anchor — did you mean ':top-right'?` |
| `(>)` | `'(>)' is reserved — the angle op is '(<)'` |
| One-ended `(-)` / `\|\|` | `a linear dimension measures two anchors` / `a mate seats two parts` |
| Two-ended `(o)` | `'(o)' measures one round feature — write 'a:top (o)' for a span` |
| Empty one-ended leader | `a leader needs its text — 'bolt <- "THRU"'` |
| One-ended `->` / `-*` | `a leader points back at its feature — write 'a <- "…"'` |
| Bare `(o)` with no axis | `'(o)' can't pick an axis on 'X' — anchor a side ('X:top (o)') or a segment` |
| `(<)` on a point anchor | `an angle reads two edges — a named segment, a '\|line\|', or a side` |
| Unary `(<)` on an unmirrored name | `'(<)' on ':taper' needs 'mirror:' or 'revolve:' — no twin to measure against` |
| Station `⌀` on a mirror-only profile | `a station '⌀' reads a revolved profile — 'revolve: x-axis'` |
| `revolve:` + `mirror:` together | `a sketch takes 'revolve:' or 'mirror:', not both` |
| Bad `revolve:` value | `'revolve' takes x-axis or y-axis` |
| Bad `thread:` group | `'thread' takes a segment and its pitch — 'thread: m8 1.5'` |
| `thread:` without `revolve:` | `'thread' dresses a revolved profile — add 'revolve: x-axis'` |
| `thread:` segment off-axis / not straight | `'thread' runs along the axis — 'm8' must be a straight run parallel to it` |
| Unknown `thread:` segment | `no segment 'm8' in this 'draw:'` + suggestions |
| `thread:` on a non-round node | `'thread' dresses a '\|sketch\|' segment or a round feature` |
| Bad `sheet:` | `'sheet' takes a size — a5…a0 (ISO) or a…e (ANSI) — and an optional portrait / landscape` + did-you-mean |
| `:segment` shadows a built-in point | `':left' is a built-in anchor — pick another name` |
| Unknown `:segment` | `no segment ':step' on 'body'` + suggestions |
| Duplicate `:segment` in one `draw:` | `':step' is already named in this 'draw:'` |
| Label on a mate | `a mate takes no label` |
| Mate on sheet content | `a mate seats geometry — '\|note\|' is sheet content` |
| `gap:` on a point mate | `a point mate coincides — 'gap' needs directed anchors (sides or named edges)` |
| Non-parallel mate directions | `mated anchors must face along one axis — 'a:left \|\| b:top' has no shared normal` |
| Over-constrained mate | `mate over-constrains 'X' — already positioned via 'A \|\| B'` |
| Mate within one part | `'a' and 'b' are features of one part — a part is rigid` |
| Mixed dim axes | `'a:left (-) b:top' mixes axes — anchor one axis` |
| `side:` off-axis | `a horizontal dimension stacks on top or bottom` / `a vertical dimension stacks on left or right` |
| Parallel `(<)` edges | `the angle's edges are parallel — they never meet` |
| Bad `tol:` | `'tol' takes a number, '+upper -lower', or a fit ident` |
| Bad `pattern:` | `'radial' needs count ≥ 2 and radius > 0` |
| `scale:` ≤ 0 | `'scale' must be > 0` |
| Chain past a label | `a text callout ends its statement — chain before it` |
| Mate in a flow scope | `a '\|row\|' places its own children — mates seat a drawing's` |
| Empty drawing | `a drawing needs at least one geometry child` |
An **unknown property name** is not currently an error — it is silently ignored; a
warning with a did-you-mean hint is deferred ([SPEC 23](#23-deferred)).
---
## 21. Grammar
```
file = [ stylesheet ] { drawn } # setup block, then drawn statements in source order
stylesheet = "{" { setup_item } "}" # the root's setup block; omit when empty
setup_item = decl | vardecl | binding | rule | define | comment | newline
decl = ident ":" values ";" # ';' optional before '}'
vardecl = css_var ":" values ";" # --name : value ;
binding = ident [ "(" [ ident { "," ident } ] ")" ] "=" value ";" # my_r = 5 ; scale(n) = (…) ;
rule = selector style # |box| { } , |table| |box| { } , .hot { } , #hero { }
define = "|" ident "::" ident "|" body # name :: base, optional children
node = ident_bars [ string ] [ classes ] [ style ] [ children ]
text = string [ style ] # bare content; a styleable leaf, never a box
ident_bars = "|" ( type [ "#" ident ] | "#" ident ) "|" # |type| , |type#id| , |#id|
type = ident
classes = "." ident { "." ident } # a worn class chain — .hot, .hot.loud
style = "{" { decl } "}" # declarations only
children = "[" { node | text | link } "]" # nodes, text, links — in source order
body = [ style ] [ children ] # define / container body
link = endpoints op [ endpoints ] { op endpoints }
[ string ] [ classes ] [ style ] [ label_block ] # the node tail, on a link head
op = link_op | draw_op
draw_op = "||" | "(-)" | "(o)" | "(<)" # mate, linear, round, angle (SPEC 15)
selector = sel_unit { sel_unit } # whitespace-separated = descendant
endpoint = ident { "." ident } [ ":" point ]
point = "top" | "bottom" | "left" | "right" # + corners, center, authored segments
# in a drawing scope (SPEC 15.2)
pen_item = call [ ":" ident ] # a draw: item — a pen call, optionally
# naming its product (point(): a station)
label_block = "[" { text } "]" # canonical labels — styleable text leaves
values = value_group { "," value_group } # comma only between list items
value_group = value { value } # space-separated scalars
group = "(" expr ")" # a math group — a number or point (SPEC 10.7)
css_var = "--" ident { "-" ident }
expr = { ident "=" expr ";" } value_expr [ "," value_expr ] # locals, then a value or a point
value_expr = operators, math library, a ternary, calls, groups — the grammar of SPEC 10.7
link_op = [ start_marker ] line [ end_marker ]
number = [ "+" | "-" ] ( digit+ [ "." digit+ ] | "." digit+ )
percent = number "%" # colour components only
hex = "#" hexdigit { hexdigit } # 3, 4, 6, or 8 hex digits
hexdigit = digit | "a"…"f" | "A"…"F"
string = '"' { char | escape } '"'
escape = "\" ( '"' | "\" | "n" | "t" )
comment = "//" { not-newline } newline
```
**Single-pass LL(1).** The stylesheet-first rule plus the bracket-and-bars vocabulary make
one token of lookahead enough — the first token of every statement already tells its kind:
in the stylesheet, `|…|` → a rule or (with an inner `::`) a define, `.name` → a class rule,
`#name` → an id rule, `--name :` → a variable, `ident :` → a root declaration, `ident =`
or `ident (…) =` → a binding; after it, a
drawn statement is a `node` (`|…|`), `text` (`"…"`), or — when a bare `ident` is followed by
a link-op, `&`, or a `.` path — a `link`. A **declaration** ends with `;` (its value may
span lines); a **statement** ends at a newline or `;`.
**Adjacency tells a `.class` from a path; a `:` tells a side.** A space before the `.`
makes it a worn class (`a .hot`), no space an endpoint path (`a.b`); the first class is
spaced from the identity, the rest of the chain glues (`.hot.loud`); a `:` after an
endpoint forces a side (`a:left`), distinct from the declaration `:` by position.
**Every layout reuses this grammar; drawing alone extends it.** Charts and sequences add
**no** lexer or parser grammar — they are nodes, declarations, and children, distinguished
by type name and by the scope's `layout` ([SPEC 13](#13-sequence), [SPEC 14](#14-charts)).
The `drawing` layout ([SPEC 15](#15-drawing)) adds exactly: the four `draw_op` tokens —
glued, like every link op; `||` is resolved in the parser from two **adjacent** pipes at
**operator position only**, so bars stay paired and selectors are untouched — the
**one-ended relaxation** (the right-hand endpoints may be omitted for `<-`, `*-`,
`>-`, `(<)`, and **must** be for the unary-only `(o)`; one token of lookahead
decides — after the op, an ident is an endpoint; a string, `.`, `{`, `[`, or
end-of-statement is the tail; the binary `(-)` and `||`
require both ends), the widened endpoint `point` set in drawing scope, the `(-)`
dimension-family `sel_unit` at a stylesheet statement head (a leading `(` there is
unambiguous — calls and groups appear only in value position), and the
`pen_item` form inside a `draw:` value. A call's `(` **glues to its name**; a
free-standing `(…)` is a math group and a free-standing `(-)`, `(o)`, or `(<)` an op
([SPEC 2](#2-lexical-syntax)) — so `move(-2, 5)` is a call, `(8 * 2)` a group, and
`pin (o)` a dimension, with no ambiguity. The pen calls,
`grid` / `radial`, and `hatch` are **call names**, contextual before `(` like `rgb` /
`repeat` ([SPEC 22](#22-reserved-words)).
---
## 22. Reserved Words
Because a type only ever appears in bars (`|box|`) and an id always wears a `#`, **type
names are free as ids and ids are free as type names** — `|block#oval|` is fine, and
`block -> oval` is two ordinary nodes. A small set of words stays reserved:
- **`node`, `link`,** and the structural class names **`text`, `marker`, `canvas`,
`scene`, `cut`:** not instantiable types — `node` is the umbrella concept (write
`|block|` for the bare box), links are drawn by operators and styled by `|-|` (`|link|`
is an error), and a **define** may not take one of these (its generated `.lini-<name>`
would collide with a built-in SVG class — `|-|` lowers to the reserved `.lini-link`).
The **`.lini-*` class prefix** is reserved: desugar generates the type classes
(`.lini-block`, `.lini-box`, `.lini-<define>`), so a user class may not begin `lini-`.
User classes are emitted `.lini-style-<name>`.
The side names **`top`, `bottom`, `left`, `right`** are **not** reserved — they are
keywords only after an endpoint's `:` (`a:left`), so a node may be named `|box#left|`.
Single quotes (`'`) are reserved and are not strings.
Value keywords are **contextual**, not reserved as ids — `flow`, `grid`, `sequence`,
`chart`, `pie`, `row`, `column`, `radial`, `start`, `center`, `end`, `stretch`, `evenly`,
`none`, `auto`, `orthogonal`, `straight` mean their keyword only after the property that
expects them. The layout type names (`chart`, `pie`, `axis`, `band`, `mark`, `bars`, `dots`,
`bubble`, `slice`, `area`, `line`, and the sequence `loop`, `opt`, `alt`, `else`, `note`)
are built-in types like `box` — protected from a define shadowing them, free as ids; so
are the drawing types (`drawing`, `sketch`, `hole`, `centerline`, `pitch-circle`,
`balloon`, `breakline` — [SPEC 15](#15-drawing)).
Function names `rgb`, `rgba`, `hsl`, `repeat` are reserved only before `(` — as are
`hatch`, `grid` / `radial` (in `pattern:`), and the pen calls (`move`, `left`, `right`,
`up`, `down`, `line`, `angle`, `arc`, `curve`, `fillet`, `chamfer`, `circle`, `close`)
inside a `draw:` value.
In **link-operator position** the marker glyphs `+` (one) and `o` (zero) are contextual —
they compose the ER cardinality marker ([SPEC 9](#9-links)) and mean nothing elsewhere;
`o` is valid only next to a max glyph (`-o<`, `+o-`, …), so it never collides with an id or
the round measuring op `(o)` (delimited by parens). A leading `+` not followed by a digit
starts a cardinality op, mirroring `-`. The digit `0` is **not** part of any operator — a
hollow endpoint is `marker-end: circle`, never `-o`.
Inside a `(…)` expression ([SPEC 10.7](#107-expressions--functions)), `pi`, `e`, and the
sample parameters `u` / `x` are keywords, and the math-function names (`sin`, `exp`,
`min`, …) are reserved before `(` — all contextual to the expression, free as ids
elsewhere. The backtick `` ` `` is unused and reserved.
---
## 23. Deferred
Named in the language, not built yet; the syntax is stable.
**Core**
- `routing: curved` — the curved link strategy ([SPEC 9](#9-links); `orthogonal` and `straight`
are built).
- **a bare hollow-circle endpoint operator** — `o` spells zero only next to a max glyph
(`-o<` / `-o+`); a standalone hollow endpoint is the paint `marker-end: circle`
([SPEC 7](#7-nodes)), so `o` never needs to stand alone.
- `stroke-style: wavy` on **closed** primitives (`|line|` waves — it backs an async
sequence message; a hex / oval / rect outline does not yet).
- **gradient fills on text** — gradients fill nodes today ([SPEC 10.3](#103-gradients)).
- `radius` on non-rect primitives (hex / diamond / slant / poly).
- numeric `font-weight` (`100…900`); a solid (`fill`-weight) icon variant (the built-in set
is Phosphor duotone, behind a default-on `icons` cargo feature).
- embedded font metrics — the monospace default keeps the estimate close; a proportional
`font-family` override is approximate until then.
- **an unknown-property warning + a "did you mean" property-name hint** — an unrecognised
property is silently ignored today ([SPEC 16](#16-property-ledger--support), [SPEC 20](#20-errors)).
- `aria-label`.
**Tables & entities**
- arbitrary per-cell backgrounds in a `|table|` — only the header and any `|footer|` cells
carry a fill today; a body cell that needs one is a `|block|` ([SPEC 8](#8-templates)).
**Sequences** ([SPEC 13](#13-sequence)) — fragments `par` (parallel, with an `|and|` separator),
`break`, `critical`, and `ref`; participant grouping; found / lost messages and
create / destroy lifelines; explicit activation spans; message auto-numbering;
dividers / delays (`==` / `...`); and an `|actor|` stick-figure primitive (an actor is
`|icon|` today).
**Charts** ([SPEC 14](#14-charts))
- **bands / marks in `row` and `radial` charts** — they render in `column` direction today.
- a general per-axis `labels:` (explicit tick text for any axis) — `categories:` sets the
x-axis labels today.
- **gauge** (a partial arc for one value); **stacked areas** (`bars: stacked` extended to
`|area|`); polar-area **circular gridlines** and a configurable radial **start angle /
direction** (the polygon web and top-clockwise are the defaults).
- per-slice **explode**, **on-slice value / percent labels**, and a **centred total** in a
donut hole; **per-segment styling** (a style list mirroring a segmented `fn:`).
- **time scale** (date domains with calendar-aware ticks); **multi-ring pie / sunburst**;
**per-datum paint styling** (a parallel paint list over `data:` — today, overlay a
`|mark|`).
**Drawings** ([SPEC 15](#15-drawing))
- **per-kind dimension selectors** — `(o) { }` / `(<) { }`; the family selector `(-) { }`
reaches every dimension today ([SPEC 4](#4-selectors-cascade--specificity), [SPEC 15.6](#156-dimensions)),
and a leader-specific selector under `|-|` is deferred too (YAGNI).
- **aligned (point-to-point) dimensions** — today a dim is horizontal or vertical.
- **per-copy pattern anchors** (`bolt.2`) and pitch dims between copies — the callout
count and a `\|pitch-circle\|`'s own `(o)` cover the common cases.
- **fan leaders** — `a & b <- "2× R5"`, one note with two leaders.
- **`explode:`** — scale every directed mate's separation along its normal for exploded
views; unmated overlaid children stay put (overlay composes one part, mates relate
parts — only relationships explode). Balloons follow their parts.
- **authored-segment twins** — a `mirror:` / `pattern:` copy of a `:segment` is unaddressable
(the name reads the drawn original; the unary mirrored readings cover the
turned-profile cases).
- **routed links to authored anchors** — `a -> b:port` in a flow / grid diagram needs a
[ROUTING.md](ROUTING.md) contract extension (ports and Law 2 are side-based).
- **repeated-segment counting** — one `:segment` on several corners auto-prefixing `4× R3`,
as `pattern:` does for features; today, type it.
- **GD&T** — feature-control frames, boxed datums, surface finish: note types over
`\|table\|` / `\|note\|` with a built-in glyph set named by ident, drawn as paths like
icons — the designed direction, no new grammar. Today: `body:seat >- "A"`,
`face *- "Ra 1.6"`.
- **hole variants** — counterbore and countersink (threads are built — `thread:`,
[SPEC 15.3](#153-the-sketch-pen), [SPEC 15.4](#154-features-holes--patterns)).
- **view machinery** — projection lines between views, detail circles ("VIEW A"),
cutting-plane arrows (A–A), cross-view alignment; today, composed by hand.
- **angled break lines** and a scope-level `break:` on the `\|drawing\|` itself; a
`break:` station **through a `curve()`** (lines and arcs clip exactly today — move the
stations off the cubic) and `break:` on non-sketch geometry (draw the profile with
the pen).
- **`fillet` / `chamfer` against a curved segment** — today the modifiers join two
straight runs (an arc is already tangent-friendly; draw it with the radius you
want).
- **dim-line breaks / halos** where annotations cross geometry; the ASME
text-in-a-broken-line diametral form and a horizontal-text knob (ISO aligned is the
built-in).
- an ambient **`w` / `h`** bound to a node's own size (circular against auto-sizing
today — a named constant covers the workflow, [SPEC 10.7](#107-expressions--functions)).
- **physical-size emission** — an SVG `width` / `height` in real mm for true-scale
prints.
- **balloon auto-numbering and auto-BOM** from the scene's parts.
- **`\|mark\|` / `\|note\|` in charts** — data-coordinate placement (`at:`).
---
## 24. Examples
**A scene — grid, defines, groups, nested links:**
```
{
layout: grid; columns: repeat(3); gap: 40; padding: 20;
fill: --bg; clearance: 12; // clearance cascades to every link
--accent: #0a84ff;
.loud { stroke: red; stroke-width: 2; } // a link (or node) class — one vocabulary
|room::group| {
gap: 8;
} [
|box#inlet| "Inlet"
|box#outlet| "Outlet"
inlet -> outlet "flows" // an internal link, per-instance
]
}
|treat#bowl| "Bowl of oats"
|box#water| "Water"
]
|room#closet| "Closet" { cell: 1 2 }
|room#fridge| "Fridge" { cell: 2 2 }
cat:right -> kitchen.bowl:left "watches"
kitchen.water -> closet .loud
closet.outlet -> fridge.inlet "restocks"
```
**Table, entity, and shorthand:**
```
"Apple" "12" "fresh"
"Mango" "3" "ripe"
]
users -o< orders "places" // zero-or-many — [min][max] = hollow ring + crow's foot
cat -> dog -> bird // 3 implicit boxes, 2 links
fox & owl -> mouse // fan-in
frog ~> pond // wavy arrow
```
**A sequence — a login flow:**
```
{ layout: sequence }
|box#api| "API"
|cyl#db| "Sessions"
user -> browser "click login"
browser -> api "POST /login"
api -> db "lookup"
db --> api "record"
browser --> user "dashboard"
|else| "wrong"
api --> browser "401"
]
|note| "rate-limited" { over: api db }
```
**Charts — bars, a formula with a band, and a pie:**
```
|bars| "2.3 kW" { data: 7 13 20; fill: --amber }
]
|axis#x| "Speed (mm/s)" { side: bottom; range: 0 133 }
|area| "Pressure" { axis: bar; fn: `x <= 93 ? 1000 : 1000 - 319*((x-93)/40)`; fill: --teal }
|band| { span: 93 133; axis: x; fill: --red }
|mark| "1000 bar @ 93" { at: 93; axis: x; color: --muted }
]
|slice| "SEO" { value: 30 }
|slice| "Direct" { value: 30 }
]
```
**A radar (radial chart) and labelled scatter:**
```
|line| "Scout" { data: 5 4 2 3 5 }
|area| "Cruiser" { data: 3 3 5 4 2; fill: --teal }
]
|axis| "score %" { side: left }
|line| "GLM-5.2" { data: 35 63, 42 72, 84 75; tags: "Base" "High" "Max"; marker: circle; tooltip: always }
]
```
**Drawings — a broken tie bar, a bushing in section, a mated assembly, and a
sheeted screw** ([SPEC 15](#15-drawing)):
```
{ layout: drawing; scale: 3; unit: "mm" }
right(40):m20 point():a right(260) chamfer(1.5) down(10);
revolve: x-axis; // a turned part: axis + edge lines
thread: m20 1.5; // minor line + thread-end line
break: -80 60; // cut the boring middle from the view
}
bar:left (-) bar:right { side: bottom } // → 300 mm — true, across the break
bar:left (-) bar:a { side: top } // → 40 mm — ':a' is a point() station
bar:m20 (o) { side: left; tol: h6 } // → ⌀20 h6 — doubled about the axis
bar:m20 <- { side: top } // → M20×1.5 — the thread composes its spec
```
```
{
layout: drawing; scale: 3;
mirror: x-axis; // → both walls (duplicated)
}
|rect#bore| { width: 60; height: 16; fill: --bg; stroke: none } // the bore punches
|centerline| { points: -34 0, 34 0 } // duplicated subpaths add no auto axis
bore:top (o) { side: right } // → ⌀16 — written first, the inner row
body:top (o) { side: right } // → ⌀36 — stacks outside it
body:left (-) body:right { side: bottom } // → 60
```
```
{
gap: 24;
draw: move(-90, 0) up(23) right(60) up(6) right(60) down(6) right(60) down(23);
revolve: x-axis;
}
revolve: x-axis;
}
barrel:left (-) nozzle:right { side: bottom } // → the overall length, as seated
b1 -* barrel
b2 -* nozzle
]
"1" "Barrel" "1"
"2" "Nozzle" "1"
]
```
```
draw: move(0, 0) up(6.5) chamfer(0.8) right(8):head down(2.5):k right(12)
point():v right(28):m8 chamfer(1) down(4);
revolve: x-axis;
// a turned part
thread: m8 1.25;
// the threaded run
} [
|hidden#socket| {
// the hex socket, dashed
draw: move(0, 3) right(4) line(3, -3);
mirror: x-axis;
}
]
screw:head (o) { side: left; }
// → ⌀13
screw:left (-) screw:k { side: bottom; }
// → 8 — K, the head
screw:k (-) screw:right { side: bottom; }
// → 40 — L, under the head
screw:v (-) screw:right { side: top; }
// → 28 — the thread length
screw:m8 <- { side: top; }
// → M8×1.25 — composed by the thread
]
|oval| { width: 11.4; height: 11.4; }
// the head, end-on
|hex#socket| { width: 7; height: 6; }
// the socket, visible here
socket:left (-) socket:right
]
"Scale" "1.5:1"
"Units" "mm"
]
]
```