reladraw 0.1.0 → 0.2.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
@@ -10,7 +10,7 @@ One statement per line. No multi-line statements, no line continuations, no bloc
10
10
 
11
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.
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
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.
16
16
 
@@ -33,7 +33,7 @@ box <name> ["<text>"] [<placement> ...] [attributes]
33
33
 
34
34
  `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
35
 
36
- The text is optional, and a box without it is labelled with its own name:
36
+ The text is optional, and a box without it is labeled with its own name:
37
37
 
38
38
  ```
39
39
  box parser
@@ -50,11 +50,13 @@ The whitespace is part of the marker, not decoration. A slash inside a word is a
50
50
 
51
51
  For a label that wants a spaced slash and no break, `\/` escapes it: `"Before \/ After"` is one line. The escapes are `\"`, `\\` and `\/`.
52
52
 
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.
53
+ `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.
54
54
 
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.
55
+ It was called `width` until it was renamed, and the old name is now an error naming the new one. `width` was wrong in the way this project's naming rule catches: it says how wide something is, and this says nothing of the sort — `width: 200` meaning units was accepted, folded at two hundred characters, and did nothing visible. The reference had to carry a sentence explaining that the number was not a distance, which is the tell that a name is doing the wrong job.
56
56
 
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`.
57
+ `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 unrecognized value is an error naming it.
58
+
59
+ Every kind of text has a default, and `size:` overrides it exactly as `fill:` overrides the theme's color. Only `note` defaults to anything other than `normal`, and it defaults to `small`.
58
60
 
59
61
  Containment is a dotted name. A node named `server.docker` is inside `server`. The parent must be declared before the child.
60
62
 
@@ -66,10 +68,10 @@ box server.deploy "services deploy dir"
66
68
 
67
69
  A container is sized by its contents. Children stack vertically in written order unless a child carries a placement of its own.
68
70
 
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:
71
+ 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:
70
72
 
71
73
  ```
72
- style invisible fill: none stroke: none
74
+ style invisible fill: none border: none
73
75
 
74
76
  box hub "" style: invisible
75
77
  box hub.dropbox "Dropbox"
@@ -83,16 +85,18 @@ Here the server clears the whole cluster. Placed `left of hub.dropbox` instead,
83
85
  A container's label can say where in the box it goes, in brackets on the label itself:
84
86
 
85
87
  ```
86
- box docker "Docker" (at: bottom, align: centre) below deploy
88
+ box docker "Docker" (at: bottom, align: center) below deploy
87
89
  ```
88
90
 
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.
91
+ `at: top | bottom` says which end of the box the label sits at; the contents take the other end. `align: left | center | right` says how the text sits across it. The defaults are `top` and `left` in a container, and the comma is optional punctuation.
90
92
 
91
93
  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.
92
94
 
93
95
  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.
94
96
 
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.
97
+ A leaf has no contents, so there is no band and nothing for `at` to be at either end of; writing it on a childless box is an error. `align` is fine there, and its default is `center` rather than `left`, because a leaf's label is centered in its box. It is worth having: any label of more than one line — from a ` / ` break or from `wrap:` — has lines of unequal length, and how those sit across each other is a real question in a leaf as much as in a container.
98
+
99
+ Both are refused on a note, which has no box at all.
96
100
 
97
101
  `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.
98
102
 
@@ -116,16 +120,16 @@ A leaf's label is centred in its box with nothing to sit clear of, so it takes n
116
120
  box ext_hd "External HD" icon: disk
117
121
  ```
118
122
 
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.
123
+ 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 unrecognized 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.
120
124
 
121
125
  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".
122
126
 
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.
127
+ 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 centered 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.
124
128
 
125
129
  `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:
126
130
 
127
131
  ```
128
- style store fill: #142814 stroke: #486544 icon: database
132
+ style store fill: #142814 border: #486544 icon: database
129
133
  box records "Records" style: store
130
134
  ```
131
135
 
@@ -144,12 +148,12 @@ box svc "" shape: instance
144
148
 
145
149
  `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
150
 
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.
151
+ That distinction is worth having because it 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.
148
152
 
149
153
  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.
150
154
 
151
155
  ```
152
- box services "" fill: none stroke: none
156
+ box services "" fill: none border: none
153
157
  box services.web "web" shape: instance
154
158
  box services.api "api" right of services.web gap: tight shape: instance
155
159
  ```
@@ -158,7 +162,7 @@ box services.api "api" right of services.web gap: tight shape: instance
158
162
 
159
163
  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.
160
164
 
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.
165
+ 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 unrecognized name is an error listing what is available.
162
166
 
163
167
  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.
164
168
 
@@ -197,13 +201,13 @@ Nothing states how far apart `hub` and `side` are. They start one gap apart, and
197
201
 
198
202
  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
203
 
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.
204
+ **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
205
 
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.
206
+ **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
207
 
204
208
  **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.
205
209
 
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.
210
+ `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
211
 
208
212
  ```
209
213
  box dumps "" right of server.docker left of dropbox_and_machines level with server.docker
@@ -211,7 +215,7 @@ box dumps "" right of server.docker left of dropbox_and_machines level with s
211
215
 
212
216
  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
217
 
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.
218
+ Naming an edge in front of it aligns that edge instead of the center. `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.
215
219
 
216
220
  ### Several targets at once
217
221
 
@@ -222,11 +226,11 @@ note rotations "Weekly rotations …" right of bup_hd.borg and bup_hd.bare gap
222
226
  note archive "archive remains …" left of bup_hd.archive and bup_hd.par2 gap: tight
223
227
  ```
224
228
 
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.
229
+ Neither note 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 boxes, so each note lands centered on the pair it explains.
226
230
 
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.
231
+ 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 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
232
 
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:
233
+ 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
234
 
231
235
  ```
232
236
  box a "" shape: instance
@@ -236,7 +240,7 @@ box d "" below a and b gap: tight shape: instance
236
240
  box e "" below b and c gap: tight shape: instance
237
241
  ```
238
242
 
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.
243
+ 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
244
 
241
245
  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.
242
246
 
@@ -285,7 +289,7 @@ Every axis is solved as one system, so a target does not have to come first. Wha
285
289
 
286
290
  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.
287
291
 
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.
292
+ **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 centered 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.
289
293
 
290
294
  **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.
291
295
 
@@ -303,13 +307,13 @@ link <from> <-> <to> ["<label>"] [between <a> and <b> [vertically|horizontally]]
303
307
 
304
308
  `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.
305
309
 
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.
310
+ 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 labeled link claims room in the gap it crosses, which is the next section.
307
311
 
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.
312
+ A link's label breaks on ` / ` exactly as a node's does, and the block centers 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. `wrap:` is a node attribute and does not apply — a link label folds where you say and nowhere else.
309
313
 
310
314
  ### A label makes room for itself
311
315
 
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.
316
+ Putting something between two boxes is what pushes them apart, and a label drawn in a corridor is something in that corridor. So a labeled 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.
313
317
 
314
318
  ```
315
319
  box parser "Parser"
@@ -319,9 +323,9 @@ link parser -> resolver "statements" from: right to: left
319
323
 
320
324
  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.
321
325
 
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.
326
+ 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 traveling horizontally needs the label's width, one traveling vertically needs only its depth, so a long label across a vertical gap opens it by a single line and hangs out either side.
323
327
 
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.
328
+ An unlabeled 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.
325
329
 
326
330
  ### Which side a link leaves and arrives on
327
331
 
@@ -331,9 +335,9 @@ An unlabelled link asks for nothing, since every gap is wide enough for an arrow
331
335
  link computer1.files <-> dropbox from: right to: top
332
336
  ```
333
337
 
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.
338
+ 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 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
339
 
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.
340
+ You name a side and never a point on it. Alone on a side, a link lands at its center. 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.
337
341
 
338
342
  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.
339
343
 
@@ -356,7 +360,7 @@ A side too short to hold the whole group is squeezed, exactly as above — and t
356
360
 
357
361
  #### Several links with no side named at all
358
362
 
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.
363
+ 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 center 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.
360
364
 
361
365
  ```
362
366
  box a
@@ -398,7 +402,7 @@ link c -> d "threaded" between a and b vertically
398
402
 
399
403
  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.
400
404
 
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.
405
+ 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 labeled link runs, an arrow's width where none does.
402
406
 
403
407
  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.
404
408
 
@@ -410,7 +414,7 @@ note <name> "<text>" <placement> ...
410
414
 
411
415
  A note is text with no box, anchored to a node so it travels with it.
412
416
 
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.
417
+ Nothing bounds a note the way a border bounds a box, so a sentence-length note without a `wrap` is drawn as one very long line and will cross whatever is beside it. Give every note a wrap.
414
418
 
415
419
  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.
416
420
 
@@ -422,35 +426,111 @@ deck <name> "<label>" ["<label>" ...]
422
426
 
423
427
  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.
424
428
 
429
+ ## Attributes
430
+
431
+ Every attribute, and what takes one. The kinds here are what a statement *draws* rather than which keyword declared it: a node whose `shape:` names an icon is drawn as a glyph body and takes a different set from an ordinary box.
432
+
433
+ | attribute | box | note | glyph body | link | says |
434
+ |---|---|---|---|---|---|
435
+ | `style` | ✓ | ✓ | ✓ | ✓ | the named bundle to take appearance from |
436
+ | `size` | ✓ | ✓ | ✓ | ✓ | how big the text is set |
437
+ | `gap` | ✓ | ✓ | ✓ | | the default distance to whatever it is placed against |
438
+ | `overlap` | ✓ | ✓ | ✓ | | `allow`, to opt out of non-overlap |
439
+ | `wrap` | ✓ | ✓ | ✓ | | how many characters fit on a line before the label folds |
440
+ | `align` | ✓ | | | | `widths`, to widen every child to the widest of them |
441
+ | `icon` | ✓ | | | | the glyph that takes the column beside the label |
442
+ | `shape` | ✓ | | ✓ | | what the node is drawn as |
443
+ | `from` `to` | | | | ✓ | which side the line leaves and arrives on |
444
+ | `fill` | ✓ | | | | color — see "A color names the part it colors" |
445
+ | `border` | ✓ | | | | color |
446
+ | `text` | ✓ | ✓ | ✓ | ✓ | color |
447
+ | `subtext` | ✓ | | ✓ | | color |
448
+ | `line` | | | | ✓ | color |
449
+
450
+ 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 box is an error that points at `fill:`.
451
+
452
+ **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:
453
+
454
+ ```
455
+ "one" is a box and has from: left. `from:` belongs to a link — a box takes style, size, gap, ...
456
+ ```
457
+
458
+ Three of the gaps in the table are worth saying out loud, because each was silent until then and none of them looks like a mistake while you are writing it. A glyph body takes no `icon:` — it is drawn *as* a picture and has no box for a second one to sit in. A note and a glyph body take no `align:`, which widens a node's children, and neither may have any. A link takes no `gap:` or `overlap:` — those say where a box sits, and a link is not placed, it joins two things that are.
459
+
460
+ **Changed 2026-09-09.** Until then a node or link attribute the tool did not recognize was parsed, stored and never read: `wibble: red` on a box 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.
461
+
425
462
  ## Styles
426
463
 
427
464
  ```
428
465
  style <name> <attributes>
429
466
  ```
430
467
 
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.
468
+ A named bundle of appearance, applied with `style: <name>` on a node or link. Color carries meaning through the style name rather than being written per node.
432
469
 
433
470
  ```
434
- style backup stroke: #d2904e
471
+ style backup border: #d2904e
435
472
  box server.mirror "\\"important\\" mirror" style: backup
436
473
  ```
437
474
 
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.
475
+ The appearance attributes are `fill`, `border`, `text`, `line`, `subtext`, `size`, `icon` and `shape`. The first five each take a color written as the viewer will receive it — `#142814`, or any CSS color, or `none`.
476
+
477
+ ### A color names the part it colors
439
478
 
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.
479
+ A color attribute says which part of a thing it colors, and a part exists only on the kinds that have one:
441
480
 
442
- `subtext` colours every label line after the first, so a box can carry a name and a quieter qualifier under it:
481
+ | attribute | colors | on |
482
+ |---|---|---|
483
+ | `fill` | the area inside the outline | a box |
484
+ | `border` | the outline | a box |
485
+ | `text` | the label | a box, a note, a glyph body, a link |
486
+ | `line` | the drawn line and its arrowheads | a link |
487
+ | `subtext` | every label line after the first | a box, a glyph body |
443
488
 
489
+ 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 note is a mistake, not something to ignore, for the same reason an unknown `diagram` key is.
490
+
491
+ A link's label takes the line's color unless `text` says otherwise, so a link 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.
492
+
493
+ A style contributes a part only to the kinds that have it, so a style shared between boxes and links writes one key for each:
494
+
495
+ ```
496
+ style backup border: #d2904e line: #d2904e
444
497
  ```
445
- style synced fill: #142814 stroke: #486544 subtext: muted
498
+
499
+ The boxes take the border, the links take the line, and neither sees the other's word. Writing only `border` there would color the boxes and leave the links plain.
500
+
501
+ ### A style may carry what a thing cannot use
502
+
503
+ 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, a border and a subtext for five boxes and a `line` for the four links 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.
504
+
505
+ What is refused is a style that gives a thing **nothing at all**:
506
+
507
+ ```
508
+ style boxy fill: #142814 icon: disk
509
+ note n "An aside" style: boxy
510
+ ```
511
+
512
+ A note is bare text, with neither a fill nor an icon, 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`.
513
+
514
+ 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.
515
+
516
+ **Removed: `stroke`.** It named no part — it meant the border of a box, the *text* of a note or a glyph body, and the line of a link, 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 box, which is why `subtext` exists in the odd shape it does. An older file carrying it gets an error naming the word to use instead.
517
+
518
+ 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: `subtext: "medium-fine"` reads as the text that goes underneath, and every attribute that takes a color would otherwise accept the string, find it is not a color, and draw nothing without saying so. The qualifier under a name is a second line of the label, not a `subtext` value.
519
+
520
+ 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.
521
+
522
+ `subtext` colors every label line after the first, so a box can carry a name and a quieter qualifier under it:
523
+
524
+ ```
525
+ style synced fill: #142814 border: #486544 subtext: muted
446
526
  box pc.files "\"important\" directory / Dropbox-synced" style: synced
447
527
  ```
448
528
 
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.
529
+ `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.
450
530
 
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.
531
+ Bundling `size` into a style is how a size comes to mean something. `style aside size: small text: #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.
452
532
 
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.
533
+ `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 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.
454
534
 
455
535
  ## The diagram itself
456
536
 
@@ -464,16 +544,16 @@ Settings that belong to the drawing as a whole rather than to anything in it. Th
464
544
  diagram background: #111111
465
545
  ```
466
546
 
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.
547
+ One attribute so far. `background` takes a color the same way `fill` does, and it colors 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.
468
548
 
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.
549
+ 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
550
 
471
551
  ## What the language refuses
472
552
 
473
553
  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".
474
554
 
475
555
  - **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.
556
+ - **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 box shares, not how far it sits from something.
477
557
  - **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
558
  - **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
559
  - **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.
@@ -489,7 +569,7 @@ Designed, decided, and absent from the code. Written down so the next version ha
489
569
 
490
570
  **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.
491
571
 
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.
572
+ **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 labeled 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.
493
573
 
494
574
  ## Known to be wrong
495
575
 
@@ -497,7 +577,7 @@ Not omissions — defects, left here so nobody rediscovers them. Most were found
497
577
 
498
578
  ~~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.
499
579
 
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.
580
+ **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
581
 
502
582
  ~~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
583
 
@@ -505,7 +585,7 @@ Not omissions — defects, left here so nobody rediscovers them. Most were found
505
585
 
506
586
  ~~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
587
 
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.
588
+ ~~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
589
 
510
590
  ~~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.
511
591
 
@@ -515,20 +595,23 @@ Not omissions — defects, left here so nobody rediscovers them. Most were found
515
595
 
516
596
  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.
517
597
 
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.
598
+ ~~A labeled 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 labeled 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.
519
599
 
520
600
  ~~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".
521
601
 
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".
602
+ ~~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 center-to-center ray crosses the border, so no single link anywhere moved. See "Several links with no side named at all".
603
+
604
+ ~~An unknown attribute was ignored in silence.~~ Fixed. `wibble: red` on a box 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: `icon:` on a node already drawn as a glyph, `gap:` on a link. 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".
523
605
 
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.
606
+ ~~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 centers on the point the label already occupied, so a one-line label sits exactly where it did.
525
607
 
526
608
  ## Undecided
527
609
 
528
610
  Open questions the benchmark raised, recorded so a later session does not rediscover them.
529
611
 
530
612
  - 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.
613
+ - Four machines each holding an identically-labeled `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.
532
614
  - 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
615
  - ~~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
616
  - Nothing yet expresses one box spanning several rows of a parallel column, which the OSI reference render needs.
617
+ - **How contents sit inside a container that is wider than they are.** A container is as wide as the wider of its title and its contents, and when the title wins, the children sit against the left of the slack — because every member goes at the smallest position its constraints allow, and nothing pushes them right. The column then reads as ragged inside a box whose own label may well be centered. `align: widths` already means "how the contents sit", so the key is the obvious home for a second, independent word; what is not settled is whether centering is one word there, whether it should be sayable per axis, and whether it is the same question as the bracketed node sitting against one side of its slack under "Known to be wrong" — which smells like it is. Automatic centering is ruled out either way: it would move every existing diagram whose container title is wider than its contents.
package/dist/ast.d.ts CHANGED
@@ -7,10 +7,10 @@ export type Direction = (typeof DIRECTIONS)[number];
7
7
  export declare function isDirection(word: string): word is Direction;
8
8
  export type Axis = 'x' | 'y';
9
9
  /**
10
- * Which edge of the target a `level with` shares. `centre` is the plain form;
10
+ * Which edge of the target a `level with` shares. `center` is the plain form;
11
11
  * the rest are written in front of it, as in `top level with media`.
12
12
  */
13
- export declare const EDGES: readonly ["centre", "top", "bottom", "left", "right"];
13
+ export declare const EDGES: readonly ["center", "top", "bottom", "left", "right"];
14
14
  export type Edge = (typeof EDGES)[number];
15
15
  /** An edge belongs to one axis, so an alignment never has to say which. */
16
16
  export declare const EDGE_AXIS: Record<Edge, Axis>;
@@ -40,7 +40,7 @@ export interface OffsetPlacement {
40
40
  gap?: string;
41
41
  line: number;
42
42
  }
43
- /** `level with docker` — share an edge or a centre line, with no gap in between. */
43
+ /** `level with docker` — share an edge or a center line, with no gap in between. */
44
44
  export interface AlignPlacement {
45
45
  kind: 'align';
46
46
  axis: Axis;
@@ -55,7 +55,7 @@ export interface AlignPlacement {
55
55
  export type Placement = OffsetPlacement | AlignPlacement;
56
56
  /**
57
57
  * The modifiers a placement understands, in brackets after its targets. Refused
58
- * by name when unrecognised, for the reason `DIAGRAM_KEYS` are: a modifier that
58
+ * by name when unrecognized, for the reason `DIAGRAM_KEYS` are: a modifier that
59
59
  * silently does nothing looks like a bug in the tool rather than a typo.
60
60
  */
61
61
  export declare const PLACEMENT_KEYS: readonly ["gap"];
@@ -87,7 +87,7 @@ export declare function describeAxis(axis: Axis): string;
87
87
  /** `key: value` pairs trailing a statement. Values are always strings here. */
88
88
  export type Attrs = Record<string, string>;
89
89
  /**
90
- * What a label's brackets may say: `"Docker" (at: bottom, align: centre)`.
90
+ * What a label's brackets may say: `"Docker" (at: bottom, align: center)`.
91
91
  *
92
92
  * They are bracketed onto the label rather than written among the node's
93
93
  * attributes for the same reason a gap is bracketed onto its placement — they
@@ -148,6 +148,74 @@ export interface DiagramStmt {
148
148
  }
149
149
  /** The attributes a `diagram` statement understands. */
150
150
  export declare const DIAGRAM_KEYS: readonly ["background"];
151
+ /**
152
+ * The attributes whose value is a color rather than text. A color is written
153
+ * as the viewer will receive it and the renderer keeps no list of color words
154
+ * of its own, so there is nothing to check a value *against* — but quoting is
155
+ * the author saying "this is text", and an unquoted value cannot hold a space,
156
+ * so prose has to be quoted to get in at all. Refusing a quoted color is
157
+ * therefore the whole of what can be checked here, and it happens to be the
158
+ * mistake people actually make: `subtext: "medium-fine"` reads as the text
159
+ * that goes underneath, and was accepted and dropped in silence.
160
+ */
161
+ export declare const COLOR_KEYS: readonly ["fill", "border", "text", "line", "subtext", "background"];
162
+ /**
163
+ * A color attribute names the *part* it colors, and a part exists only on the
164
+ * kinds that have one. A box has a border and text; a note and a glyph body are
165
+ * text and nothing else; a link is a line and its label.
166
+ *
167
+ * This table is what makes the words checkable. `border:` on a note is refused
168
+ * by name rather than ignored — the same rule as an unknown `diagram` key, and
169
+ * for the same reason: an attribute that silently does nothing looks like the
170
+ * tool being broken.
171
+ *
172
+ * A style spanning kinds writes one key per kind — `border: #d2904e line:
173
+ * #d2904e` — since a style contributes a part only to the kinds that have it.
174
+ * That is what replaced `stroke:`, which named no part and so could never be
175
+ * wrong, and which is why a box's text had no word of its own until now.
176
+ */
177
+ export declare const COLOR_PARTS: Record<Kind, readonly string[]>;
178
+ /**
179
+ * The four things an attribute can be written on. A glyph is a node whose
180
+ * `shape:` names an icon, so it is not a statement keyword — but it takes a
181
+ * different set of attributes from an ordinary box, which is what makes it a
182
+ * kind here.
183
+ */
184
+ export type Kind = 'box' | 'note' | 'glyph' | 'link';
185
+ /**
186
+ * Every attribute each kind understands. An attribute a kind has no use for is
187
+ * refused by name rather than dropped, the same rule as an unknown `diagram`
188
+ * key, a `PLACEMENT_KEYS` modifier or a color part — and for the same reason,
189
+ * which the color parts only closed one level down: a key that silently does
190
+ * nothing looks like the tool being broken rather than like a typo.
191
+ *
192
+ * The color entries repeat `COLOR_PARTS` and must agree with it. They are
193
+ * written out rather than spliced in because this table is the answer to "what
194
+ * may I write here", and a reader of it should not have to assemble the list
195
+ * from two places.
196
+ *
197
+ * Three of the exclusions are the whole of what this table decides, and each is
198
+ * a place the old silence hid something:
199
+ *
200
+ * - A glyph takes no `icon:`. It is drawn *as* a picture and has no box for a
201
+ * second one to sit in; `sizeNode` returns before it would ever be read.
202
+ * - A glyph and a note take no `align:`, which widens a node's children, and
203
+ * neither may have any.
204
+ * - A link takes no `gap:` or `overlap:`. Those are about where a box sits, and
205
+ * a link is not placed — it joins two things that are.
206
+ */
207
+ export declare const ATTR_KEYS: Record<Kind, readonly string[]>;
208
+ /**
209
+ * Every word that is an attribute *somewhere*, which is what separates a
210
+ * misspelling from a key written on the wrong kind of thing. The two deserve
211
+ * different errors: one has no remedy but the spelling, the other has a real
212
+ * meaning somewhere else in the file.
213
+ *
214
+ * `DIAGRAM_KEYS` is in here so that `background:` on a box is understood to be
215
+ * a real word in the wrong place — that mistake wants to be pointed at `fill:`,
216
+ * not told the word does not exist.
217
+ */
218
+ export declare const ALL_ATTR_KEYS: readonly string[];
151
219
  export interface StyleStmt {
152
220
  kind: 'style';
153
221
  name: string;