reladraw 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SYNTAX.md CHANGED
@@ -1,4 +1,4 @@
1
- # Syntax reference — v0
1
+ # Syntax reference — 0.3.0
2
2
 
3
3
  What the language accepts. The parser, resolver and SVG renderer implement all of it; the sections at the end record what is defective, unchecked or undecided.
4
4
 
@@ -8,97 +8,172 @@ The worked example is `examples/arch.reladraw`, transcribed from the reference r
8
8
 
9
9
  One statement per line. No multi-line statements, no line continuations, no blocks.
10
10
 
11
- Blank lines and `//` comments are ignored. A comment runs to the end of the line and may trail a statement, so `box a "Docker" // the one that matters` is fine. Indentation is ignored entirely — a formatter may add it for readability, and stale indentation cannot change what a file means.
11
+ Blank lines and `//` comments are ignored. A comment runs to the end of the line and may trail a statement, so `node a "Docker" // the one that matters` is fine. Indentation is ignored entirely — a formatter may add it for readability, and stale indentation cannot change what a file means.
12
12
 
13
- A lone `/` is an ordinary character rather than the start of a comment, and `#` is ordinary too — it opens a hex colour. Comments were spelled `#` in an earlier version and moved to `//` so that `fill: #14532d` could be written the way every other tool writes a colour.
13
+ A lone `/` is an ordinary character rather than the start of a comment, and `#` is ordinary too — it opens a hex color. Comments were spelled `#` in an earlier version and moved to `//` so that `fill: #14532d` could be written the way every other tool writes a color.
14
14
 
15
- A statement is a positional head followed by optional `key: value` attributes. Attributes begin at the first token ending in a colon, which is the only rule a parser needs to tell the two apart. Everything positional therefore comes first — name, text and placements, in that order — and once an attribute has appeared nothing positional may follow it.
15
+ A statement is a positional head — the keyword, a name and a text — followed by attributes and placements in any order. A token ending in a colon opens an attribute and nothing else does, so the two never have to be told apart by position.
16
16
 
17
17
  ```
18
- box server "My Server Machine" left of dropbox gap: wide icon: desktop
19
- ^ ^ ^ ^
20
- | | | attributes
21
- | | placements
22
- | text
23
- name
18
+ node server "Home Server" left of cluster gap: wide badge: desktop
19
+ ^ ^ ^ ^
20
+ | | | attributes
21
+ | | a placement
22
+ | text
23
+ name
24
24
  ```
25
25
 
26
+ `node aside "…" shape: none below dumps (gap: tight) overlap: allow` is one statement with a placement in the middle of its attributes, and reads the same as any other arrangement of those three. Until 0.3.0 placements had to come first; that rule existed only because a bare `gap:` between two placements could not be told from the node-wide default, and [bracketing a gap onto its placement](#a-gap-belongs-to-the-placement) removed the ambiguity that made it necessary.
27
+
26
28
  Attribute values are a single bare word unless quoted. No commas between attributes.
27
29
 
28
30
  ## Nodes
29
31
 
30
32
  ```
31
- box <name> ["<text>"] [<placement> ...] [attributes]
33
+ node <name> ["<text>" [(<text properties>)]] [<placement> | <attribute>] ...
32
34
  ```
33
35
 
34
36
  `name` identifies the node and must be unique. `text` is what appears inside it, with ` / ` — a slash with whitespace on both sides — marking a line break.
35
37
 
36
- The text is optional, and a box without it is labelled with its own name:
38
+ The text is optional, and a node without it takes its own name as its text:
37
39
 
38
40
  ```
39
- box parser
40
- box resolver right of parser
41
+ node parser
42
+ node resolver right of parser
41
43
  ```
42
44
 
43
- draws two boxes reading "parser" and "resolver". A dotted name shows its last segment only — `box server.docker` reads "docker", because the containment is already drawn and repeating it in the label says nothing new.
44
-
45
- Write `""` for a box that is deliberately blank: an invisible container, a node that is nothing but its icon, a glyph body. The empty string is the way to say a box has no label, and leaving the text out entirely is the way to say the name is the label.
46
-
47
- A name written this way is doing two jobs, so renaming such a node changes the picture. That is the trade, and the escape from it is to state the label.
45
+ draws two nodes reading "parser" and "resolver". A dotted name shows its last segment only — `node server.docker` reads "docker", because the containment is already drawn and repeating it in the text says nothing new.
48
46
 
49
- The whitespace is part of the marker, not decoration. A slash inside a word is an ordinary character, so `TCP/IP`, `16/9`, `I/O` and `https://example.com/x` all render as written. An earlier version broke on every `/` and quietly tore those labels in half.
47
+ Write `""` for a node that is deliberately blank: an invisible container, a node that is nothing but its badge. The empty string is the way to say a node has no text, and leaving the text out entirely is the way to say the name is the text. A node drawn as a picture is the one exception — there, leaving the text out means no text, and `""` says the same thing.
50
48
 
51
- For a label that wants a spaced slash and no break, `\/` escapes it: `"Before \/ After"` is one line. The escapes are `\"`, `\\` and `\/`.
49
+ A name written this way is doing two jobs, so renaming such a node changes the picture. That is the trade, and the escape from it is to state the text.
52
50
 
53
- `width: <n>` folds the text at word boundaries every `n` characters, on top of whatever ` / ` already breaks. It is a character count, not a distance, so it says how much text fits on a line and never where anything sits. It is how you make a block of text narrow and tall so it can sit snugly beside something, rather than wide and short so it cannot.
51
+ The whitespace is part of the marker, not decoration. A slash inside a word is an ordinary character, so `TCP/IP`, `16/9`, `I/O` and `https://example.com/x` all render as written. An earlier version broke on every `/` and quietly tore those texts in half.
54
52
 
55
- `size: small | normal | large` sets how big the text is, and works on a box, a note or a link label. The sizes are named for the reason gaps are named: a number would be typography by coordinate, stale the moment the document is set at another size, and silent about *why* one piece of text is smaller than another. An unrecognised value is an error naming it.
53
+ For a text that wants a spaced slash and no break, `\/` escapes it: `"Before \/ After"` is one line. The escapes are `\"`, `\\`, `\/` and `\[`.
56
54
 
57
- Every kind of text has a default, and `size:` overrides it exactly as `fill:` overrides the theme's colour. Only `note` defaults to anything other than `normal`, and it defaults to `small`.
55
+ Everything else a text has to say about itself it says in brackets after it — see "The text and its brackets" below.
58
56
 
59
57
  Containment is a dotted name. A node named `server.docker` is inside `server`. The parent must be declared before the child.
60
58
 
61
59
  ```
62
- box server "My Server Machine" left of dropbox
63
- box server.mirror "\\"important\\" mirror"
64
- box server.deploy "services deploy dir"
60
+ node server "Home Server" left of cluster
61
+ node server.mirror "shared folder / mirror"
62
+ node server.deploy "deploy dir"
65
63
  ```
66
64
 
67
65
  A container is sized by its contents. Children stack vertically in written order unless a child carries a placement of its own.
68
66
 
69
- A container with empty text and no fill or stroke takes up no room of its own and draws nothing. It exists so that everything inside it can be placed against as a single shape:
67
+ A container with empty text and no fill or border takes up no room of its own and draws nothing. It exists so that everything inside it can be placed against as a single shape:
68
+
69
+ ```
70
+ style invisible fill: none border: none
71
+
72
+ node cluster "" style: invisible
73
+ node cluster.hub "Cloud sync"
74
+ node cluster.desktop1 "Desktop 1" above-left of cluster.hub
75
+
76
+ node server "Home Server" left of cluster
77
+ ```
78
+
79
+ Here the server clears the whole cluster. Placed `left of cluster.hub` instead, it would only clear the hub, and the machines around the hub would be free to grow into it.
80
+
81
+ A container's text can say where in the node it goes — see "The text and its brackets" below.
82
+
83
+ ### How the contents sit
84
+
85
+ A container is as wide as the wider of its title and its contents. When the title wins there is slack, and every child sits at the smallest position its constraints allow — so all of it ends up on the right and the column reads as ragged inside a node whose own text may well be centered. `contents:` is what says otherwise:
86
+
87
+ ```
88
+ node svc "Ingestion and enrichment pipeline" contents: (widths: match, align: center)
89
+ ```
90
+
91
+ `widths:` sizes the children and `align:` positions the block of them. They are independent, and the bracket is what keeps the levels apart: `contents:` is a property of the node, `widths:` a property of the contents.
92
+
93
+ | written | means |
94
+ |---|---|
95
+ | nothing | natural widths, ranged left |
96
+ | `contents: (widths: match)` | every child as wide as the widest, ranged left |
97
+ | `contents: (widths: fill)` | every child spans the whole content band; `align` then has no slack to work in. A single column only: children side by side cannot all be the full width, and are refused by name |
98
+ | `contents: (align: center)` | natural widths, the block of them centered |
99
+ | `contents: (widths: match, align: center)` | the tidy centered column |
100
+
101
+ `align:` takes `left`, `center` and `right` here, the same three words it takes in a text's brackets. Widths are the only thing `widths:` touches and position the only thing `align:` touches; neither reaches a child's own contents.
102
+
103
+ **Changed 2026-09-20.** This replaced `align: widths`, which is now an error naming its substitution. That key was a size operation wearing an alignment's name, and its value set had one member — a flag in a property's clothes. With it gone, `align` means one thing everywhere in the language: how a block's lines or children range against each other.
70
104
 
105
+ ### The text and its brackets
106
+
107
+ Everything a text says about itself rides in brackets after it. The keys are `color`, `size`, `wrap`, `align` and `at`, in any order, with the comma optional punctuation:
108
+
109
+ ```
110
+ node docker "Docker" (at: bottom-center, align: center) below deploy
111
+ node aside "Written by a nightly cron job." (size: small, wrap: 30) shape: none
112
+ ```
113
+
114
+ They are bracketed onto the text rather than written among the node's attributes for the same reason [a gap is bracketed onto its placement](#a-gap-belongs-to-the-placement): they modify that one thing, and the brackets make the scope visible instead of leaving it to be inferred from what happens to sit nearby. What is left at the top level is then about the node — its body, its color, where it goes.
115
+
116
+ An edge's text takes the same brackets, less `at`: a node's text sits somewhere in a box and an edge's rides at the middle of its line, so there is no position to name.
117
+
118
+ ```
119
+ edge a -> b "rclone" (color: muted, size: small)
71
120
  ```
72
- style invisible fill: none stroke: none
73
121
 
74
- box hub "" style: invisible
75
- box hub.dropbox "Dropbox"
76
- box hub.computer1 "Computer 1" above-left of hub.dropbox
122
+ A **style** has no text of its own for a bracket to hang off, so it hangs the bracket off a key instead:
77
123
 
78
- box server "My Server Machine" left of hub
79
124
  ```
125
+ style aside text: (size: small, color: muted)
126
+ ```
127
+
128
+ Bundling a text's properties into a style is how they come to mean something: `style aside` applied to several nodes says those are the same kind of remark, which `(size: small)` written out at each of them does not.
80
129
 
81
- Here the server clears the whole cluster. Placed `left of hub.dropbox` instead, it would only clear Dropbox, and the machines around Dropbox would be free to grow into it.
130
+ `color` takes a color written as the viewer will receive it — `#8b8b8b`, or any CSS color — or the one reserved word `muted`, which means the theme's secondary text color and so survives a change of theme.
82
131
 
83
- A container's label can say where in the box it goes, in brackets on the label itself:
132
+ `size: small | normal | large` sets how big the text is set. The sizes are named for the reason gaps are named: a number would be typography by coordinate, stale the moment the document is set at another size, and silent about *why* one piece of text is smaller than another. Every kind of text has a default and `size` overrides it, exactly as `fill:` overrides the theme's color; only `shape: none` defaults to anything but `normal`, and it defaults to `small`.
133
+
134
+ `wrap: <n>` folds the text at word boundaries every `n` characters, on top of whatever ` / ` already breaks. It is how you make a block of text narrow and tall so it can sit snugly beside something, rather than wide and short so it cannot. The number is a count of characters and not a distance: it says how much fits on a line and nothing about where anything sits.
135
+
136
+ `at: <position>` is where the block of text sits in the node, named from [the nine positions](#against-a-part-of-a-node) — `top-left`, `bottom-center`, `center` and the rest. In a container the vertical half of the word says which end of the node the text's band is at, and the contents take the other end; a container whose text names neither end is an error, since there would be no other end left for the contents. The default is `top-left` in a container and `center` in a leaf.
137
+
138
+ `align: left | center | right` is a different question and stays one: it says how the block's own *lines* range against each other, which matters whenever they are of unequal length and is not the same as where the block is. Its default is `left` in a container and `center` in a leaf.
139
+
140
+ Where the node is exactly the size of what it holds — which is most leaves, since a leaf is sized from its own text — there is no slack and `at` changes nothing. It bites where there is some: an icon placed `inside <node> right` is two lines tall, so a one-line text beside it has room to sit at either end of. A badge does not give it that room, because a badge is placed against the text and goes wherever the text goes.
141
+
142
+ #### Markup in a text
143
+
144
+ A word or a stretch of a text can borrow the look of a style:
84
145
 
85
146
  ```
86
- box docker "Docker" (at: bottom, align: centre) below deploy
147
+ style dim text: (color: muted)
148
+
149
+ node pc.files "shared folder / [dim]synced[/dim]"
87
150
  ```
88
151
 
89
- `at: top | bottom` says which end of the box the label sits at; the contents take the other end. `align: left | centre | right` says how the text sits across it. The defaults are `top` and `left`, and the comma is optional punctuation.
152
+ The opener names a **style**, never a color, so the marked words borrow a meaning the file already has instead of restating a value that goes stale the day the thing it means is recolored. A style that says nothing about text is an error naming the missing part, and so is a name no style answers to.
90
153
 
91
- The two are independent and neither implies the other. `(at: bottom)` on its own is an ordinary label that happens to be at the bottom.
154
+ The closer repeats the name. `[/dim]`, not `[/]` — a reader should never have to count openers, and a mismatched close is then refused by name. Marks do not nest.
92
155
 
93
- They are bracketed onto the label rather than written among the node's attributes for the same reason [a gap is bracketed onto its placement](#a-gap-belongs-to-the-placement): they modify that one thing, and the brackets make the scope visible instead of leaving it to be inferred from what happens to sit nearby.
156
+ A mark may cross a line break or a fold, so `"[dim]placement is / an output[/dim]"` quiets both lines. That is what it buys over the `subtext:` it replaced, which could only quiet everything after the first line and did it by counting, in a style somewhere else in the file, where a reader of the text could not see it.
94
157
 
95
- A leaf's label is centred in its box with nothing to sit clear of, so it takes no modifiers and saying otherwise is an error. So is putting them on a note, which has no box at all.
158
+ `[` opens a mark, so a literal one is written `\[`:
96
159
 
97
- `align: widths` on a container widens every direct child to match the widest of them, so a stack of boxes with labels of different lengths draws as a column with one edge rather than a ragged one. It is the only value the key accepts; anything else is an error. Widths are the only thing it touches — it never moves a child.
160
+ ```
161
+ node sizes "sizes \[small, normal, large]" shape: none
162
+ ```
98
163
 
99
- ### Icons
164
+ ### The body
100
165
 
101
- `icon: <name>` puts a small glyph beside the box's label. There are seven:
166
+ Every node has a *body*: the thing that is drawn where the node is. Two keys name it, and each names one part.
167
+
168
+ `shape: <name>` is the outline the node is drawn with.
169
+
170
+ | Value | What it draws |
171
+ | --- | --- |
172
+ | `rectangle` | The plain rounded box. The default, so nothing has to say it. |
173
+ | `document` | The same box with its top-right corner folded — the flowchart symbol saying *this is an artifact, not a process*. |
174
+ | `none` | No outline, no fill, no padding. The node is its text and nothing else. |
175
+
176
+ `icon: <name>` is a picture the node is drawn **as**, with no box at all.
102
177
 
103
178
  | Name | What it means |
104
179
  | --- | --- |
@@ -107,60 +182,66 @@ A leaf's label is centred in its box with nothing to sit clear of, so it takes n
107
182
  | `laptop` | A portable machine. |
108
183
  | `package` | Something stored as a whole rather than run: an archive, a bucket, a sync root. |
109
184
  | `cubes` | Several interchangeable units of the same kind. |
110
- | `instance` | One of them. |
185
+ | `cube` | One of them. |
111
186
  | `database` | A store queried rather than read as files. |
112
187
 
113
- `cubes` draws three whatever the number, because it is the symbol for "several" and not a count. Where the number matters — where one of them is the end of an arrow — they are separate nodes, and `shape: instance` is how you draw those.
114
-
115
188
  ```
116
- box ext_hd "External HD" icon: disk
189
+ node dump "db dump / (app 1)" shape: document
190
+ node aside "Written by a nightly cron job." (wrap: 30) shape: none
191
+ node svc icon: cube
117
192
  ```
118
193
 
119
- A name says what the thing *is*, never what the picture looks like, for the same reason `gap: wide` beats `gap: 110`: naming the meaning is what lets the drawing be improved later without every diagram that uses it changing sense. An unrecognised name is an error listing the whole set, rather than a box that quietly draws no icon — you would go looking for the mistake in the wrong place.
194
+ A node has one body, so writing both keys is an error naming both.
120
195
 
121
- The set is small on purpose, and it is not the same trade a drawing tool makes. There you pick a shape out of a visual palette and hundreds are browsable; here you type the word from memory, which caps the useful vocabulary at what fits in a head. Adding your own is not possible yet — see "Not built yet".
196
+ The `document` fold is worth having because a shape is a second channel alongside color, and a stronger one. A fill means whatever you assigned it and a reader has to learn it from the diagram; a folded corner has meant "a document" for as long as there have been flowcharts, and reads with no legend. Most diagrams lose the difference between a thing that runs and a thing that is produced, because every node is a rectangle. `circle` and `diamond` will join these when a diagram asks for them.
122
197
 
123
- The glyph is two lines of the label tall, so it follows `size:` down and up with the text, and it sits at the top of a container beside the title and centred in a leaf beside the label. There is nothing to write about where it goes or how big it is. It takes a column of its own, so the box grows to hold the label and the icon side by side and one never runs under the other.
198
+ A name says what the thing *is*, never what the picture looks like: naming the meaning is what lets the drawing be improved later without every diagram that uses it changing sense. `document`, not `folded-corner`. The one place that rule stops is a picture with no single meaning — the cube stands for a container in one diagram, a VM in another, a service in a third — which is why it is called `cube` and not `instance`. An unrecognized name is an error listing the whole set, rather than a node that quietly draws nothing.
124
199
 
125
- `icon` is appearance, so a style can carry one and every store in a diagram then looks alike without the word being written more than once:
200
+ The icon set is small on purpose, and it is not the trade a drawing tool makes. There you pick a shape out of a visual palette and hundreds are browsable; here you type the word from memory, which caps the useful vocabulary at what fits in a head. Adding your own is not possible yet — see "Not built yet".
126
201
 
127
- ```
128
- style store fill: #142814 stroke: #486544 icon: database
129
- box records "Records" style: store
130
- ```
202
+ `cubes` draws three whatever the number, because it is the symbol for "several" and not a count. Where the number matters — where one of them is the end of an arrow — they are separate nodes, each with `icon: cube`.
203
+
204
+ A node drawn as a picture is an ordinary node in every other way. It takes placements, it takes edges and sides, other nodes keep clear of it. A [badge](#badges) is one of these, written for you beside a node's text. It cannot contain anything, though, and a picture with children is an error, as is a `shape: none` node with children — neither has a box for anything to go inside.
131
205
 
132
- A box with no label at all is exactly the icon and its padding, which makes an icon usable as a marker and not only as a title-block ornament. A note cannot take one, and says so: an icon decorates a box, and a note has no box.
206
+ **A picture with no text of its own shows none.** Everywhere else a node with no text is labelled with its name, because `node a` and `node b right of a` mean the two boxes to read "a" and "b". A picture usually *is* the statement, so the default flips: `node svc icon: cube` draws the cube and no caption, and a row of five of them does not come out reading a, b, c, d, e. Write the text if you want one.
133
207
 
134
208
  Icons are drawn from path data inside the tool, never from a font or a linked file. The output is a standalone SVG and has to stay one — an icon font renders as blank boxes on a machine that does not have it, and a linked image has to travel beside the file.
135
209
 
136
- ### Shapes
210
+ ### Badges
137
211
 
138
- `shape: <name>` says what a node is drawn as. It answers one question, and the answer is either a different outline for the box or a glyph standing where the box would be.
212
+ `badge: <name>` puts one of the same pictures beside the node's text, as a small mark on a node that keeps its own body.
139
213
 
140
214
  ```
141
- box dump "pg_dump output / (DB 1)" shape: document
142
- box svc "" shape: instance
215
+ node drive "External HD" badge: disk
143
216
  ```
144
217
 
145
- `box` is the plain rectangle and is what you get by saying nothing. `document` is the same box with its top-right corner folded — the flowchart symbol saying *this is an artifact, not a process*.
146
-
147
- That distinction is worth having because it is a second channel alongside colour, and a stronger one. A fill means whatever you assigned it, and a reader has to learn it from the diagram; a folded corner has meant "a document" for as long as there have been flowcharts, and reads with no legend. Most diagrams lose the difference between a thing that runs and a thing that is produced, because every node is a rectangle.
148
-
149
- Any icon name is also a shape, and then the node *is* the glyph: no outline, no fill, no padding, and its size is the picture's rather than its label's. A label goes underneath it.
218
+ **`badge:` is a shorthand, and this is what it stands for:**
150
219
 
151
220
  ```
152
- box services "" fill: none stroke: none
153
- box services.web "web" shape: instance
154
- box services.api "api" right of services.web gap: tight shape: instance
221
+ node drive "External HD"
222
+ node drive.badge icon: disk right of drive text
155
223
  ```
156
224
 
157
- **`icon:` decorates a box; `shape:` replaces it.** The test is whether the node still sizes itself from its label — a `document` does, a glyph does not. The same artwork can serve both, as a corner ornament on one node and as another node's whole body.
225
+ A child drawn as a picture, placed beside its parent's text. Everything a badge does follows from that line and from the rules for [a child against its own parent](#against-its-own-parent) — there is no rule of its own:
158
226
 
159
- A glyph body is an ordinary node in every other way. It takes placements, it takes links and sides, other boxes keep clear of it. That is why it exists rather than being an icon: an icon cannot be the end of an arrow. It cannot contain anything, though, and a glyph with children is an error — a picture is not a box.
227
+ - The node grows to hold the text and the badge side by side, because a node grows to hold its children.
228
+ - On a leaf the text and the badge center in the box as one group. On a container the badge sits beside the title in the band, and the band is as tall as the two together.
229
+ - The picture is two lines of the node's text tall, so it follows `(size: …)` down and up with the text.
230
+ - With no text at all — `node flag "" badge: disk` — the badge sits where the text would have been, with no gap beside it, so the node is exactly the badge and its padding. That makes a badge usable as a marker and not only as a title-block ornament.
231
+ - A text sent to one end with `at:` does not take the badge with it. A leaf is as tall as the badge, the badge is centered on the text, and so the text sits in the middle. To give a text room to move, put the picture against a side instead — see `at:` under [The text and its brackets](#the-text-and-its-brackets).
160
232
 
161
- Shapes are named for what a node is, never for the geometry: `document`, not `folded-corner`. Same rule as the icon names, and for the same reason. An unrecognised name is an error listing what is available.
233
+ The child is called `<node>.badge` and can be named like any other: `below drive.badge`, or an edge to it. Declaring your own `drive.badge` beside a `badge:` is an error naming both ways out. Something else placed `right of drive text` is placed exactly as the badge is, so the two form [one list](#several-nodes-saying-the-same-thing), badge first.
162
234
 
163
- The fold is a fixed size rather than a fraction of the box, so it looks the same on a narrow node and a wide one. Sizing it proportionally is what makes it vanish on a long label.
235
+ A badge in the far corner of a container is not what the shorthand says. Write the child yourself, `node drive.mark icon: disk inside drive top-right`.
236
+
237
+ **A badge decorates a node; `icon:` replaces its body.** The test is whether the node still sizes itself from its text: a badged node does, a picture does not. The same artwork serves both. Only a box can wear a badge. On a picture or a `shape: none` node the word is an error, since neither has a box to grow around it.
238
+
239
+ `badge` is appearance, so a style can carry one and every store in a diagram then looks alike without the word being written more than once:
240
+
241
+ ```
242
+ style store fill: #142814 border: #486544 badge: database
243
+ node records "Records" style: store
244
+ ```
164
245
 
165
246
  ## Placement
166
247
 
@@ -177,7 +258,7 @@ Any of them may name more than one target — `right of borg and bare`, `level w
177
258
 
178
259
  `of` is optional after any direction, so `below X` and `below of X` both parse. Write whichever reads as English.
179
260
 
180
- There are no coordinates and no numeric offsets. Each direction leaves a gap, one of `none`, `tight`, `normal` (the default) and `wide`. Write it in brackets on the placement itself, or as `gap:` on a node to set the default for every relationship that node is in — including the ones named against it. See [A gap belongs to the placement](#a-gap-belongs-to-the-placement).
261
+ There are no coordinates. Each direction leaves a gap, one of `none`, `tight`, `normal` (the default) and `wide`, or a plain number of pixels — `gap: 12`. Prefer the names: change what `tight` means and every tight gap follows. A number is for the distance you actually mean, and it is no less relative than a name, since it is still measured from the target. Write it in brackets on the placement itself, or as `gap:` on a node to set the default for every relationship that node is in — including the ones named against it. See [A gap belongs to the placement](#a-gap-belongs-to-the-placement).
181
262
 
182
263
  Exactly one node in the document may be left unplaced. Everything else is positioned, directly or transitively, relative to it.
183
264
 
@@ -188,64 +269,64 @@ Exactly one node in the document may be left unplaced. Everything else is positi
188
269
  That single rule is what makes a corridor work. Say two things sit side by side, then put a third between them:
189
270
 
190
271
  ```
191
- box hub "Hub"
192
- box side "Side" left of hub
193
- box wedge "Wedged" right of side left of hub level with hub
272
+ node hub "Hub"
273
+ node side "Side" left of hub
274
+ node wedge "Wedged" right of side left of hub level with hub
194
275
  ```
195
276
 
196
- Nothing states how far apart `hub` and `side` are. They start one gap apart, and adding `wedge` between them pushes them to exactly `wedge`'s width plus two gaps. Delete `wedge` and they close back up. You never pick a number, and no number goes stale when a label grows.
277
+ Nothing states how far apart `hub` and `side` are. They start one gap apart, and adding `wedge` between them pushes them to exactly `wedge`'s width plus two gaps. Delete `wedge` and they close back up. You never pick a number, and no number goes stale when a text grows.
197
278
 
198
279
  This is the step you would otherwise do by hand: shove two things apart to make room, then drag everything back together so the diagram isn't full of holes. The file states the relationships and the distances fall out.
199
280
 
200
- **A lone directional placement still sets both axes.** `right of docker` on its own also centres the node vertically on Docker, because walking right from something keeps you on its centre line. That half is dropped as soon as another placement binds the axis, so it never fights anything you wrote.
281
+ **A lone directional placement still sets both axes.** `right of docker` on its own also centers the node vertically on Docker, because walking right from something keeps you on its center line. That half is dropped as soon as another placement binds the axis, so it never fights anything you wrote.
201
282
 
202
- **Two placements on one axis and nothing on the other is an error.** `right of docker left of macbook` with no vertical placement would have to choose between Docker's centre line and the Macbook's, and that choice decides which row of the diagram the node shares. The tool refuses and names the axis you left unstated. Add `level with docker`, or `below` something, and it resolves.
283
+ **Two placements on one axis and nothing on the other is an error.** `right of docker left of macbook` with no vertical placement would have to choose between Docker's center line and the Macbook's, and that choice decides which row of the diagram the node shares. The tool refuses and names the axis you left unstated. Add `level with docker`, or `below` something, and it resolves.
203
284
 
204
- **Placements that cannot all hold are an error naming them,** rather than a picture with one box on top of another. Because gaps are minimums, this only happens when something is pinned exactly — by `level with`, or by a loop of placements that each demand more room than the last.
285
+ **Placements that cannot all hold are an error naming them,** rather than a picture with one node on top of another. Because gaps are minimums, this only happens when something is pinned exactly — by `level with`, or by a loop of placements that each demand more room than the last.
205
286
 
206
- `level with X` is the one placement that fixes a distance outright: share a centre line, no gap involved. It binds the vertical only.
287
+ `level with X` is the one placement that fixes a distance outright: share a center line, no gap involved. It binds the vertical only.
207
288
 
208
289
  ```
209
- box dumps "" right of server.docker left of dropbox_and_machines level with server.docker
290
+ node dumps "" right of server.docker left of cluster level with server.docker
210
291
  ```
211
292
 
212
293
  Horizontally between two different targets, vertically level with a third. No single relation can say that, which is why a node can carry several.
213
294
 
214
- Naming an edge in front of it aligns that edge instead of the centre. `top level with media` puts the node's top edge on the media box's top edge; `bottom`, `left` and `right` work the same way. `top` and `bottom` bind the vertical, `left` and `right` the horizontal — so `left level with X` and `left of X` are different statements, and the word after `left` is what tells them apart.
295
+ Naming a side in front of it aligns that side instead of the center. `top level with media` puts the node's top side on the media node's top side; `bottom`, `left` and `right` work the same way. `top` and `bottom` bind the vertical, `left` and `right` the horizontal — so `left level with X` and `left of X` are different statements, and the word after `left` is what tells them apart.
215
296
 
216
297
  ### Several targets at once
217
298
 
218
299
  A placement may name several targets joined by `and`, with optional commas. It then places the node against the box that just bounds them all — a region you never have to declare.
219
300
 
220
301
  ```
221
- note rotations "Weekly rotations …" right of bup_hd.borg and bup_hd.bare gap: tight
222
- note archive "archive remains …" left of bup_hd.archive and bup_hd.par2 gap: tight
302
+ node swapped "Swapped weekly …" shape: none left of drive.mirror and drive.clone gap: tight
303
+ node kept "never rotated …" shape: none right of drive.archive and drive.old gap: tight
223
304
  ```
224
305
 
225
- Neither note says anything about its own vertical position, and neither needs to. A lone directional placement centres on what it names, and what these name is the region covering two boxes, so each note lands centred on the pair it explains.
306
+ Neither annotation says anything about its own vertical position, and neither needs to. A lone directional placement centers on what it names, and what these name is the region covering two nodes, so each one lands centered on the pair it explains.
226
307
 
227
- This is why it is one placement with two targets rather than two placements. Two separate `level with` statements are two demands that both have to hold, and boxes at different heights cannot both share a centre line with the same node, so that combination is a contradiction. One statement naming two targets is a single demand about a single region.
308
+ This is why it is one placement with two targets rather than two placements. Two separate `level with` statements are two demands that both have to hold, and nodes at different heights cannot both share a center line with the same node, so that combination is a contradiction. One statement naming two targets is a single demand about a single region.
228
309
 
229
- Combined with the rule that a lone directional placement also binds the other axis, this is how you offset one row against another. `below a and b` reads as "under the pair, centred between them", because the direction binds the vertical against the region and the horizontal falls on the region's centre line:
310
+ Combined with the rule that a lone directional placement also binds the other axis, this is how you offset one row against another. `below a and b` reads as "under the pair, centered between them", because the direction binds the vertical against the region and the horizontal falls on the region's center line:
230
311
 
231
312
  ```
232
- box a "" shape: instance
233
- box b "" right of a gap: tight shape: instance
234
- box c "" right of b gap: tight shape: instance
235
- box d "" below a and b gap: tight shape: instance
236
- box e "" below b and c gap: tight shape: instance
313
+ node a icon: cube
314
+ node b right of a gap: tight icon: cube
315
+ node c right of b gap: tight icon: cube
316
+ node d below a and b gap: tight icon: cube
317
+ node e below b and c gap: tight icon: cube
237
318
  ```
238
319
 
239
- Three above, two below, each sitting in the gap between two of them. Adding a second placement to bind the horizontal is what would left-justify the lower row instead — the centring is dropped as soon as anything else claims that axis.
320
+ Three above, two below, each sitting in the gap between two of them. Adding a second placement to bind the horizontal is what would left-justify the lower row instead — the centering is dropped as soon as anything else claims that axis.
240
321
 
241
- For a direction the region is a floor, so `right of one and three` clears whichever of them sticks out furthest, and it works wherever the targets sit. For an alignment the region is an exact position, and there is one restriction worth knowing: it must not depend on the node being aligned to it. Aligning a note to the region covering `A` and `B` while `B` is placed relative to that same note is refused by name, because there is no order in which each could wait for the other.
322
+ For a direction the region is a floor, so `right of one and three` clears whichever of them sticks out furthest, and it works wherever the targets sit. For an alignment the region is an exact position, and there is one restriction worth knowing: it must not depend on the node being aligned to it. Aligning a node to the region covering `A` and `B` while `B` is placed relative to that same node is refused by name, because there is no order in which each could wait for the other.
242
323
 
243
324
  ### A gap belongs to the placement
244
325
 
245
- A gap is a fact about a relationship, not about a box, so it is written on the placement that names that relationship:
326
+ A gap is a fact about a relationship, not about a node, so it is written on the placement that names that relationship:
246
327
 
247
328
  ```
248
- box dumps "" right of server.docker left of machines (gap: wide) level with server.docker
329
+ node dumps "" right of server.docker left of machines (gap: wide) level with server.docker
249
330
  ```
250
331
 
251
332
  The dumps come straight out of Docker, so they sit at the default distance from it; the two curves on the other side need room to fan out before they reach the machines, so that gap is wide. One number could not say both.
@@ -253,16 +334,16 @@ The dumps come straight out of Docker, so they sit at the default distance from
253
334
  `gap:` written as an ordinary attribute still works and is the node's default, used by every placement that does not name its own:
254
335
 
255
336
  ```
256
- box stack "Stack" below wedge right of wall (gap: tight) gap: wide
337
+ node stack "Stack" below wedge right of wall (gap: tight) gap: wide
257
338
  ```
258
339
 
259
340
  Wide below the wedge, tight to the right of the wall.
260
341
 
261
- **A node's `gap:` reaches every relationship it is in, not only the ones it wrote down.** A relationship exists regardless of which of its two ends happened to name the other, so `gap:` on a box also applies to placements written *against* it:
342
+ **A node's `gap:` reaches every relationship it is in, not only the ones it wrote down.** A relationship exists regardless of which of its two ends happened to name the other, so `gap:` on a node also applies to placements written *against* it:
262
343
 
263
344
  ```
264
- box parser "Parser" gap: wide
265
- box renderer "Renderer" right of parser
345
+ node parser "Parser" gap: wide
346
+ node renderer "Renderer" right of parser
266
347
  ```
267
348
 
268
349
  `parser` names nothing, and the gap still opens. Without this, the only way to push those two apart would be to know that `renderer` is the one that mentioned `parser` and to edit that line instead — which is a fact about how the file was typed, not about the picture.
@@ -271,9 +352,13 @@ Where both ends state a gap the larger applies, since a gap is a minimum either
271
352
 
272
353
  The brackets take `gap:` and nothing else at present; anything else in them is an error naming it. An alignment is an error too — `level with x (gap: tight)` — because sharing a line leaves no distance for a gap to set, and a word that quietly does nothing looks like a fault in the tool.
273
354
 
274
- ### Edge to edge
355
+ ### On a box
275
356
 
276
- A zero gap turns an offset into contact. `below docker (gap: none)` puts the node's top edge flat against Docker's bottom edge, so exact edge relations need no vocabulary of their own.
357
+ A placement's target may be a *part* of a node rather than the whole of it — `inside hub top-right`, `right of hub text`, `on hub bottom-left`. That is where `inside`, `outside` and `on` come in, and it has a section of its own: [Against a part of a node](#against-a-part-of-a-node).
358
+
359
+ ### Side to side
360
+
361
+ A zero gap turns an offset into contact. `below docker (gap: none)` puts the node's top side flat against Docker's bottom side, so exact side relations need no vocabulary of their own.
277
362
 
278
363
  ### Targets
279
364
 
@@ -281,107 +366,107 @@ The target of a placement may be a child of another container. `right of server.
281
366
 
282
367
  Every axis is solved as one system, so a target does not have to come first. What cannot be satisfied is a loop of placements each demanding more room than the last, and that is an error naming the placements in it.
283
368
 
284
- ## Boxes do not overlap
369
+ ## Nodes do not overlap
285
370
 
286
- You never have to say that two boxes must not sit on top of each other. Every pair carries that already, and `overlap: allow` on either one is the opt-out for the rare case where one is meant to cover another. A container never counts as overlapping its own contents.
371
+ You never have to say that two nodes must not sit on top of each other. Every pair carries that already, and `overlap: allow` on either one is the opt-out for the rare case where one is meant to cover another. A container never counts as overlapping its own contents, and neither does a node placed `inside` or `on` a part of another and the box it is stamped on — naming a part and saying `inside` is the author stating the overlap, so there is nothing to report. That exemption is for the pair and nothing else, so the node is still kept clear of everything else in the drawing.
287
372
 
288
- **The tool never picks which way to separate two boxes.** It reads the direction off the arrangement you already stated. Say `A` is left of `B`, put `x` between them, and hang a wide box below `x`: because `x` is right of `A` and the wide box is centred under `x`, the file lets the wide box travel rightward away from `A` and offers no way back. So the only separation it allows is `A` moving further left. Nothing is chosen. Where nothing in the file orders a pair on either axis, the tool refuses and names the pair rather than guessing — which is what happens if you hang two boxes off the same side of the same target and expect them to sort themselves out.
373
+ **The tool never picks which way to separate two nodes.** It reads the direction off the arrangement you already stated. Say `A` is left of `B`, put `x` between them, and hang a wide node below `x`: because `x` is right of `A` and the wide node is centered under `x`, the file lets the wide node travel rightward away from `A` and offers no way back. So the only separation it allows is `A` moving further left. Nothing is chosen. Where nothing in the file orders a pair on either axis, the tool refuses and names the pair rather than guessing — which is what happens if you hang two nodes off the same side of the same target and expect them to sort themselves out.
289
374
 
290
- **One place the tool decides something you didn't write.** Sometimes both axes already imply an order. The wide box is rightward of `A` and also below it, so the overlap clears either by pushing `A` and `B` apart or by dropping the wide box lower, and both satisfy everything you wrote. The rule is to **separate along the axis where the two boxes overlap least**, which is also the smallest movement, and which matches what a person does by hand.
375
+ **One place the tool decides something you didn't write.** Sometimes both axes already imply an order. The wide node is rightward of `A` and also below it, so the overlap clears either by pushing `A` and `B` apart or by dropping the wide node lower, and both satisfy everything you wrote. The rule is to **separate along the axis where the two nodes overlap least**, which is also the smallest movement, and which matches what a person does by hand.
291
376
 
292
- If you want that pinned down rather than defaulted, group the boxes: put `x` and the wide box in an invisible container. The container becomes the thing that must not overlap `A` and `B`, and it moves as one.
377
+ If you want that pinned down rather than defaulted, group the nodes: put `x` and the wide node in an invisible container. The container becomes the thing that must not overlap `A` and `B`, and it moves as one.
293
378
 
294
- Separation leaves a tight gap, deliberately small — enough to read as two boxes rather than one, never enough to look like a distance somebody asked for. Say `gap:` if you want breathing room there.
379
+ Separation leaves a tight gap, deliberately small — enough to read as two nodes rather than one, never enough to look like a distance somebody asked for. Say `gap:` if you want breathing room there. Between two things inside one box it leaves the step the box's contents are stacked by instead, since that is the spacing everything else in there has.
295
380
 
296
- ## Links
381
+ ## Edges
297
382
 
298
383
  ```
299
- link <from> -> <to> ["<label>"] [between <a> and <b> [vertically|horizontally]] [attributes]
300
- link <to> <- <from> ["<label>"] [between <a> and <b> [vertically|horizontally]] [attributes]
301
- link <from> <-> <to> ["<label>"] [between <a> and <b> [vertically|horizontally]] [attributes]
384
+ edge <from> -> <to> ["<text>"] [between <a> and <b> [vertically|horizontally]] [attributes]
385
+ edge <to> <- <from> ["<text>"] [between <a> and <b> [vertically|horizontally]] [attributes]
386
+ edge <from> <-> <to> ["<text>"] [between <a> and <b> [vertically|horizontally]] [attributes]
302
387
  ```
303
388
 
304
- `a <- b` is exactly `b -> a` — same arrow, same picture. What changes is which name you write first, and that is worth having: the first name reads as the subject of the line, and plenty of links are about the thing the arrow points at rather than the thing it leaves. `from:` and `to:` follow the arrow, not the writing order, so they still name the tail and the head.
389
+ `a <- b` is exactly `b -> a` — same arrow, same picture. What changes is which name you write first, and that is worth having: the first name reads as the subject of the line, and plenty of edges are about the thing the arrow points at rather than the thing it leaves. `from:` and `to:` follow the arrow, not the writing order, so they still name the tail and the head.
305
390
 
306
- Endpoints may be nested (`computer1.files`). A link never says where a box goes and routing is the renderer's problem, with one exception: a labelled link claims room in the gap it crosses, which is the next section.
391
+ Endpoints may be nested (`desktop1.files`). An edge never says where a node goes and routing is the renderer's problem, with one exception: an edge with text claims room in the gap it crosses, which is the next section.
307
392
 
308
- A link's label breaks on ` / ` exactly as a node's does, and the block centres on the point the label would otherwise have occupied, so ``"run `deploy` / shell command"`` stacks its two lines around the midpoint of the line rather than running off along it. `width:` is a node attribute and does not apply — a link label folds where you say and nowhere else.
393
+ An edge's text breaks on ` / ` exactly as a node's does, and the block centers on the point the text would otherwise have occupied, so ``"run `deploy` / shell command"`` stacks its two lines around the midpoint of the line rather than running off along it. An edge's text takes the same brackets a node's does, less `at`, so `(wrap: 20)` folds it and `(color: muted)` quiets it.
309
394
 
310
- ### A label makes room for itself
395
+ ### A text makes room for itself
311
396
 
312
- Putting something between two boxes is what pushes them apart, and a label drawn in a corridor is something in that corridor. So a labelled link widens the gap it crosses by what its label needs — the label, a run of line either side of it, and the arrowhead that covers part of that run — and by no more than that.
397
+ Putting something between two nodes is what pushes them apart, and a text drawn in a corridor is something in that corridor. So an edge with text widens the gap it crosses by what its text needs — the text, a run of line either side of it, and the arrowhead that covers part of that run — and by no more than that.
313
398
 
314
399
  ```
315
- box parser "Parser"
316
- box resolver "Resolver" right of parser
317
- link parser -> resolver "statements" from: right to: left
400
+ node parser "Parser"
401
+ node resolver "Resolver" right of parser
402
+ edge parser -> resolver "statements" from: right to: left
318
403
  ```
319
404
 
320
- Nothing there says how far apart those two boxes are. The default gap is sized for two boxes to breathe rather than to hold a word, so without this the label would be drawn across both of them. Delete the label and the gap closes back to the default. Write `gap: wide` on that placement and nothing further happens, because the minimum you asked for is already the larger of the two — a gap is a minimum, and a label is one more thing bidding into it.
405
+ Nothing there says how far apart those two nodes are. The default gap is sized for two nodes to breathe rather than to hold a word, so without this the text would be drawn across both of them. Delete the text and the gap closes back to the default. Write `gap: wide` on that placement and nothing further happens, because the minimum you asked for is already the larger of the two — a gap is a minimum, and a text is one more thing bidding into it.
321
406
 
322
- Which gap the label lands in is derived, never stated. Two boxes clear of each other on exactly one axis have exactly one corridor between them, and that is the one that widens. Two sitting corner to corner have no single corridor, because the line runs diagonally through open space, so nothing is widened for them. The room is measured along the run: a link travelling horizontally needs the label's width, one travelling vertically needs only its depth, so a long label across a vertical gap opens it by a single line and hangs out either side.
407
+ Which gap the text lands in is derived, never stated. Two nodes clear of each other on exactly one axis have exactly one corridor between them, and that is the one that widens. It is judged where the nodes finally sit — after anything centered has been centered and anything overlapping pulled apart — and if widening a gap moves something that was centered on what moved, it is centered again. The same holds between the children of a node, however they are placed. Two sitting corner to corner have no single corridor, because the line runs diagonally through open space, so nothing is widened for them. The room is measured along the run: an edge traveling horizontally needs the text's width, one traveling vertically needs only its depth, so a long text across a vertical gap opens it by a single line and hangs out either side.
323
408
 
324
- An unlabelled link asks for nothing, since every gap is wide enough for an arrowhead. A link carrying a `between` clause asks for nothing here either — its label rides in the channel it named rather than in the gap between its own two ends, and what that does *not* do yet is at the end of the next section but one.
409
+ An edge with no text asks for nothing, since every gap is wide enough for an arrowhead. An edge carrying a `between` clause asks for nothing here either — its text rides in the channel it named rather than in the gap between its own two ends, and what that does *not* do yet is at the end of the next section but one.
325
410
 
326
- ### Which side a link leaves and arrives on
411
+ ### Which side an edge leaves and arrives on
327
412
 
328
- `from:` and `to:` name a side of the box at each end — `top`, `bottom`, `left` or `right`.
413
+ `from:` and `to:` name a side of the node at each end — `top`, `bottom`, `left` or `right`.
329
414
 
330
415
  ```
331
- link computer1.files <-> dropbox from: right to: top
416
+ edge desktop1.files <-> hub from: right to: top
332
417
  ```
333
418
 
334
- That line leaves the right side of `computer1.files` heading right, and arrives at the top of `dropbox` heading down. Naming a side is a statement about how the line should leave or arrive, so the link is drawn as a curve that actually does. A link naming neither side stays the straight centre-to-centre line it has always been. Either end may be named on its own; the unnamed one aims at wherever its partner ended up.
419
+ That line leaves the right side of `desktop1.files` heading right, and arrives at the top of `hub` heading down. Naming a side is a statement about how the line should leave or arrive, so the edge is drawn as a curve that actually does. An edge naming neither side stays the straight center-to-center line it has always been. Either end may be named on its own; the unnamed one aims at wherever its partner ended up.
335
420
 
336
- You name a side and never a point on it. Alone on a side, a link lands at its centre. Sharing a side with other links, the attachments space themselves apart, and which one goes where is derived from where the far ends actually sit — of two links arriving at one top edge, the one coming from further left arrives further left. Move a box and the order follows it. This is the same rule as boxes not overlapping: the tool separates things by default and reads the direction off the solved layout rather than asking you.
421
+ You name a side and never a point on it. Alone on a side, an edge lands at its center. Sharing a side with other edges, the attachments space themselves apart, and which one goes where is derived from where the far ends actually sit — of two edges arriving at one top side, the one coming from further left arrives further left. Move a node and the order follows it. This is the same rule as nodes not overlapping: the tool separates things by default and reads the direction off the solved layout rather than asking you.
337
422
 
338
- The space it leaves is deliberately small, and shrinks further if the side is too short to hold the whole group. On a side short enough, it shrinks to nothing and the attachments coincide. Their labels still come apart, because the lines bow in the middle to make up what the edge could not give them, but the arrowheads themselves land on one point and nothing warns you — so a small box with several links arriving on one edge is worth a look.
423
+ The space it leaves is deliberately small, and shrinks further if the side is too short to hold the whole group. On a side short enough, it shrinks to nothing and the attachments coincide. Their texts still come apart, because the lines bow in the middle to make up what the side could not give them, but the arrowheads themselves land on one point and nothing warns you — so a small node with several edges arriving on one side is worth a look.
339
424
 
340
- #### Several links between the same two sides
425
+ #### Several edges between the same two sides
341
426
 
342
- Links that run between the *same* pair of sides are a case of their own, because "where the far ends sit" cannot order them: every one of them goes to the same box.
427
+ Edges that run between the *same* pair of sides are a case of their own, because "where the far ends sit" cannot order them: every one of them goes to the same node.
343
428
 
344
429
  ```
345
- link gateway -> queue "publishes messages" from: bottom to: left
346
- link queue -> gateway "receives messages" from: left to: bottom
430
+ edge gateway -> queue "publishes messages" from: bottom to: left
431
+ edge queue -> gateway "receives messages" from: left to: bottom
347
432
  ```
348
433
 
349
- Both of those join the bottom of `gateway` to the left of `queue`, so they run in nested lanes. Whichever lane a link takes on one edge, it takes the matching lane on the other — further left along `gateway`'s bottom is further down `queue`'s left — so the lines never cross each other. Any number of links between one pair of sides nests the same way.
434
+ Both of those join the bottom of `gateway` to the left of `queue`, so they run in nested lanes. Whichever lane an edge takes on one side, it takes the matching lane on the other — further left along `gateway`'s bottom is further down `queue`'s left — so the lines never cross each other. Any number of edges between one pair of sides nests the same way.
350
435
 
351
- Which link takes the outer lane is derived where the diagram says anything: two links pointing opposite ways each keep to one side of their own run, so a reciprocal pair reads as a circulation and re-ordering the two lines changes nothing. Links pointing the *same* way give the tool nothing to read, and there the order you wrote them in is what decides.
436
+ Which edge takes the outer lane is derived where the diagram says anything: two edges pointing opposite ways each keep to one side of their own run, so a reciprocal pair reads as a circulation and re-ordering the two lines changes nothing. Edges pointing the *same* way give the tool nothing to read, and there the order you wrote them in is what decides.
352
437
 
353
- The lanes are as wide as the labels riding in them, so labels come apart with the lines. Note which way that pushes: on a horizontal run the labels stack, a line apart, but on a vertical run they all sit at the same height and have to clear each other sideways, so each lane is a whole label wide.
438
+ The lanes are as wide as the texts riding in them, so texts come apart with the lines. Note which way that pushes: on a horizontal run the texts stack, a line apart, but on a vertical run they all sit at the same height and have to clear each other sideways, so each lane is a whole text wide.
354
439
 
355
440
  A side too short to hold the whole group is squeezed, exactly as above — and the room the ends could not give is then made up in the middle, each line bowing across its run by its own share of the shortfall. The captions come apart even where the attachments are packed together. A group that fits its sides is drawn exactly as it was, because the shortfall is nothing.
356
441
 
357
- #### Several links with no side named at all
442
+ #### Several edges with no side named at all
358
443
 
359
- Links between the same two boxes that name no side anywhere have the same problem in a harder form: an unnamed end has no side to be spread along. It aims at the far box's centre and attaches wherever that ray crosses the border, so every link between one pair produces the same point, and three of them come out as one visible line with three labels stacked on it.
444
+ Edges between the same two nodes that name no side anywhere have the same problem in a harder form: an unnamed end has no side to be spread along. It aims at the far node's center and attaches wherever that ray crosses the border, so every edge between one pair produces the same point, and three of them come out as one visible line with three texts stacked on it.
360
445
 
361
446
  ```
362
- box a
363
- box b right of a
364
- link a -> b "first"
365
- link a -> b "second"
366
- link a -> b "third"
447
+ node a
448
+ node b right of a
449
+ edge a -> b "first"
450
+ edge a -> b "second"
451
+ edge a -> b "third"
367
452
  ```
368
453
 
369
- Where a link would attach is not changed by there being others. What changes is only that they no longer do it in the same place: each takes the line it would have drawn alone and moves it sideways, across its own run, by a lane. The lines are parallel and a lane apart, and where each end lands falls out of that. With the two boxes level, all three attach further up and down the same two edges. With them on a diagonal — where a single line would leave through a corner — the two lines straddle it, and one end lands on each of the two edges meeting there. Neither of those is a case you have to know about; they are the same rule seen from two positions.
454
+ Where an edge would attach is not changed by there being others. What changes is only that they no longer do it in the same place: each takes the line it would have drawn alone and moves it sideways, across its own run, by a lane. The lines are parallel and a lane apart, and where each end lands falls out of that. With the two nodes level, all three attach further up and down the same two sides. With them on a diagonal — where a single line would leave through a corner — the two lines straddle it, and one end lands on each of the two sides meeting there. Neither of those is a case you have to know about; they are the same rule seen from two positions.
370
455
 
371
- Lane order follows the rule above: opposite-pointing links keep to their own side of the run, same-pointing ones fall back to the order you wrote them in. A lone link is in no group and is untouched, and naming a side on either end takes a link out of the group, since it then has a side of its own to be spread along.
456
+ Lane order follows the rule above: opposite-pointing edges keep to their own side of the run, same-pointing ones fall back to the order you wrote them in. A lone edge is in no group and is untouched, and naming a side on either end takes an edge out of the group, since it then has a side of its own to be spread along.
372
457
 
373
- Lanes are as wide as the labels riding in them, as with named sides — so two links between the same two boxes come apart far enough for their captions to clear.
458
+ Lanes are as wide as the texts riding in them, as with named sides — so two edges between the same two nodes come apart far enough for their captions to clear.
374
459
 
375
- Past a point they cannot: every line still has to attach on the same two edges, and those are only as tall as the boxes. When the group wants more room than the edges can give, the attachments squeeze evenly to fit and each line makes up the shortfall in the middle, bowing across its own run by exactly what its ends could not give it. The labels ride at the midpoints, so they still come apart even though the arrowheads crowd together. The innermost line has no shortfall and stays straight, which is what every group small enough for its boxes looks like.
460
+ Past a point they cannot: every line still has to attach on the same two sides, and those are only as tall as the nodes. When the group wants more room than the edges can give, the attachments squeeze evenly to fit and each line makes up the shortfall in the middle, bowing across its own run by exactly what its ends could not give it. The texts ride at the midpoints, so they still come apart even though the arrowheads crowd together. The innermost line has no shortfall and stays straight, which is what every group small enough for its nodes looks like.
376
461
 
377
- The bow is the shortfall, not a style — nothing bends until the edge is full, and a group that fits is drawn with straight lines exactly as it always was.
462
+ The bow is the shortfall, not a style — nothing bends until the side is full, and a group that fits is drawn with straight lines exactly as it always was.
378
463
 
379
464
  ### Passing between two things
380
465
 
381
466
  `between <a> and <b>` says that the line travels down the gap between two named nodes.
382
467
 
383
468
  ```
384
- link dumps.db1 -> dropbox "rclone" between computer1 and computer2 from: right to: left
469
+ edge dumps.db1 -> hub "rclone" between desktop1 and laptop1 from: right to: left
385
470
  ```
386
471
 
387
472
  It says nothing about the rest of the line. The clause binds only the stretch where the line is actually passing that pair — where it enters the span the two of them occupy, it is in the gap between them, and before and after it goes wherever its ends take it. The line is drawn as a curve into the gap, a straight run along it, and a curve out to its far end.
@@ -391,36 +476,187 @@ Which gap is meant is usually derived, not stated. One of the two is above the o
391
476
  Two nodes sitting diagonally have *two* gaps between them, and there the derivation has nothing to go on. Add `vertically` or `horizontally`:
392
477
 
393
478
  ```
394
- link c -> d "threaded" between a and b vertically
479
+ edge c -> d "threaded" between a and b vertically
395
480
  ```
396
481
 
397
482
  `vertically` is the gap you measure with a vertical ruler — under the upper one, over the lower one — so a line running along it travels horizontally. The word describes the gap, not the direction of travel, which is the reading to watch for.
398
483
 
399
- Leave it out on a diagonal pair and the error asks for it, in your own node names. Write it where it was not needed and it is checked rather than quietly dropped, so a pair that is only apart vertically will tell you that `horizontally` is wrong. A pair that touches or overlaps has no gap at all, whatever you write, and naming a pair the link never actually passes is an error too.
484
+ Leave it out on a diagonal pair and the error asks for it, in your own node names. Write it where it was not needed and it is checked rather than quietly dropped, so a pair that is only apart vertically will tell you that `horizontally` is wrong. A pair that touches or overlaps has no gap at all, whatever you write, and naming a pair the edge never actually passes is an error too.
400
485
 
401
- Several links may share one channel, and they take a lane each. As with attachments on a side, which link gets which lane is derived from where their ends sit, so lines through a channel come out in the order their ends are in and do not cross. The lanes are spaced by what is actually running along them: a label's depth where a labelled link runs, an arrow's width where none does.
486
+ Several edges may share one channel, and they take a lane each. As with attachments on a side, which edge gets which lane is derived from where their ends sit, so lines through a channel come out in the order their ends are in and do not cross. The lanes are spaced by what is actually running along them: a text's depth where an edge with text runs, an arrow's width where none does.
402
487
 
403
- A named channel does not widen. It is measured off the layout you described, so if you name a gap too narrow for the lines you put through it they crowd together rather than pushing the two boxes apart. That is the difference between this and a label making room for itself, above: there, the corridor is the gap between the link's own two ends, and opening it moves them apart exactly as anything else put between them would. Here the pair is named by a link merely passing through, and nothing yet lets a link bid into a gap it is only a visitor in. It is the remaining half and it is not built.
488
+ A named channel does not widen. It is measured off the layout you described, so if you name a gap too narrow for the lines you put through it they crowd together rather than pushing the two nodes apart. That is the difference between this and a text making room for itself, above: there, the corridor is the gap between the edge's own two ends, and opening it moves them apart exactly as anything else put between them would. Here the pair is named by an edge merely passing through, and nothing yet lets an edge bid into a gap it is only a visitor in. It is the remaining half and it is not built.
404
489
 
405
- ## Notes
490
+ ## Against a part of a node
406
491
 
407
492
  ```
408
- note <name> "<text>" <placement> ...
493
+ <direction> of <node> <part>
494
+ inside <node> <part>
495
+ outside <node> <part>
496
+ on <node> <part>
409
497
  ```
410
498
 
411
- A note is text with no box, anchored to a node so it travels with it.
499
+ A placement's target may be a node, as everywhere else, or **a part of a node** — written as the node's name and the part as a separate word. The parts are a node's `text`, its four sides `top`, `bottom`, `left` and `right`, and its nine points:
500
+
501
+ ```
502
+ top-left top-center top-right
503
+ left-center center right-center
504
+ bottom-left bottom-center bottom-right
505
+ ```
506
+
507
+ Those nine are the whole set, and every part of the language that has a position accepts all of them. They are words anybody can point at without measuring, which is what makes them allowed where `x: 140` is not — and a diagram written in them still moves correctly when a box moves, which is the property that refusal exists to protect.
508
+
509
+ **A side is a segment and a point is a point,** and the spelling is what says which. Bare `top`, `bottom`, `left` and `right` name a side, which is why every midpoint carries `-center`: `right` is the whole right edge, `right-center` is the one point halfway down it. Naming a side leaves the other axis free, so `inside plate right` sits against that edge and centers down it; `inside plate right-center` pins it to the midpoint.
510
+
511
+ Three direction words go with a part target:
512
+
513
+ | | |
514
+ |---|---|
515
+ | `inside` | wholly within, against that part |
516
+ | `outside` | wholly beyond it |
517
+ | `on` | centered on it, so a node on a corner straddles it |
518
+
519
+ ```
520
+ node bob "Bob the builder"
521
+ node bob_link "bob.example.com" inside bob bottom-center
522
+ ```
523
+
524
+ **`inside` and `outside` are shorthands, and their long form is derived rather than listed.** *Inside* is the direction from the named part toward the box's center and *outside* is away from it, so `inside right` is `left of`, `inside top-right` is `below-left of`, and one rule covers every part. `inside <node> center` and `inside <node> text` are errors: neither part is on the boundary, so there is no direction toward the interior from them.
525
+
526
+ **`on` is the one that is not a shorthand.** It is a center alignment on both axes at once, which the language did not otherwise have — `level with` gives the vertical and the edge alignments give whichever axis their edge belongs to, and there is no horizontal center alignment at all. It takes no gap: a center sits on a point rather than a distance from it.
527
+
528
+ The inset for `inside` is a gap on the placement. If nothing says, it is `tight` against another node's box and the box's own padding against the node's own parent — see [Against its own parent](#against-its-own-parent). `(gap: none)` puts the node hard against the edge. It is deliberately *not* the node's own `gap:`, which says how the node stands off its neighbours — a node marked `gap: wide` so its siblings keep clear should not thereby wear its mark 110 pixels in from the corner.
529
+
530
+ **Saying `inside` or `on` is saying the overlap,** so there is nothing for the overlap error to report about that pair. Everything else in the drawing still keeps clear in the ordinary way.
531
+
532
+ `inside`, `outside` and `on` take one target. A direction may name several — `right of a and b` means "clear of the box bounding both", which is a floor and decomposes into one demand per target — but these three read a direction off one part of one box.
533
+
534
+ ### Against its own parent
535
+
536
+ A child may name a part of the node it is inside. **The dotted name is what decides what happens:** a child grows its parent to hold it, and a stranger does not.
537
+
538
+ ```
539
+ node server "Server"
540
+ node server.web "web"
541
+ node server.db "database"
542
+ node server.mark "!" inside server right level with server.db
543
+ node stamp "!" on server top-right
544
+ ```
412
545
 
413
- Nothing bounds a note the way a border bounds a box, so a sentence-length note without a `width` is drawn as one very long line and will cross whatever is beside it. Give every note a width.
546
+ `server` widens so `server.mark` sits against its right edge, held in by the padding, on the database's row. A side word straight after `inside`, `outside` or `on <node>` is always the part, so `right level with` here is not read as the right-edge alignment. `stamp` is not part of `server`, so nothing moves for it: it lies over the corner, half in and half out.
414
547
 
415
- A note starts one step smaller than a box label, because a note annotates the diagram rather than being part of it and at the same size an aside reads as a statement. That is a default, not a ceiling: say `size:` and it does what you said. The two things a note most often needs saying about it are how big its text is and how wide it runs, and both are attributes of the note rather than something to be inferred from the fact that it is one.
548
+ The title band is not something a node has; it is what happens when things stack below the text. Children that say nothing about where they go stack below the text in written order, as they always have, and that is what makes the band. A node whose children all sit beside its text or against its frame has nothing below its text, so it has no band and draws as a leaf — its text centered, and whatever is placed against the text centered with it as one group:
549
+
550
+ ```
551
+ node hub "Cloud sync"
552
+ node hub.star "★" right of hub text
553
+ ```
554
+
555
+ **A child against its own parent is spaced as that parent spaces what it holds.** `inside` insets it by the padding, a placement against the parent's text stands off by the step the contents are stacked by, and the parent's own `gap:` — which says how the parent stands off its neighbours — does not reach it. A gap written on the placement still wins.
556
+
557
+ To give a node a band on purpose, place things below its text. The band is the same one the contents would have made — title at the top, what was placed beneath it one step below:
558
+
559
+ ```
560
+ node q "Deployment"
561
+ node q.a "north-west" below q text inside q left
562
+ node q.b "north-east" below q text inside q right
563
+ node q.c "south-west" inside q bottom-left
564
+ node q.d "south-east" inside q bottom-right
565
+ ```
566
+
567
+ A text given an `at:` in a node with no band ranges in the room its own row and column leave it — between whatever sits beside, above or below it — not across the whole box, so a `top-right` title stops short of a child in the top-right corner rather than pushing it out.
568
+
569
+ A child placed against one of its parent's sides stands beside the rest of the box, not above or below it: if it would cover the title or the contents, the box grows across for a left or right side and down for a top or bottom one. `overlap: allow` on the child is the other answer — "I am inside my parent, do not grow for me" — and it lies over whatever is there.
570
+
571
+ **A child placed `outside` its parent is still part of it.** The parent does not grow for it, but everything that keeps clear of the parent keeps clear of the child too: `right of router` lands beyond the note hanging off Router's right side, not on top of it, and a container holding Router widens to hold the note. An alignment still reads the parent's own box, so `below router` centers under Router and not under Router and its note together.
572
+
573
+ ### Several nodes saying the same thing
574
+
575
+ ```
576
+ node hub "hub"
577
+ node a "one" right of hub
578
+ node b "two" right of hub
579
+ node c "three" right of hub
580
+ ```
581
+
582
+ Three nodes that say the identical thing are one list, not three boxes on one spot. It runs down the page in the order they were written, and is centered on `hub` as a whole — the balanced picture that "hub points at three things" means, which no chain of placements can draw. The same goes for a part: three children `inside p right` are a column against that edge, and three at `inside p top-right` stack into the corner and grow down. The list runs down the page whatever the direction, so three nodes `below hub` are a column under it.
583
+
584
+ Only the identical placement makes a list; add `level with x` to one of them and it places itself. `overlap: allow` asks for the literal pile instead.
585
+
586
+ **A direction and a position are two vocabularies and stay two.** A *direction* is a relation between two nodes and puts this one outside the other, clear of it by a gap: `above-left of hub`. A *position* is a point of one box: `inside hub top-left`. They reach the same corner with different words on purpose, because `above hub` could never become `top of hub` — "the top of the hub" is unambiguously its edge.
587
+
588
+ ## Notes and other bare text
589
+
590
+ There is no `note` statement. A note is a node with no body:
591
+
592
+ ```
593
+ node aside "Written by a nightly cron job." (wrap: 30) shape: none below dumps gap: tight
594
+ ```
595
+
596
+ A keyword names a picture, and "note" names a use. The picture is *text with no box*, and that serves plenty of uses which are not asides — a caption on a brace, a title over a diagram. So the keyword went and the picture stayed. An older file writing `note` gets an error quoting the replacement.
597
+
598
+ Nothing bounds bare text the way a border bounds a node, so a sentence-length one without a `wrap` is drawn as one very long line and will cross whatever is beside it. Give every one a wrap.
599
+
600
+ `shape: none` starts one step smaller than a node's text, because an aside at the same size reads as a statement. That is a default, not a ceiling: say `(size: …)` and it does what you said.
416
601
 
417
602
  ## Decks
418
603
 
419
604
  ```
420
- deck <name> "<label>" ["<label>" ...]
605
+ deck <name> "<text>" ["<text>" ...]
606
+ ```
607
+
608
+ Draws the named container with offset copies behind it, one per text, to say "there are several of these and they are the same." Only the front copy shows its contents.
609
+
610
+ ## Attributes
611
+
612
+ Every attribute, and what takes one. The kinds here are what a node's **body** is rather than which keyword declared it — every one of the three is written `node`, and each takes a different set.
613
+
614
+ | attribute | `shape:` | `icon:` | `shape: none` | edge | says |
615
+ |---|---|---|---|---|---|
616
+ | `style` | ✓ | ✓ | ✓ | ✓ | the named bundle to take appearance from |
617
+ | `gap` | ✓ | ✓ | ✓ | | the default distance to whatever it is placed against |
618
+ | `overlap` | ✓ | ✓ | ✓ | | `allow`, to opt out of non-overlap |
619
+ | `contents` | ✓ | | | | how the children are sized and where the block of them sits, in brackets |
620
+ | `badge` | ✓ | | | | the picture beside the text |
621
+ | `shape` | ✓ | | ✓ | | the outline the node is drawn with, `none` included |
622
+ | `icon` | | ✓ | | | the picture the node is drawn as |
623
+ | `from` `to` | | | | ✓ | which side the line leaves and arrives on |
624
+ | `fill` | ✓ | | | | color — see "A color names the part it colors" |
625
+ | `border` | ✓ | | | | color |
626
+ | `text` | ✓ | ✓ | ✓ | ✓ | the text's properties, in brackets — a style's form of what a node or an edge writes after its own words |
627
+ | `line` | | | | ✓ | color |
628
+ | `url` | ✓ | ✓ | ✓ | ✓ | a destination to open when the thing is clicked |
629
+
630
+ The `diagram` statement has a vocabulary of its own — `background`, and so far nothing else — which is checked the same way. Writing `background:` on a node is an error that points at `fill:`.
631
+
632
+ **A word this table does not give the kind is an error.** The two ways of being wrong get different answers, because they have different remedies. A word that is an attribute nowhere is a misspelling, and the error lists what the kind does take. A word that is an attribute *somewhere else* is usually a real statement written on the wrong half of the diagram, so the error says where it belongs:
633
+
634
+ ```
635
+ "one" is a node and has from: left. `from:` belongs to an edge — a node takes style, gap, ...
636
+ ```
637
+
638
+ Some of the gaps in the table are worth saying out loud, because none of them looks like a mistake while you are writing it. A picture and a bodiless node take no `fill:` or `border:` — there is no outline for either to reach. Neither takes `contents:` either, which says how a node's children sit, and neither may have any. `shape:` and `icon:` each appear only on the kind they make, and writing both is an error naming both. An edge takes no `gap:` or `overlap:` — those say where a node sits, and an edge is not placed, it joins two things that are.
639
+
640
+ **Changed 2026-09-09.** Until then a node or edge attribute the tool did not recognize was parsed, stored and never read: `wibble: red` on a node drew nothing and said nothing. This was the last place in the language where a key could silently do nothing, and the rule everywhere else — an unknown `diagram` key, an unknown placement modifier, a color naming a part the kind has not got — has always been that a key which silently does nothing looks like the tool being broken rather than like a typo. A file that rendered with a stray word in it will now stop with an error naming it.
641
+
642
+ ## Destinations
643
+
644
+ ```
645
+ node docs "Documentation" url: "https://example.com/docs"
646
+ edge docs -> store "read first" url: "https://example.com/order"
421
647
  ```
422
648
 
423
- Draws the named container with offset copies behind it, one per label, to say "there are several of these and they are the same." Only the front copy shows its contents.
649
+ `url:` makes the thing clickable in a viewer that follows links — a browser showing the SVG, or a page it is embedded in. The whole thing is the target: a node's box and everything drawn in it, an edge's text. A rasteriser ignores it, so a PNG is unaffected.
650
+
651
+ **The value is quoted.** Without the quotes everything from the `//` onwards is a comment, so `url: https://example.com` would set the destination to `https:`. The error for the unquoted form says so rather than reporting a missing value.
652
+
653
+ **Nothing about a destination is visible.** A clickable node looks like any other. Color was considered and dropped: an edge with no text and a node that is nothing but a picture have nothing to color, so it would be a decoration that sometimes applies — and a destination is *content*, while color is appearance, and nothing else in the language lets one reach the other. An author who wants a destination to look like one writes the color themselves.
654
+
655
+ **A style may not carry one**, for the same reason: a style is a bundle worn by many things, and one `url:` in it would point every one of them at the same place.
656
+
657
+ **A container's destination reaches its children.** A child that names none of its own is clickable with its container's, and one that names its own overrules it inside its own box — so a container catches every click its children do not.
658
+
659
+ **An edge with no text is refused one.** The line is a pixel and a half wide, which is a target nobody can hit; a destination that technically works and practically does not is the silent defect this language refuses everywhere else.
424
660
 
425
661
  ## Styles
426
662
 
@@ -428,29 +664,72 @@ Draws the named container with offset copies behind it, one per label, to say "t
428
664
  style <name> <attributes>
429
665
  ```
430
666
 
431
- A named bundle of appearance, applied with `style: <name>` on a node or link. Colour carries meaning through the style name rather than being written per node.
667
+ A named bundle of appearance, applied with `style: <name>` on a node or edge. Color carries meaning through the style name rather than being written per node.
432
668
 
433
669
  ```
434
- style backup stroke: #d2904e
435
- box server.mirror "\\"important\\" mirror" style: backup
670
+ style backup border: #d2904e
671
+ node server.mirror "shared folder / mirror" style: backup
436
672
  ```
437
673
 
438
- The appearance attributes are `stroke`, `fill`, `subtext`, `size`, `icon` and `shape`. The first three each take a colour written as the viewer will receive it — `#142814`, or any CSS colour, or `none`. On a link `stroke` colours the line, its arrowheads *and* its label, since a link that means something by being orange means it in its words too.
674
+ The appearance attributes are `fill`, `border`, `line`, `text`, `badge`, `icon` and `shape`. The first three each take a color written as the viewer will receive it — `#142814`, or any CSS color, or `none`; `text` takes the bracket described under "The text and its brackets".
675
+
676
+ ### A color names the part it colors
439
677
 
440
- There is no list of colour words the tool knows. An earlier version had one, and it was wrong in the way such lists always are: `dark-green` existed only because somebody added it to a map in the renderer, and the next colour a diagram wanted would have needed a code change to say. Writing the colour directly removes both the list and the reason to grow it. `green` still works, because it is a CSS colour, not because this tool has heard of it.
678
+ A color attribute says which part of a thing it colors, and a part exists only on the kinds that have one:
441
679
 
442
- `subtext` colours every label line after the first, so a box can carry a name and a quieter qualifier under it:
680
+ | attribute | colors | on |
681
+ |---|---|---|
682
+ | `fill` | the area inside the outline | a node |
683
+ | `border` | the outline | a node |
684
+ | `text: (color: …)` | the text | every node, and an edge |
685
+ | `line` | the drawn line and its arrowheads | an edge |
686
+
687
+ A word written on a kind that has no such part is refused by name, and the error lists the parts that kind does have — `border:` on a node with no body is a mistake, not something to ignore, for the same reason an unknown `diagram` key is.
688
+
689
+ An edge's text takes the line's color unless its own brackets say otherwise, so an edge that means something by being orange means it in its words too, and there is still a way to say the words are not orange.
690
+
691
+ A style contributes a part only to the kinds that have it, so a style shared between nodes and edges writes one key for each:
443
692
 
444
693
  ```
445
- style synced fill: #142814 stroke: #486544 subtext: muted
446
- box pc.files "\"important\" directory / Dropbox-synced" style: synced
694
+ style backup border: #d2904e line: #d2904e
447
695
  ```
448
696
 
449
- `icon` and `shape` belong in a style for the same reason a colour does: they say what kind of thing this is, and a kind wants to look alike everywhere it appears. `style artifact fill: #460000 shape: document` puts the folded corner on every dump in the diagram, and the use site stays one word.
697
+ The nodes take the border, the edges take the line, and neither sees the other's word. Writing only `border` there would color the nodes and leave the edges plain.
698
+
699
+ ### A style may carry what a thing cannot use
700
+
701
+ The table under "Attributes" is checked against what you wrote *on the statement*, never against what a style handed it. That is what makes a bundle spanning kinds possible at all: the benchmark's `style synced` carries a fill and a border for five nodes and a `line` for the four edges joining them, and every use of it leaves some of its keys unused. That is the style doing its job, not a mistake, so nothing is said about it.
702
+
703
+ What is refused is a style that gives a thing **nothing at all**:
704
+
705
+ ```
706
+ style boxy fill: #142814 badge: disk
707
+ node n "An aside" shape: none style: boxy
708
+ ```
450
709
 
451
- Bundling `size` into a style is how a size comes to mean something. `style aside size: small stroke: #8b8b8b` applied to several nodes says they are the same kind of remark, which a `size: small` written out at each of them does not.
710
+ A node with no body is bare text, with neither a fill nor a badge, so `boxy` dresses it in nothing whatever and the name is on the wrong sort of thing. Partial overlap is the normal case; zero overlap is never anything else. A style that named every attribute in the language would slip through this, since it contributes to everything by construction — nobody writes one by accident, and the hole is left open rather than closed with a rule that would fire on `synced`.
452
711
 
453
- `muted` is the one reserved word left, and it earns the exception: it means the theme's secondary text colour rather than a fixed one, so a qualifier stays readable when the theme changes. Writing `subtext: #8b8b8b` instead would pin it to one theme. Say nothing and every line of a label reads alike, which is what most labels want — `Computer 1 / Ubuntu` is two lines of one name, not a name and a qualifier, and the distinction is the author's to make rather than the renderer's to guess.
712
+ A style's own keys are checked against the whole vocabulary, since a word that is an attribute of nothing is a misspelling wherever it sits. `style s wibble: red` is an error; a style was the last place one could hide.
713
+
714
+ **Removed: `stroke`.** It named no part — it meant the border of a node, the *text* of one drawn as a picture or with no body at all, and the line of an edge, whichever the thing happened to have. That is coherent one kind at a time and ambiguous read across them; it meant no ink attribute could ever be *wrong*; and it left one thing with no way to be said at all, the color of the text on an ordinary node. An older file carrying it gets an error naming the word to use instead.
715
+
716
+ A color is never written in quotes, and a quoted one is refused. There is nothing to check a color *against* — the tool keeps no list of color words, as below — so this is the one thing that can be checked, and it is the mistake that actually gets made: every attribute that takes a color would otherwise accept a quoted string, find it is not a color, and draw nothing without saying so.
717
+
718
+ There is no list of color words the tool knows. An earlier version had one, and it was wrong in the way such lists always are: `dark-green` existed only because somebody added it to a map in the renderer, and the next color a diagram wanted would have needed a code change to say. Writing the color directly removes both the list and the reason to grow it. `green` still works, because it is a CSS color, not because this tool has heard of it.
719
+
720
+ A style carrying `text: (color: muted)` is what a marked-up word borrows from, which is how a node carries a name with a quieter qualifier under it:
721
+
722
+ ```
723
+ style synced fill: #142814 border: #486544
724
+ style dim text: (color: muted)
725
+ node pc.files "shared folder / [dim]synced[/dim]" style: synced
726
+ ```
727
+
728
+ **Removed: `subtext`.** It colored every text line after the first, which is a positional slice: the rule lived in a style elsewhere in the file and was applied by counting, so a reader of `"shared folder / synced"` could not see that the second line was quiet. The mark says what is quiet where it is quiet, and reaches a word in the middle of a line, which the slice never could. An older file carrying it gets an error naming the mark to write instead.
729
+
730
+ `badge`, `icon` and `shape` belong in a style for the same reason a color does: they say what kind of thing this is, and a kind wants to look alike everywhere it appears. `style artifact fill: #460000 shape: document` puts the folded corner on every dump in the diagram, and the use site stays one word.
731
+
732
+ `muted` is the one reserved word left, and it earns the exception: it means the theme's secondary text color rather than a fixed one, so a quiet line stays readable when the theme changes. Writing `#8b8b8b` instead would pin it to one theme. Say nothing and every line of a text reads alike, which is what most texts want — `Computer 1 / Ubuntu` is two lines of one name, not a name and a qualifier, and the distinction is the author's to make rather than the renderer's to guess.
454
733
 
455
734
  ## The diagram itself
456
735
 
@@ -464,16 +743,16 @@ Settings that belong to the drawing as a whole rather than to anything in it. Th
464
743
  diagram background: #111111
465
744
  ```
466
745
 
467
- One attribute so far. `background` takes a colour the same way `fill` does, and it colours the page behind everything, including the strip a link label knocks out of whatever it crosses. Say nothing and the theme's own background stands.
746
+ One attribute so far. `background` takes a color the same way `fill` does, and it colors the page behind everything, including the strip an edge text knocks out of whatever it crosses. Say nothing and the theme's own background stands.
468
747
 
469
- An unknown key is refused by name — `diagram has no "backround" — it takes background` — rather than quietly ignored. A node attribute the tool does not recognise is harmless, because you can see the node; a diagram-wide setting that silently does nothing looks exactly like a renderer bug.
748
+ An unknown key is refused by name — `diagram has no "backround" — it takes background` — rather than quietly ignored, the same as every other attribute. See "Attributes".
470
749
 
471
750
  ## What the language refuses
472
751
 
473
- Deliberate omissions. What they protect is that the renderer never *chooses* an arrangement — it computes the one you described. Working out coordinates from a stated arrangement is arithmetic and is not what is being refused here; picking between arrangements that all satisfy what you wrote is. There is exactly one narrow exception, and it is named as such under "Boxes do not overlap".
752
+ Deliberate omissions. What they protect is that the renderer never *chooses* an arrangement — it computes the one you described. Working out coordinates from a stated arrangement is arithmetic and is not what is being refused here; picking between arrangements that all satisfy what you wrote is. There is exactly one narrow exception, and it is named as such under "Nodes do not overlap".
474
753
 
475
754
  - **Coordinates**, in any form, including as an escape hatch.
476
- - **Guessing an axis nobody constrained.** When two placements bind one axis and nothing binds the other, the tool refuses rather than picking a target to centre on. Choosing there would decide which row a box shares, not how far it sits from something.
755
+ - **Guessing an axis nobody constrained.** When two placements bind one axis and nothing binds the other, the tool refuses rather than picking a target to center on. Choosing there would decide which row a node shares, not how far it sits from something.
477
756
  - **Placements that run in a circle.** A loop where each placement demands more room than the last cannot be satisfied and is an error naming the placements involved. A target does *not* have to be positioned before the node naming it — the whole system is solved at once — so ordinary mutual references are fine.
478
757
  - **Edge waypoints.** A point a line must pass through is a coordinate wearing a hat. Saying a line goes between two named things is not one — it names things the diagram already contains, and it survives those things moving.
479
758
  - **Choosing a route.** The tool will not find its own way around an obstacle. A line that crosses something it should not is a line you have not yet said enough about, and `between` is how you say it.
@@ -485,50 +764,76 @@ Note what is *not* on this list: saying more about where something goes. A state
485
764
 
486
765
  Designed, decided, and absent from the code. Written down so the next version has somewhere to start.
487
766
 
488
- **Nothing keeps a link clear of a box on its own.** Non-overlap applies to boxes only. A line may still cut across a box it has nothing to do with, and a link label may still land on top of one. `between` is how you say where a line goes when that matters, and nothing checks the ones where you have not said. A check belongs on the diagnostics list, but finding a route by itself does not — see "What the language refuses".
767
+ **Nothing keeps an edge clear of a node on its own.** Non-overlap applies to nodes only. A line may still cut across a node it has nothing to do with, and an edge text may still land on top of one. `between` is how you say where a line goes when that matters, and nothing checks the ones where you have not said. A check belongs on the diagnostics list, but finding a route by itself does not — see "What the language refuses".
489
768
 
490
- **An icon outside the built-in six.** The set is closed, and a diagram wanting a glyph that is not in it has nowhere to go. The two shapes this could take are a declaration in the file, `icon <name> "<path data>"` beside `style`, and `icon: ./thing.svg` inlined by the tool at render time. Either keeps the output standalone, which is the constraint any answer has to meet.
769
+ **An icon outside the built-in seven.** The set is closed, and a diagram wanting a picture that is not in it has nowhere to go. The two shapes this could take are a declaration in the file, `icon <name> "<path data>"` beside `style`, and `icon: ./thing.svg` inlined by the tool at render time. Either keeps the output standalone, which is the constraint any answer has to meet.
491
770
 
492
- **A named channel cannot make room for itself.** Lines through a `between` gap too narrow for them crowd together silently, in exactly the way attachments on a too-short side do. A labelled link *does* now open the gap between its own two ends — see "A label makes room for itself" — and it does so by the measure-then-constrain route that region alignments already use, which is the route this wants too. What is missing is the harder case: several links sharing a channel between two nodes neither of them is an end of, where the room needed is the whole stack of lanes rather than one label.
771
+ **A named channel cannot make room for itself.** Lines through a `between` gap too narrow for them crowd together silently, in exactly the way attachments on a too-short side do. An edge with text *does* now open the gap between its own two ends — see "A text makes room for itself" — and it does so by the measure-then-constrain route that region alignments already use, which is the route this wants too. What is missing is the harder case: several edges sharing a channel between two nodes neither of them is an end of, where the room needed is the whole stack of lanes rather than one text.
493
772
 
494
773
  ## Known to be wrong
495
774
 
496
775
  Not omissions — defects, left here so nobody rediscovers them. Most were found by rendering the benchmark diagram; the last was not, and that is the interesting one, because the benchmark could never have caught it.
497
776
 
498
- ~~A placement written after an attribute reports the wrong mistake.~~ Fixed. Attributes still end the positional part of a statement, so `box q "Q" gap: wide level with p` is still an error, but the message now names the token that starts the stray placement and says to move it in front of the first `key: value`, rather than reporting where the parser had got to. The shape was easy to write by accident because a gap read as though it belonged to the placement it followed — which it now does, in brackets.
777
+ ~~A placement written after an attribute was an error.~~ Gone entirely in 0.3.0, along with the papercut about how it was reported. `node q "Q" gap: wide level with p` is now an ordinary statement. The rule existed only because a bare `gap:` written between two placements could not be told from the node-wide default; [bracketing a gap onto its placement](#a-gap-belongs-to-the-placement) removed that ambiguity and left the ordering rule with nothing to protect.
499
778
 
500
- **A bracketed node sits against one side of any slack.** When two opposing placements leave more room than the node needs — because something else forced the two targets further apart — the node sits against the side it was pushed from rather than centred between them. In practice the tightest arrangement usually leaves no slack, so this rarely shows. Whether it should centre instead is not decided.
779
+ **A bracketed node sits against one side of any slack.** When two opposing placements leave more room than the node needs — because something else forced the two targets further apart — the node sits against the side it was pushed from rather than centered between them. In practice the tightest arrangement usually leaves no slack, so this rarely shows. Whether it should center instead is not decided.
501
780
 
502
781
  ~~A node placed only with `left of` or `above` drifted to the canvas edge.~~ Fixed. Every constraint reads "this one is at least so far right of that one", so the solve puts each node at the smallest position its constraints allow — right for anything with something behind it, but `left of X` bounds *X* rather than the node that wrote it, leaving such a node nothing to be pushed by. It settled at the edge of the drawing while its target was carried off by the rest of the diagram. A node with nothing behind it now travels until the first of its own placements binds, which is what "as close together as your placements allow" always said.
503
782
 
504
- ~~A note could not be put beside the rows it was about.~~ Fixed by letting a placement name several targets, which places the node against the region bounding them. The workaround before it was to wrap the targets in an invisible container so there was a single thing to name, which made the author declare a box to stand in for an idea the language could have expressed directly — and cost the container's padding on top.
783
+ ~~An annotation could not be put beside the rows it was about.~~ Fixed by letting a placement name several targets, which places the node against the region bounding them. The workaround before it was to wrap the targets in an invisible container so there was a single thing to name, which made the author declare a node to stand in for an idea the language could have expressed directly — and cost the container's padding on top.
505
784
 
506
785
  ~~One relation cannot say what a real arrangement needs.~~ Fixed by letting a node carry several placements: it takes its horizontal position from one target and its vertical from another, and two opposing placements put it between two more.
507
786
 
508
- ~~The unwritten axis is a silent guess.~~ Fixed. A lone placement's centring is now the documented meaning of the direction rather than a fallback, and the case where it would have to choose between two targets is an error.
787
+ ~~The unwritten axis is a silent guess.~~ Fixed. A lone placement's centering is now the documented meaning of the direction rather than a fallback, and the case where it would have to choose between two targets is an error.
509
788
 
510
- ~~Two boxes can land on the same pixels in silence.~~ Fixed. Every pair of boxes must now clear the other, and where the file does not order them the tool says so instead of drawing one over the other. Links are still unchecked.
789
+ ~~Two nodes can land on the same pixels in silence.~~ Fixed. Every pair of nodes must now clear the other, and where the file does not order them the tool says so instead of drawing one over the other. Edges are still unchecked.
511
790
 
512
791
  ~~Gaps get used as a fixing hack.~~ Fixed by making every gap a minimum. Room for something is made by saying that something goes there, not by widening a number on an unrelated line.
513
792
 
514
- ~~A label cannot contain a slash.~~ Fixed in two parts: the line-break marker now needs whitespace on both sides, so `TCP/IP`, `16/9`, `I/O` and every path and URL survive untouched, and `\/` escapes the marker for a label that wants a spaced slash and no break, such as `Before \/ After`.
793
+ ~~A text cannot contain a slash.~~ Fixed in two parts: the line-break marker now needs whitespace on both sides, so `TCP/IP`, `16/9`, `I/O` and every path and URL survive untouched, and `\/` escapes the marker for a text that wants a spaced slash and no break, such as `Before \/ After`.
794
+
795
+ That one was found by testing the lexer, not by rendering — and it could not have been found by rendering, because every text in the benchmark happens to use spaces around its separator. Worth knowing that the repository's own second test target is an OSI and **TCP/IP** diagram, so a picture the language was meant to be tested against could not have been written in it. A benchmark only exercises the cases it happens to contain.
796
+
797
+ ~~An edge with text between two nodes at the default gap drew its text across both of them.~~ Fixed. The default gap is sized for nodes to breathe and a text is wider than that, so `edge a -> b "statements"` on two adjacent nodes came out unreadable and nothing said so; the authoring workaround was to name a wider gap on a placement that had no reason to be wider. An edge with text now widens the corridor it crosses by what the text needs. Note what this is *not*: no coordinate, no repair of a solved layout, and nothing that finds a route — the corridor is derived from where the nodes landed and then becomes an ordinary minimum distance like any other.
798
+
799
+ ~~Two edges between the same pair of sides were drawn on top of each other.~~ Fixed. Each side was ordered on its own, by where the far ends sat, and for edges that share both ends that signal says nothing — so the two edges were ordered without reference to each other and the lines converged in the middle instead of nesting. The visible damage was to the texts: both landed at the same point and the second knocked a hole through the first, leaving one word of it. A group like this now takes one lane order used at both ends. See "Several edges between the same two sides".
800
+
801
+ ~~Several edges between the same two nodes with no side named were drawn on top of each other.~~ Fixed. This is the same defect as the one above, one step out: a bundle is a statement about two named sides, and an end with no side named has not made one, so nothing saw the group. `edge a -> b` three times drew one visible line carrying one text. Each such edge now takes its own line, parallel to the one it would have drawn alone and a lane away from it. Note what did *not* change: an unnamed end still attaches where the center-to-center ray crosses the border, so no single edge anywhere moved. See "Several edges with no side named at all".
802
+
803
+ ~~A text's properties sat among the node's.~~ Gone in 0.3.0. `size:`, `wrap:` and the text color `text:` were top-level attributes, sitting beside `fill:` and `shape:` as though how big a text is set were a fact about the node. They are in the brackets after the text now, where the reader can see what they modify, and an older file writing one at the top level gets an error naming the bracket. `at:` came with them and grew from `top | bottom` to the nine named positions, which is the same repair one level down: two of the nine had been handed out because those were the two somebody needed.
804
+
805
+ ~~An unknown attribute was ignored in silence.~~ Fixed. `wibble: red` on a node parsed, was stored, and was never read again — nothing drew and nothing was said. So did every real attribute written on a kind with no use for it: `align:` on a node that may have no children, `gap:` on an edge. This was the same defect the color parts had closed one level down a version earlier, and it is how that migration produced false results from the repository's own regression check, since the older build simply dropped every `border:` it had not heard of. See "Attributes".
806
+
807
+ ~~An edge text ignored the line break.~~ Fixed. ` / ` split a node's text and was never applied to an edge's, so the marker came out as a literal slash on an arrow and the benchmark's two-line captions had to be flattened to one. The measurer had always returned the split lines; the renderer was handing it the raw string and drawing that instead. The block now centers on the point the text already occupied, so a one-line text sits exactly where it did.
808
+
809
+ ## Changelog
515
810
 
516
- That one was found by testing the lexer, not by rendering — and it could not have been found by rendering, because every label in the benchmark happens to use spaces around its separator. Worth knowing that the repository's own second test target is an OSI and **TCP/IP** diagram, so a picture the language was meant to be tested against could not have been written in it. A benchmark only exercises the cases it happens to contain.
811
+ Pre-1.0, so the minor number is where a breaking change goes. Every removal below is refused by name with the replacement quoted, rather than dropped in silence — an older file stops with an error saying what to write instead.
517
812
 
518
- ~~A labelled link between two boxes at the default gap drew its label across both of them.~~ Fixed. The default gap is sized for boxes to breathe and a label is wider than that, so `link a -> b "statements"` on two adjacent boxes came out unreadable and nothing said so; the authoring workaround was to name a wider gap on a placement that had no reason to be wider. A labelled link now widens the corridor it crosses by what the label needs. Note what this is *not*: no coordinate, no repair of a solved layout, and nothing that finds a route — the corridor is derived from where the boxes landed and then becomes an ordinary minimum distance like any other.
813
+ **0.3.0** — the vocabulary, reworked in one breaking version so there is one migration rather than five.
519
814
 
520
- ~~Two links between the same pair of sides were drawn on top of each other.~~ Fixed. Each side was ordered on its own, by where the far ends sat, and for links that share both ends that signal says nothing — so the two edges were ordered without reference to each other and the lines converged in the middle instead of nesting. The visible damage was to the labels: both landed at the same point and the second knocked a hole through the first, leaving one word of it. A group like this now takes one lane order used at both ends. See "Several links between the same two sides".
815
+ - `box` is `node` and `link` is `edge`. The string on either is its *text*; "label" is not a word the language has.
816
+ - A node's **body** is `shape: rectangle | document | none` or `icon: <name>`, and writing both is an error naming both. `note` is gone — a note is `node … shape: none`. `shape: instance` is gone — the icon is `cube`, and a node drawn as one is `icon: cube`.
817
+ - A node drawn as a picture and given no text of its own shows none. A node with a body still falls back to its name.
818
+ - The decoration icon is `badge:`, which freed `icon:` for the body. It is a shorthand for a child placed beside the text, so a container's badge now sits beside its title rather than in the far corner. It is refused on a picture and on a `shape: none` node, where it used to draw nothing.
819
+ - A placement may target a *part* of a node — its text, a side or one of nine named points — with `inside`, `outside` and `on` reading a direction off it.
820
+ - Everything a text says about itself is in the brackets after it — `color`, `size`, `wrap`, `align`, `at`. The top-level `size:`, `wrap:`, `align:` and the text color `text:` are gone; a style says them as `text: (…)`. `at:` grew from two positions to the nine.
821
+ - `subtext:` is gone, replaced by inline markup `[style]word[/style]`, which names a style and reaches a word anywhere in a text. `\[` escapes a literal bracket.
822
+ - `align: widths` is `contents: (widths: match)`, and the same brackets take `align:` for where the block of contents sits.
823
+ - `url:` is new: a destination on a node or an edge.
824
+ - Attributes and placements may be written in any order after the text.
521
825
 
522
- ~~Several links between the same two boxes with no side named were drawn on top of each other.~~ Fixed. This is the same defect as the one above, one step out: a bundle is a statement about two named edges, and an end with no side named has not made one, so nothing saw the group. `link a -> b` three times drew one visible line carrying one label. Each such link now takes its own line, parallel to the one it would have drawn alone and a lane away from it. Note what did *not* change: an unnamed end still attaches where the centre-to-centre ray crosses the border, so no single link anywhere moved. See "Several links with no side named at all".
826
+ **0.2.0** — `stroke:` split into the part it colors (`border:`, `line:`, and the text's own color); `width:` became `wrap:`; every attribute is refused by name on a kind that has no use for it, where an unknown one used to be parsed and dropped.
523
827
 
524
- ~~A link label ignored the line break.~~ Fixed. ` / ` split a node's label and was never applied to a link's, so the marker came out as a literal slash on an arrow and the benchmark's two-line captions had to be flattened to one. The measurer had always returned the split lines; the renderer was handing it the raw string and drawing that instead. The block now centres on the point the label already occupied, so a one-line label sits exactly where it did.
828
+ **0.1.0** — first published version.
525
829
 
526
830
  ## Undecided
527
831
 
528
832
  Open questions the benchmark raised, recorded so a later session does not rediscover them.
529
833
 
530
834
  - Named gaps are the first step toward numbers, but making them minimums took most of the pressure off: they now set how much a diagram breathes, never whether something fits. Whether four names is the right number is still open.
531
- - Four machines each holding an identically-labelled `files` child means writing the same line four times. This is the strongest case for a set-level declaration, for terseness rather than for placement.
835
+ - Four machines each holding a `files` child with the same text means writing the same line four times. This is the strongest case for a set-level declaration, for terseness rather than for placement.
532
836
  - The 2×2 arrangement around a hub is four independent statements, so a fifth machine has no slot to reflow into. There are only eight directions.
533
- - ~~Two notes anchored to the same side of one node will collide.~~ Answered by putting both in an invisible container and placing the container, so they stack instead of stacking on top of each other. Writing it the colliding way is now an error rather than a bad picture, since nothing in the file orders the two. Whether the container idiom is good enough or wants dedicated syntax is open.
534
- - Nothing yet expresses one box spanning several rows of a parallel column, which the OSI reference render needs.
837
+ - ~~Two annotations anchored to the same side of one node will collide.~~ Answered by putting both in an invisible container and placing the container, so they stack instead of stacking on top of each other. Writing it the colliding way is now an error rather than a bad picture, since nothing in the file orders the two. Whether the container idiom is good enough or wants dedicated syntax is open.
838
+ - Nothing yet expresses one node spanning several rows of a parallel column, which the OSI reference render needs.
839
+ - ~~How contents sit inside a container that is wider than they are.~~ Answered by `contents: (widths: …, align: …)` — see "How the contents sit". Automatic centering stays ruled out: it would move every existing diagram whose container title is wider than its contents. Whether this is the same question as the bracketed node sitting against one side of its slack, under "Known to be wrong", is still open — they share the phrase and not the mechanism, since one distributes to a group at size time and the other is a single member in a constraint system.