reladraw 0.0.1 → 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/LICENSE +202 -0
- package/NOTICE +14 -0
- package/README.md +135 -2
- package/SYNTAX.md +617 -0
- package/dist/ast.d.ts +228 -0
- package/dist/ast.js +171 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +72 -0
- package/dist/constants.d.ts +145 -0
- package/dist/constants.js +189 -0
- package/dist/constrain.d.ts +56 -0
- package/dist/constrain.js +95 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.js +14 -0
- package/dist/grammar.d.ts +103 -0
- package/dist/grammar.js +215 -0
- package/dist/icons.d.ts +86 -0
- package/dist/icons.js +166 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +16 -0
- package/dist/lexer.d.ts +25 -0
- package/dist/lexer.js +91 -0
- package/dist/measure.d.ts +38 -0
- package/dist/measure.js +66 -0
- package/dist/model.d.ts +78 -0
- package/dist/model.js +1 -0
- package/dist/parser.d.ts +3 -0
- package/dist/parser.js +459 -0
- package/dist/render.d.ts +31 -0
- package/dist/render.js +1180 -0
- package/dist/resolve.d.ts +22 -0
- package/dist/resolve.js +1130 -0
- package/package.json +42 -4
package/SYNTAX.md
ADDED
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
# Syntax reference — v0
|
|
2
|
+
|
|
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
|
+
|
|
5
|
+
The worked example is `examples/arch.reladraw`, transcribed from the reference render beside it. Nearly every construct here exists because that diagram demanded it. The exception is non-overlap, which that diagram never triggers, so `examples/separation.reladraw` covers it instead.
|
|
6
|
+
|
|
7
|
+
## Shape of the file
|
|
8
|
+
|
|
9
|
+
One statement per line. No multi-line statements, no line continuations, no blocks.
|
|
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.
|
|
12
|
+
|
|
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
|
+
|
|
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
|
+
|
|
17
|
+
```
|
|
18
|
+
box server "My Server Machine" left of dropbox gap: wide icon: desktop
|
|
19
|
+
^ ^ ^ ^
|
|
20
|
+
| | | attributes
|
|
21
|
+
| | placements
|
|
22
|
+
| text
|
|
23
|
+
name
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Attribute values are a single bare word unless quoted. No commas between attributes.
|
|
27
|
+
|
|
28
|
+
## Nodes
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
box <name> ["<text>"] [<placement> ...] [attributes]
|
|
32
|
+
```
|
|
33
|
+
|
|
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
|
+
|
|
36
|
+
The text is optional, and a box without it is labeled with its own name:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
box parser
|
|
40
|
+
box resolver right of parser
|
|
41
|
+
```
|
|
42
|
+
|
|
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.
|
|
48
|
+
|
|
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.
|
|
50
|
+
|
|
51
|
+
For a label that wants a spaced slash and no break, `\/` escapes it: `"Before \/ After"` is one line. The escapes are `\"`, `\\` and `\/`.
|
|
52
|
+
|
|
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
|
+
|
|
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
|
+
|
|
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`.
|
|
60
|
+
|
|
61
|
+
Containment is a dotted name. A node named `server.docker` is inside `server`. The parent must be declared before the child.
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
box server "My Server Machine" left of dropbox
|
|
65
|
+
box server.mirror "\\"important\\" mirror"
|
|
66
|
+
box server.deploy "services deploy dir"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
A container is sized by its contents. Children stack vertically in written order unless a child carries a placement of its own.
|
|
70
|
+
|
|
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:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
style invisible fill: none border: none
|
|
75
|
+
|
|
76
|
+
box hub "" style: invisible
|
|
77
|
+
box hub.dropbox "Dropbox"
|
|
78
|
+
box hub.computer1 "Computer 1" above-left of hub.dropbox
|
|
79
|
+
|
|
80
|
+
box server "My Server Machine" left of hub
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
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.
|
|
84
|
+
|
|
85
|
+
A container's label can say where in the box it goes, in brackets on the label itself:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
box docker "Docker" (at: bottom, align: center) below deploy
|
|
89
|
+
```
|
|
90
|
+
|
|
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.
|
|
92
|
+
|
|
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.
|
|
94
|
+
|
|
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.
|
|
96
|
+
|
|
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.
|
|
100
|
+
|
|
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.
|
|
102
|
+
|
|
103
|
+
### Icons
|
|
104
|
+
|
|
105
|
+
`icon: <name>` puts a small glyph beside the box's label. There are seven:
|
|
106
|
+
|
|
107
|
+
| Name | What it means |
|
|
108
|
+
| --- | --- |
|
|
109
|
+
| `disk` | A physical drive — the hardware, not the filesystem on it. |
|
|
110
|
+
| `desktop` | A workstation. |
|
|
111
|
+
| `laptop` | A portable machine. |
|
|
112
|
+
| `package` | Something stored as a whole rather than run: an archive, a bucket, a sync root. |
|
|
113
|
+
| `cubes` | Several interchangeable units of the same kind. |
|
|
114
|
+
| `instance` | One of them. |
|
|
115
|
+
| `database` | A store queried rather than read as files. |
|
|
116
|
+
|
|
117
|
+
`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.
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
box ext_hd "External HD" icon: disk
|
|
121
|
+
```
|
|
122
|
+
|
|
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.
|
|
124
|
+
|
|
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".
|
|
126
|
+
|
|
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.
|
|
128
|
+
|
|
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:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
style store fill: #142814 border: #486544 icon: database
|
|
133
|
+
box records "Records" style: store
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
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.
|
|
137
|
+
|
|
138
|
+
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.
|
|
139
|
+
|
|
140
|
+
### Shapes
|
|
141
|
+
|
|
142
|
+
`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.
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
box dump "pg_dump output / (DB 1)" shape: document
|
|
146
|
+
box svc "" shape: instance
|
|
147
|
+
```
|
|
148
|
+
|
|
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*.
|
|
150
|
+
|
|
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.
|
|
152
|
+
|
|
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.
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
box services "" fill: none border: none
|
|
157
|
+
box services.web "web" shape: instance
|
|
158
|
+
box services.api "api" right of services.web gap: tight shape: instance
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**`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.
|
|
162
|
+
|
|
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.
|
|
164
|
+
|
|
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.
|
|
166
|
+
|
|
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.
|
|
168
|
+
|
|
169
|
+
## Placement
|
|
170
|
+
|
|
171
|
+
A *placement* says one thing about where a node goes. A node carries as many as it needs.
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
above X below X left of X right of X
|
|
175
|
+
above-left of X above-right of X below-left of X below-right of X
|
|
176
|
+
level with X top level with X bottom level with X
|
|
177
|
+
left level with X right level with X
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Any of them may name more than one target — `right of borg and bare`, `level with borg, bare and media`. See [Several targets at once](#several-targets-at-once).
|
|
181
|
+
|
|
182
|
+
`of` is optional after any direction, so `below X` and `below of X` both parse. Write whichever reads as English.
|
|
183
|
+
|
|
184
|
+
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).
|
|
185
|
+
|
|
186
|
+
Exactly one node in the document may be left unplaced. Everything else is positioned, directly or transitively, relative to it.
|
|
187
|
+
|
|
188
|
+
### How placements combine
|
|
189
|
+
|
|
190
|
+
**A gap sets a minimum distance rather than an exact one,** so everything ends up as close together as your placements allow.
|
|
191
|
+
|
|
192
|
+
That single rule is what makes a corridor work. Say two things sit side by side, then put a third between them:
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
box hub "Hub"
|
|
196
|
+
box side "Side" left of hub
|
|
197
|
+
box wedge "Wedged" right of side left of hub level with hub
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
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.
|
|
201
|
+
|
|
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.
|
|
203
|
+
|
|
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.
|
|
205
|
+
|
|
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.
|
|
207
|
+
|
|
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.
|
|
209
|
+
|
|
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.
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
box dumps "" right of server.docker left of dropbox_and_machines level with server.docker
|
|
214
|
+
```
|
|
215
|
+
|
|
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.
|
|
217
|
+
|
|
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.
|
|
219
|
+
|
|
220
|
+
### Several targets at once
|
|
221
|
+
|
|
222
|
+
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.
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
note rotations "Weekly rotations …" right of bup_hd.borg and bup_hd.bare gap: tight
|
|
226
|
+
note archive "archive remains …" left of bup_hd.archive and bup_hd.par2 gap: tight
|
|
227
|
+
```
|
|
228
|
+
|
|
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.
|
|
230
|
+
|
|
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.
|
|
232
|
+
|
|
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:
|
|
234
|
+
|
|
235
|
+
```
|
|
236
|
+
box a "" shape: instance
|
|
237
|
+
box b "" right of a gap: tight shape: instance
|
|
238
|
+
box c "" right of b gap: tight shape: instance
|
|
239
|
+
box d "" below a and b gap: tight shape: instance
|
|
240
|
+
box e "" below b and c gap: tight shape: instance
|
|
241
|
+
```
|
|
242
|
+
|
|
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.
|
|
244
|
+
|
|
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.
|
|
246
|
+
|
|
247
|
+
### A gap belongs to the placement
|
|
248
|
+
|
|
249
|
+
A gap is a fact about a relationship, not about a box, so it is written on the placement that names that relationship:
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
box dumps "" right of server.docker left of machines (gap: wide) level with server.docker
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
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.
|
|
256
|
+
|
|
257
|
+
`gap:` written as an ordinary attribute still works and is the node's default, used by every placement that does not name its own:
|
|
258
|
+
|
|
259
|
+
```
|
|
260
|
+
box stack "Stack" below wedge right of wall (gap: tight) gap: wide
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Wide below the wedge, tight to the right of the wall.
|
|
264
|
+
|
|
265
|
+
**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:
|
|
266
|
+
|
|
267
|
+
```
|
|
268
|
+
box parser "Parser" gap: wide
|
|
269
|
+
box renderer "Renderer" right of parser
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
`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.
|
|
273
|
+
|
|
274
|
+
Where both ends state a gap the larger applies, since a gap is a minimum either way. A gap in brackets is not a default and is not overruled: it is the specific statement about that one pair, so it wins outright. If you mean a gap to govern one placement and not the whole node, that is what the brackets are for.
|
|
275
|
+
|
|
276
|
+
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.
|
|
277
|
+
|
|
278
|
+
### Edge to edge
|
|
279
|
+
|
|
280
|
+
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.
|
|
281
|
+
|
|
282
|
+
### Targets
|
|
283
|
+
|
|
284
|
+
The target of a placement may be a child of another container. `right of server.docker` places a top-level node against something nested. Containers scope names; they do not scope placement.
|
|
285
|
+
|
|
286
|
+
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.
|
|
287
|
+
|
|
288
|
+
## Boxes do not overlap
|
|
289
|
+
|
|
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.
|
|
291
|
+
|
|
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.
|
|
293
|
+
|
|
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.
|
|
295
|
+
|
|
296
|
+
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.
|
|
297
|
+
|
|
298
|
+
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.
|
|
299
|
+
|
|
300
|
+
## Links
|
|
301
|
+
|
|
302
|
+
```
|
|
303
|
+
link <from> -> <to> ["<label>"] [between <a> and <b> [vertically|horizontally]] [attributes]
|
|
304
|
+
link <to> <- <from> ["<label>"] [between <a> and <b> [vertically|horizontally]] [attributes]
|
|
305
|
+
link <from> <-> <to> ["<label>"] [between <a> and <b> [vertically|horizontally]] [attributes]
|
|
306
|
+
```
|
|
307
|
+
|
|
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.
|
|
309
|
+
|
|
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.
|
|
311
|
+
|
|
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.
|
|
313
|
+
|
|
314
|
+
### A label makes room for itself
|
|
315
|
+
|
|
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.
|
|
317
|
+
|
|
318
|
+
```
|
|
319
|
+
box parser "Parser"
|
|
320
|
+
box resolver "Resolver" right of parser
|
|
321
|
+
link parser -> resolver "statements" from: right to: left
|
|
322
|
+
```
|
|
323
|
+
|
|
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.
|
|
325
|
+
|
|
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.
|
|
327
|
+
|
|
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.
|
|
329
|
+
|
|
330
|
+
### Which side a link leaves and arrives on
|
|
331
|
+
|
|
332
|
+
`from:` and `to:` name a side of the box at each end — `top`, `bottom`, `left` or `right`.
|
|
333
|
+
|
|
334
|
+
```
|
|
335
|
+
link computer1.files <-> dropbox from: right to: top
|
|
336
|
+
```
|
|
337
|
+
|
|
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.
|
|
339
|
+
|
|
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.
|
|
341
|
+
|
|
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.
|
|
343
|
+
|
|
344
|
+
#### Several links between the same two sides
|
|
345
|
+
|
|
346
|
+
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.
|
|
347
|
+
|
|
348
|
+
```
|
|
349
|
+
link gateway -> queue "publishes messages" from: bottom to: left
|
|
350
|
+
link queue -> gateway "receives messages" from: left to: bottom
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
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.
|
|
354
|
+
|
|
355
|
+
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.
|
|
356
|
+
|
|
357
|
+
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.
|
|
358
|
+
|
|
359
|
+
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.
|
|
360
|
+
|
|
361
|
+
#### Several links with no side named at all
|
|
362
|
+
|
|
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.
|
|
364
|
+
|
|
365
|
+
```
|
|
366
|
+
box a
|
|
367
|
+
box b right of a
|
|
368
|
+
link a -> b "first"
|
|
369
|
+
link a -> b "second"
|
|
370
|
+
link a -> b "third"
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
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.
|
|
374
|
+
|
|
375
|
+
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.
|
|
376
|
+
|
|
377
|
+
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.
|
|
378
|
+
|
|
379
|
+
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.
|
|
380
|
+
|
|
381
|
+
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.
|
|
382
|
+
|
|
383
|
+
### Passing between two things
|
|
384
|
+
|
|
385
|
+
`between <a> and <b>` says that the line travels down the gap between two named nodes.
|
|
386
|
+
|
|
387
|
+
```
|
|
388
|
+
link dumps.db1 -> dropbox "rclone" between computer1 and computer2 from: right to: left
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
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.
|
|
392
|
+
|
|
393
|
+
Which gap is meant is usually derived, not stated. One of the two is above the other, or one is left of the other, and whichever it is says which axis the gap binds — so a channel between something above and something below constrains height, and one between something left and something right constrains width, and in neither case does the file mention an axis at all.
|
|
394
|
+
|
|
395
|
+
Two nodes sitting diagonally have *two* gaps between them, and there the derivation has nothing to go on. Add `vertically` or `horizontally`:
|
|
396
|
+
|
|
397
|
+
```
|
|
398
|
+
link c -> d "threaded" between a and b vertically
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
`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.
|
|
402
|
+
|
|
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.
|
|
404
|
+
|
|
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.
|
|
406
|
+
|
|
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.
|
|
408
|
+
|
|
409
|
+
## Notes
|
|
410
|
+
|
|
411
|
+
```
|
|
412
|
+
note <name> "<text>" <placement> ...
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
A note is text with no box, anchored to a node so it travels with it.
|
|
416
|
+
|
|
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.
|
|
418
|
+
|
|
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.
|
|
420
|
+
|
|
421
|
+
## Decks
|
|
422
|
+
|
|
423
|
+
```
|
|
424
|
+
deck <name> "<label>" ["<label>" ...]
|
|
425
|
+
```
|
|
426
|
+
|
|
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.
|
|
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
|
+
|
|
462
|
+
## Styles
|
|
463
|
+
|
|
464
|
+
```
|
|
465
|
+
style <name> <attributes>
|
|
466
|
+
```
|
|
467
|
+
|
|
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.
|
|
469
|
+
|
|
470
|
+
```
|
|
471
|
+
style backup border: #d2904e
|
|
472
|
+
box server.mirror "\\"important\\" mirror" style: backup
|
|
473
|
+
```
|
|
474
|
+
|
|
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
|
|
478
|
+
|
|
479
|
+
A color attribute says which part of a thing it colors, and a part exists only on the kinds that have one:
|
|
480
|
+
|
|
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 |
|
|
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
|
|
497
|
+
```
|
|
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
|
|
526
|
+
box pc.files "\"important\" directory / Dropbox-synced" style: synced
|
|
527
|
+
```
|
|
528
|
+
|
|
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.
|
|
530
|
+
|
|
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.
|
|
532
|
+
|
|
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.
|
|
534
|
+
|
|
535
|
+
## The diagram itself
|
|
536
|
+
|
|
537
|
+
```
|
|
538
|
+
diagram <attributes>
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
Settings that belong to the drawing as a whole rather than to anything in it. There is no name, because a file holds one diagram, and a second `diagram` statement is an error rather than a second opinion.
|
|
542
|
+
|
|
543
|
+
```
|
|
544
|
+
diagram background: #111111
|
|
545
|
+
```
|
|
546
|
+
|
|
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.
|
|
548
|
+
|
|
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".
|
|
550
|
+
|
|
551
|
+
## What the language refuses
|
|
552
|
+
|
|
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".
|
|
554
|
+
|
|
555
|
+
- **Coordinates**, in any form, including as an escape hatch.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
560
|
+
- **Set-level placement.** Four siblings around a hub are four statements today. Whether a durable group that reflows when a member is added is worth the same-axis conflict it introduces is undecided.
|
|
561
|
+
|
|
562
|
+
Note what is *not* on this list: saying more about where something goes. A statement that lets you be more precise is not a step toward auto-layout, and the first version was short enough of them to render the benchmark wrong.
|
|
563
|
+
|
|
564
|
+
## Not built yet
|
|
565
|
+
|
|
566
|
+
Designed, decided, and absent from the code. Written down so the next version has somewhere to start.
|
|
567
|
+
|
|
568
|
+
**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".
|
|
569
|
+
|
|
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.
|
|
571
|
+
|
|
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.
|
|
573
|
+
|
|
574
|
+
## Known to be wrong
|
|
575
|
+
|
|
576
|
+
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.
|
|
577
|
+
|
|
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.
|
|
579
|
+
|
|
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.
|
|
581
|
+
|
|
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.
|
|
583
|
+
|
|
584
|
+
~~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.
|
|
585
|
+
|
|
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.
|
|
587
|
+
|
|
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.
|
|
589
|
+
|
|
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.
|
|
591
|
+
|
|
592
|
+
~~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.
|
|
593
|
+
|
|
594
|
+
~~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`.
|
|
595
|
+
|
|
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.
|
|
597
|
+
|
|
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.
|
|
599
|
+
|
|
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".
|
|
601
|
+
|
|
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".
|
|
605
|
+
|
|
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.
|
|
607
|
+
|
|
608
|
+
## Undecided
|
|
609
|
+
|
|
610
|
+
Open questions the benchmark raised, recorded so a later session does not rediscover them.
|
|
611
|
+
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|