reladraw 0.19.1 → 0.21.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/README.md +2 -2
- package/SYNTAX.md +59 -8
- package/dist/ast.d.ts +26 -2
- package/dist/ast.js +15 -2
- package/dist/checks.d.ts +1 -0
- package/dist/checks.js +79 -5
- package/dist/cli.js +35 -12
- package/dist/constants.d.ts +4 -0
- package/dist/constants.js +8 -0
- package/dist/constrain.d.ts +56 -0
- package/dist/constrain.js +95 -0
- package/dist/grammar.js +2 -0
- package/dist/model.d.ts +16 -1
- package/dist/model.js +7 -0
- package/dist/page.d.ts +14 -0
- package/dist/page.js +488 -0
- package/dist/parser.js +69 -4
- package/dist/place.d.ts +74 -1
- package/dist/place.js +303 -18
- package/dist/render.d.ts +17 -0
- package/dist/render.js +101 -29
- package/dist/resolve.d.ts +14 -0
- package/dist/resolve.js +34 -7
- package/dist/themes.d.ts +1 -0
- package/dist/themes.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ npm install -g reladraw
|
|
|
45
45
|
reladraw render diagram.reladraw -o diagram.svg
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
`reladraw check diagram.reladraw` prints the warnings without writing a picture, and `reladraw format diagram.reladraw` lines its statements up in columns.
|
|
48
|
+
`reladraw check diagram.reladraw` prints the warnings without writing a picture, and `reladraw format diagram.reladraw` lines its statements up in columns. `reladraw render diagram.reladraw --page` writes a page for looking at it in a browser, with pan, zoom, full screen and the warnings beside it.
|
|
49
49
|
|
|
50
50
|
Or from a clone, which also gets you the examples:
|
|
51
51
|
|
|
@@ -88,7 +88,7 @@ That gets you a copy of the skill at the time you run it, so you'll need to re-r
|
|
|
88
88
|
|
|
89
89
|
## Status
|
|
90
90
|
|
|
91
|
-
Version 0.
|
|
91
|
+
Version 0.21.0. Early stage, but works. The parser, layout engine, and SVG renderer are written in TypeScript, with zero runtime dependencies. There is a command-line tool that turns a .reladraw text file into an SVG, and an element that does the same inside a web page.
|
|
92
92
|
|
|
93
93
|
## License
|
|
94
94
|
|
package/SYNTAX.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Syntax reference — 0.
|
|
1
|
+
# Syntax reference — 0.21.0
|
|
2
2
|
|
|
3
3
|
What the language accepts. The changelog at the end lists what changed in each published version; 0.17.0's changes are large, and its entry has a table of every old spelling and what to write instead.
|
|
4
4
|
|
|
@@ -220,7 +220,7 @@ The text keeps its own position words, so `node d1 "Desktop 1 / Linux" (inside t
|
|
|
220
220
|
|
|
221
221
|
With an empty text, `node flag "" badge: disk`, the node is exactly the picture and its padding, since an empty text takes no room.
|
|
222
222
|
|
|
223
|
-
**A picture that is a thing of its own is a child**, placed like any other: `node drive.mark icon: disk
|
|
223
|
+
**A picture that is a thing of its own is a child**, placed like any other: `node drive.mark icon: disk 1 right of drive text` beside the text. Placed against the text without `join-with:`, it is not part of the label: the text stays where it would be without it, and the box grows to hold both. In the corner of a box its other children size, `node laptop.mark icon: laptop inside laptop top-right stays above laptop.photos` ([Against its own parent](#against-its-own-parent)). A corner of a box with nothing in it but its text is on the text.
|
|
224
224
|
|
|
225
225
|
### Padding
|
|
226
226
|
|
|
@@ -241,7 +241,7 @@ above-left of X above-right of X below-left of X below-right of X
|
|
|
241
241
|
|
|
242
242
|
**A gap is exactly what was said.** `right of a` puts the box a `normal` gap from `a`'s right side. The gap goes in the placement's bracket, `right of a (gap: tight)`, or as a distance in front, `2 right of a`; writing both is refused as said twice. A diagonal distance is per axis: `2 below-right of a` is 2 across and 2 down.
|
|
243
243
|
|
|
244
|
-
**A lone direction also centers on its target.** `right of a` alone puts the box level with `a`'s center, because walking right from something keeps you on its center line. That half gives way as soon as another placement says how high.
|
|
244
|
+
**A lone direction also centers on its target.** `right of a` alone puts the box level with `a`'s center, because walking right from something keeps you on its center line. That half gives way as soon as another placement says how high, whichever box it is written on: with `node b right of a` and `node a even with b.1`, B goes where B.1 is level with A, rather than centered on it.
|
|
245
245
|
|
|
246
246
|
```
|
|
247
247
|
node parser
|
|
@@ -316,7 +316,7 @@ A target need not be written first, but there must be an order in which each box
|
|
|
316
316
|
|
|
317
317
|
### One anchor
|
|
318
318
|
|
|
319
|
-
Exactly one box in the file places itself, and every other is placed relative to it, directly or through others. Two unplaced boxes are refused by name. Chaining, `b right of a` then `c right of b`, is the plain way to write a few in a row; `layout:` ([Containers](#containers)) is for "these, in a line, in written order". Place against neighbors rather than hanging many boxes off one small one: a gap from a small box says nothing about the large ones also hanging off it.
|
|
319
|
+
Exactly one box in the file places itself, and every other is placed relative to it, directly or through others. Two unplaced boxes are refused by name. The anchor may be split between two boxes, one saying nothing across and the other nothing up and down: with `node a "A" even with b.1` and `node b "B" right of a`, A says nothing across, and B, once its centering on A gives way, nothing up and down. Chaining, `b right of a` then `c right of b`, is the plain way to write a few in a row; `layout:` ([Containers](#containers)) is for "these, in a line, in written order". Place against neighbors rather than hanging many boxes off one small one: a gap from a small box says nothing about the large ones also hanging off it.
|
|
320
320
|
|
|
321
321
|
### A box goes where it is placed
|
|
322
322
|
|
|
@@ -566,7 +566,20 @@ A unit that includes a box's text is placed by the text's position words: on a l
|
|
|
566
566
|
|
|
567
567
|
### Against its own parent
|
|
568
568
|
|
|
569
|
-
A child placed against its parent's side or corner, `inside server right` or `inside server top-left`, is placed against the finished box and never grows it. If it covers something, that is reported. To make the box hold something, place it relative to the other children
|
|
569
|
+
A child placed against its parent's side or corner, `inside server right` or `inside server top-left`, is placed against the finished box and never grows it. That holds one axis at a time: `node c.j "j" right of c.k inside c top` is beside `c.k` across, so the box holds it across, and against the box's top down. A corner and a sibling saying the same axis, `inside c top-right right of c.k`, are refused: the error offers `right of c.k inside c top` and `inside c top-right stays right of c.k` (below). If it covers something, that is reported. So is a child the box has no room for in that corner, such as a two-line picture in the corner of a box one line tall, as `node-overflow`; the box doesn't grow to fit it, since a box only just tall enough would put it level with the middle, not in the corner. To make the box hold something, place it relative to the other children, or keep it in the corner and clear of them with `stays`. Something the box holds may not be placed against a thing on its own side or corner, since the box would then depend on itself; that is refused.
|
|
570
|
+
|
|
571
|
+
**`stays` keeps a child clear of the rest of its box.** A picture in the corner of a box its children size can land on one of them. `stays above`, `stays below`, `stays left of` and `stays right of` say which way to keep clear:
|
|
572
|
+
|
|
573
|
+
```
|
|
574
|
+
node laptop "Laptop" (inside top, align: left)
|
|
575
|
+
node laptop.docs "Documents"
|
|
576
|
+
node laptop.photos "Photos" right of laptop.docs
|
|
577
|
+
node laptop.mark icon: laptop inside laptop top-right stays above laptop.photos
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
The laptop is still in the corner; `stays` never says where something goes. It says the laptop is at least the usual gap above Photos, and when it isn't, the box makes room: everything else it holds moves down together, so no gap a placement states changes, and the box grows by just enough. `(gap: wide)` or a number after the target asks for a different distance. Beside the box's own text, `stays right of drive text`, the box widens until the picture clears the text.
|
|
581
|
+
|
|
582
|
+
A limit relates two things in one box that nothing else ties together: a child placed against the box's side or corner and another child, or the box's text; two children in different corners; something beside the text and the children below it. Between two children placed against each other, or across boxes, every distance is already stated, so a limit there is refused. So is one the box can't meet by growing, such as `inside laptop bottom-right stays above laptop.photos`, and one a stated size leaves no room for.
|
|
570
583
|
|
|
571
584
|
A title inside the border counts as content, so the box makes room for it. So does anything placed against the text. A title outside the border hangs off the box and doesn't count.
|
|
572
585
|
|
|
@@ -1016,10 +1029,18 @@ The default order: a container before its own children, boxes before lines, an e
|
|
|
1016
1029
|
| `(color: …)` after a text | the text | any node, an edge; `text: (color: …)` in a style or default |
|
|
1017
1030
|
| `line` | the line and its end marks | an edge |
|
|
1018
1031
|
|
|
1019
|
-
A key means one thing everywhere, so it can be wrong: `border:` on a node with no outline is refused, and the error lists what that kind has. A color is written as the viewer receives it: a hex value, any CSS color, or a theme color. There is no list of color words: `green` works because it is a CSS color. A quoted color is refused
|
|
1032
|
+
A key means one thing everywhere, so it can be wrong: `border:` on a node with no outline is refused, and the error lists what that kind has. A color is written as the viewer receives it: a hex value, any CSS color, or a theme color. There is no list of color words: `green` works because it is a CSS color. A quoted color is refused, and so is a hex value without its `#`. `none` means the part isn't painted: `fill: none`.
|
|
1020
1033
|
|
|
1021
1034
|
**Changing a color never moves anything.** Size is different: a bigger text or a thicker line takes more room.
|
|
1022
1035
|
|
|
1036
|
+
### A border's bracket
|
|
1037
|
+
|
|
1038
|
+
```
|
|
1039
|
+
node host "Host" border: (color: #d2904e, thickness: thick)
|
|
1040
|
+
```
|
|
1041
|
+
|
|
1042
|
+
`border: red` is short for `border: (color: red)`. `thickness:` takes `thin`, `normal` (the default), `thick`, a multiple of normal such as `3`, or pixels such as `4px`; `border: thick` is refused with the bracket it meant. A thick border is drawn inside the box's outline and the box grows by the extra, so its padding is still measured from the inside of the stroke, a gap to its neighbor from the outside, and a line still ends on its outer edge. A stated `width:` or `height:` is the outside of the box, stroke included. A region's border is drawn inside the edge its points state, and the region doesn't grow.
|
|
1043
|
+
|
|
1023
1044
|
### Styles
|
|
1024
1045
|
|
|
1025
1046
|
```
|
|
@@ -1101,9 +1122,12 @@ A default takes the words a style does and may wear a style of its own (`default
|
|
|
1101
1122
|
|
|
1102
1123
|
```
|
|
1103
1124
|
diagram theme: nord background: #1e2229 text: (color: #e0e0e0)
|
|
1125
|
+
diagram title: "How a login reaches the database"
|
|
1104
1126
|
```
|
|
1105
1127
|
|
|
1106
|
-
`diagram` holds what covers the whole drawing: `theme`, `background` (the page behind everything), `text: (color: …)` for every text, `guides: show`,
|
|
1128
|
+
`diagram` holds what covers the whole drawing: `theme`, `background` (the page behind everything), `text: (color: …)` for every text, `guides: show`, `checks: (…)` ([Findings](#errors-and-findings)), and `title`. It has no name, since a file holds one diagram, and a second one is an error. `diagram text:` takes a color only; a text size belongs on a default.
|
|
1129
|
+
|
|
1130
|
+
`title:` names the drawing, in quotes. It isn't drawn: the SVG carries it as its own title, which a browser shows on the tab and a screen reader announces, and a page made with `--page` is headed with it.
|
|
1107
1131
|
|
|
1108
1132
|
### Destinations
|
|
1109
1133
|
|
|
@@ -1126,7 +1150,8 @@ An **error** means the file can't be drawn: an unknown key, two placements on on
|
|
|
1126
1150
|
| `edge-node-overlap` | a line across a node that isn't one of its ends, or through its own ends | on |
|
|
1127
1151
|
| `edge-overlap` | two lines on one run for a stretch, however short; a collapsed bundle's bus, and lines from one statement such as `edge a -> b, c`, shared on purpose, are not reported | on |
|
|
1128
1152
|
| `crowded-edge-ends` | several line ends bunched on a side too short to space them: two ends' marks would touch, or, where neither has a mark, the lines come within a third of a unit | on |
|
|
1129
|
-
| `node-overlap` | two nodes on one spot that no layout, `inside`, `on` or `overlap: allow` accounts for | on |
|
|
1153
|
+
| `node-overlap` | two nodes on one spot that no layout, `inside`, `on` or `overlap: allow` accounts for; `inside hub top-right` accounts for covering `hub`, not `hub`'s other children, and a box that draws nothing (no fill, border or text) can't be covered | on |
|
|
1154
|
+
| `node-overflow` | a node placed against its container's side or corner that the container has no room for there: less room on the far side than it keeps from the side it names | on |
|
|
1130
1155
|
| `edge-no-length` | a line between two boxes that touch, drawn as its marks at the seam | on |
|
|
1131
1156
|
| `edge-crossings` | two lines crossing | off |
|
|
1132
1157
|
|
|
@@ -1154,6 +1179,7 @@ reladraw check diagram.reladraw
|
|
|
1154
1179
|
| option | |
|
|
1155
1180
|
|---|---|
|
|
1156
1181
|
| `-o`, `--out` | where `render` writes; defaults to the input with `.svg`; `-` for standard output |
|
|
1182
|
+
| `--page` | `render` writes a page for looking at the diagram instead, defaulting to the input with `.html` |
|
|
1157
1183
|
| `--theme <name>` | render in another theme, whatever the file says |
|
|
1158
1184
|
| `--guides` | draw the guides |
|
|
1159
1185
|
| `--checks "<kind>: on, …"` | turn kinds of finding on or off, over the file |
|
|
@@ -1162,6 +1188,14 @@ reladraw check diagram.reladraw
|
|
|
1162
1188
|
|
|
1163
1189
|
The output is a standalone SVG: every picture is drawn into it as path data, never from a font or a linked file. A web page can draw a diagram in place with `<reladraw-diagram>`; see "In a web page" in the README.
|
|
1164
1190
|
|
|
1191
|
+
### A page for looking at a diagram
|
|
1192
|
+
|
|
1193
|
+
```
|
|
1194
|
+
reladraw render arch.reladraw --page
|
|
1195
|
+
```
|
|
1196
|
+
|
|
1197
|
+
`--page` writes `arch.html`: one self-contained file, for opening in a browser, attaching or committing, that needs nothing running and fetches nothing. The page is in the diagram's own colors, so a dark diagram doesn't sit on a white page, and it is headed with `diagram title:`, or the file's name when there is none. Drag to pan; scroll, pinch or double-click to zoom; Fit and Actual size reset the view, and there is a full-screen button. Save SVG and Save PNG save the drawing as `render` would. The findings are listed beside it in a panel that folds away, and clicking one lights up what its line drew. A box's or a line's name shows under the pointer. Keys: `+` and `-` zoom, `0` fits, `1` is actual size, `f` is full screen.
|
|
1198
|
+
|
|
1165
1199
|
### Formatting
|
|
1166
1200
|
|
|
1167
1201
|
```
|
|
@@ -1281,6 +1315,23 @@ Designed and absent from the code.
|
|
|
1281
1315
|
|
|
1282
1316
|
Pre-1.0, so the minor number is where a breaking change goes. Every removal is refused by name with the replacement quoted, rather than dropped in silence: an older file stops with an error saying what to write instead.
|
|
1283
1317
|
|
|
1318
|
+
**0.21.0**
|
|
1319
|
+
|
|
1320
|
+
- **A diagram can have a title**: `diagram title: "How a login reaches the database"`. The SVG carries it as its own title, which a browser shows on the tab. See [The diagram](#the-diagram).
|
|
1321
|
+
- **`reladraw render --page`** writes a page for looking at a diagram: one self-contained HTML file in the diagram's colors, headed with its title, that pans, zooms, goes full screen, saves the SVG or a PNG, lists the findings and lights up what each one is about. See [A page for looking at a diagram](#a-page-for-looking-at-a-diagram).
|
|
1322
|
+
- **A border can be thicker or thinner**: `border: (color: #d2904e, thickness: thick)`, mirroring the line's bracket. A thick border is drawn inside the box, and the box grows to keep its padding, so it may move what is placed beside it. See [A border's bracket](#a-borders-bracket).
|
|
1323
|
+
- **A box can be lined up with something inside a box placed against it.** `node a "Node A" even with b.1` with `node b "Node B" right of a` used to be refused, "nothing anchors the diagram", because `right of a` also centered B on A. That centering now gives way, as it already did to a placement written on B itself, and B goes where B.1 is level with A.
|
|
1324
|
+
|
|
1325
|
+
**0.20.0**
|
|
1326
|
+
|
|
1327
|
+
- **`stays above`, `stays below`, `stays left of` and `stays right of`** keep a child placed in its box's corner clear of the other children: `node laptop.mark icon: laptop inside laptop top-right stays above laptop.photos`. The laptop stays in the corner, and the box makes room. See [Against its own parent](#against-its-own-parent).
|
|
1328
|
+
- **Breaking: a corner and a sibling saying the same axis are refused.** `node c.j "j" inside c top-right right of c.k` used to be centered between the two, neither in the corner nor beside `c.k`, straddling the box's edge. The error offers `right of c.k inside c top` to sit beside `c.k`, and `stays right of c.k` to keep the corner. The same goes for `inside c top-right above c.k` up and down.
|
|
1329
|
+
- **A new finding, `node-overflow`**: a child placed in its box's corner or against its side that the box has no room for there, such as `node drive.mark icon: disk inside drive top-right` on a box one line tall. It used to run out of the box with nothing said.
|
|
1330
|
+
- **A child in its box's corner that covers one of the box's other children is reported** as `node-overlap`, with the `stays` that would keep it clear. Being placed `inside` a box used to excuse it from covering anything in that box, not only the box.
|
|
1331
|
+
- **A child beside a sibling and against its parent's top stays inside the parent.** `node c.j "j" right of c.k inside c top` used to be drawn wholly outside `c`, with nothing reported. Being against the parent's top only measures its height from the finished box; across, the box holds it like any other child.
|
|
1332
|
+
- **Breaking: a hex color without its `#` is refused.** `fill: 444444` used to be passed to the viewer as is, which drew nothing. The error says to write `#444444`.
|
|
1333
|
+
- **The playground saves a PNG** with Save PNG, at twice the picture's size, in the fonts the page shows it in. Download is now Save SVG beside it.
|
|
1334
|
+
|
|
1284
1335
|
**0.19.1**
|
|
1285
1336
|
|
|
1286
1337
|
- **A copy that states its own `step-out:` or `turn:` keeps it.** Lines between the same two sides of the same two boxes fan into lanes, and each used to be drawn as the first one's shape shifted over, so a `step-out:` or `turn:` written on any later line was ignored. A line naming either is now drawn as written and leaves the fan, and so does a line named in its side's `right-edge-order:` list. Where that puts two lines on one run, it is reported as `edge-overlap`. Prompted by #19.
|
package/dist/ast.d.ts
CHANGED
|
@@ -234,6 +234,21 @@ export interface OnPlacement {
|
|
|
234
234
|
* it needs; the resolver intersects them.
|
|
235
235
|
*/
|
|
236
236
|
export type Placement = OffsetPlacement | AlignPlacement | OnPlacement;
|
|
237
|
+
/**
|
|
238
|
+
* `stays above laptop.photos (gap: wide)` — a limit, not a placement
|
|
239
|
+
* (design.org 5.6). It never says where the node goes, so no axis counts it;
|
|
240
|
+
* it rules out pictures where the node is less than a gap that way from its
|
|
241
|
+
* target, and the box holding both makes room by lengthening the one distance
|
|
242
|
+
* nobody stated.
|
|
243
|
+
*/
|
|
244
|
+
export interface Limit {
|
|
245
|
+
way: 'above' | 'below' | 'left' | 'right';
|
|
246
|
+
target: PlacementTarget;
|
|
247
|
+
gap?: string;
|
|
248
|
+
line: number;
|
|
249
|
+
}
|
|
250
|
+
/** How a limit reads back in the author's own words. */
|
|
251
|
+
export declare function describeLimit(limit: Limit): string;
|
|
237
252
|
/**
|
|
238
253
|
* The modifiers a placement understands, in brackets after its targets. Refused
|
|
239
254
|
* by name when unrecognized, for the reason `DIAGRAM_KEYS` are: a modifier that
|
|
@@ -339,6 +354,12 @@ export declare const TEXT_KEYS: readonly ["color", "size", "wrap", "align", "at"
|
|
|
339
354
|
* of `line: (color: red)`, and both are stored under the bare `line` key.
|
|
340
355
|
*/
|
|
341
356
|
export declare const LINE_KEYS: readonly ["color", "path", "corners", "crossing", "pattern", "thickness", "curve", "turn", "step-out"];
|
|
357
|
+
/**
|
|
358
|
+
* What a box's border takes in its bracket: `border: (color: red, thickness:
|
|
359
|
+
* thick)`, mirroring the line's. `border: red` is the short form of the color,
|
|
360
|
+
* and both are stored under the bare `border` key.
|
|
361
|
+
*/
|
|
362
|
+
export declare const BORDER_KEYS: readonly ["color", "thickness"];
|
|
342
363
|
/**
|
|
343
364
|
* The words each of the line's properties takes, the default first. Thickness
|
|
344
365
|
* takes a multiple of normal (`2`) or pixels (`3px`) as well. `turn:` takes a fraction and
|
|
@@ -368,6 +389,8 @@ export interface NodeStmt {
|
|
|
368
389
|
titlePlacements?: Placement[];
|
|
369
390
|
/** Everything the author said about where this goes. Empty for the anchor. */
|
|
370
391
|
placements: Placement[];
|
|
392
|
+
/** `stays above laptop.photos`: what it keeps clear of (design.org 5.6). */
|
|
393
|
+
limits?: Limit[];
|
|
371
394
|
attrs: Attrs;
|
|
372
395
|
/** `guide node frame ""`: does everything a node does except draw (design.org 8.1). */
|
|
373
396
|
guide?: boolean;
|
|
@@ -638,9 +661,10 @@ export interface DiagramStmt {
|
|
|
638
661
|
* `text: (color: …)`, and sets the text color of everything at once — the
|
|
639
662
|
* theme has one text color, shared by nodes and edges, and this is that.
|
|
640
663
|
* `checks` takes a bracket too, `checks: (edge-crossings: on)`, turning kinds
|
|
641
|
-
* of finding on or off by name.
|
|
664
|
+
* of finding on or off by name. `title` is quoted, and names the drawing: the
|
|
665
|
+
* SVG carries it as its own title, and a page made with `--page` heads with it.
|
|
642
666
|
*/
|
|
643
|
-
export declare const DIAGRAM_KEYS: readonly ["theme", "background", "text", "checks", "guides"];
|
|
667
|
+
export declare const DIAGRAM_KEYS: readonly ["theme", "background", "text", "checks", "guides", "title"];
|
|
644
668
|
/**
|
|
645
669
|
* `default leaf fill: #2e5d3a` — a style that applies to every thing of one
|
|
646
670
|
* kind without being named. The more specific wins: `node` covers every node,
|
package/dist/ast.js
CHANGED
|
@@ -160,6 +160,12 @@ export function describePoint(point) {
|
|
|
160
160
|
const of = /left|right/.test(point.direction) ? ' of' : '';
|
|
161
161
|
return `${point.distance} ${point.direction}${of} ${nameTarget(point.from)}`;
|
|
162
162
|
}
|
|
163
|
+
/** How a limit reads back in the author's own words. */
|
|
164
|
+
export function describeLimit(limit) {
|
|
165
|
+
const of = limit.way === 'left' || limit.way === 'right' ? ' of' : '';
|
|
166
|
+
const gap = limit.gap === undefined ? '' : ` (gap: ${limit.gap})`;
|
|
167
|
+
return `stays ${limit.way}${of} ${nameTarget(limit.target)}${gap}`;
|
|
168
|
+
}
|
|
163
169
|
/**
|
|
164
170
|
* The modifiers a placement understands, in brackets after its targets. Refused
|
|
165
171
|
* by name when unrecognized, for the reason `DIAGRAM_KEYS` are: a modifier that
|
|
@@ -292,6 +298,12 @@ export const TEXT_KEYS = ['color', 'size', 'wrap', 'align', 'at'];
|
|
|
292
298
|
* of `line: (color: red)`, and both are stored under the bare `line` key.
|
|
293
299
|
*/
|
|
294
300
|
export const LINE_KEYS = ['color', 'path', 'corners', 'crossing', 'pattern', 'thickness', 'curve', 'turn', 'step-out'];
|
|
301
|
+
/**
|
|
302
|
+
* What a box's border takes in its bracket: `border: (color: red, thickness:
|
|
303
|
+
* thick)`, mirroring the line's. `border: red` is the short form of the color,
|
|
304
|
+
* and both are stored under the bare `border` key.
|
|
305
|
+
*/
|
|
306
|
+
export const BORDER_KEYS = ['color', 'thickness'];
|
|
295
307
|
/**
|
|
296
308
|
* The words each of the line's properties takes, the default first. Thickness
|
|
297
309
|
* takes a multiple of normal (`2`) or pixels (`3px`) as well. `turn:` takes a fraction and
|
|
@@ -339,9 +351,10 @@ export const REGION_KEYS = ['style', 'fill', 'opacity', 'border', 'padding', 'pa
|
|
|
339
351
|
* `text: (color: …)`, and sets the text color of everything at once — the
|
|
340
352
|
* theme has one text color, shared by nodes and edges, and this is that.
|
|
341
353
|
* `checks` takes a bracket too, `checks: (edge-crossings: on)`, turning kinds
|
|
342
|
-
* of finding on or off by name.
|
|
354
|
+
* of finding on or off by name. `title` is quoted, and names the drawing: the
|
|
355
|
+
* SVG carries it as its own title, and a page made with `--page` heads with it.
|
|
343
356
|
*/
|
|
344
|
-
export const DIAGRAM_KEYS = ['theme', 'background', 'text', 'checks', 'guides'];
|
|
357
|
+
export const DIAGRAM_KEYS = ['theme', 'background', 'text', 'checks', 'guides', 'title'];
|
|
345
358
|
export const DEFAULT_TARGETS = [
|
|
346
359
|
'node',
|
|
347
360
|
'leaf',
|
package/dist/checks.d.ts
CHANGED
package/dist/checks.js
CHANGED
|
@@ -25,6 +25,7 @@ export const CHECK_DEFAULTS = {
|
|
|
25
25
|
'edge-overlap': true,
|
|
26
26
|
'crowded-edge-ends': true,
|
|
27
27
|
'node-overlap': true,
|
|
28
|
+
'node-overflow': true,
|
|
28
29
|
'edge-no-length': true,
|
|
29
30
|
'edge-crossings': false,
|
|
30
31
|
};
|
|
@@ -96,6 +97,8 @@ export function findings(layout, checks) {
|
|
|
96
97
|
const found = [];
|
|
97
98
|
if (checks['node-overlap'])
|
|
98
99
|
found.push(...nodeOverlaps(layout));
|
|
100
|
+
if (checks['node-overflow'])
|
|
101
|
+
found.push(...nodeOverflows(layout));
|
|
99
102
|
if (checks['text-overflow'])
|
|
100
103
|
found.push(...textOverflows(layout));
|
|
101
104
|
if (checks['edge-node-overlap'] || checks['edge-overlap'] || checks['edge-no-length'] || checks['crowded-edge-ends'] || checks['edge-crossings'] || checks['text-overlap']) {
|
|
@@ -781,12 +784,20 @@ function nodeOverlaps(layout) {
|
|
|
781
784
|
return true;
|
|
782
785
|
if (upA.some(saysOverlapAllowed) || upB.some(saysOverlapAllowed))
|
|
783
786
|
return true;
|
|
784
|
-
|
|
785
|
-
|
|
787
|
+
// `inside hub top-right` accounts for covering hub, and what it holds
|
|
788
|
+
// covering hub, but not for covering hub's other children (design.org 5.6).
|
|
789
|
+
const placedOn = (over, under) => over.some((node) => stated.get(node)?.has(under));
|
|
790
|
+
return placedOn(upA, b) || placedOn(upB, a);
|
|
786
791
|
};
|
|
787
792
|
const pairs = [];
|
|
788
|
-
// A guide box draws nothing, so nothing drawn can cover it or be covered by
|
|
789
|
-
|
|
793
|
+
// A guide box draws nothing, so nothing drawn can cover it or be covered by
|
|
794
|
+
// it, and neither can a box with no fill, no border, no picture and no
|
|
795
|
+
// text, such as a spacer holding its container open.
|
|
796
|
+
const blank = (node) => node.lines.every((line) => line.every((run) => run.text.trim() === '')) &&
|
|
797
|
+
node.body.kind !== 'icon' &&
|
|
798
|
+
node.appearance['fill'] === 'none' &&
|
|
799
|
+
node.appearance['border'] === 'none';
|
|
800
|
+
const sorted = layout.nodes.filter((node) => !node.guide && !blank(node)).sort((a, b) => a.x - b.x);
|
|
790
801
|
let active = [];
|
|
791
802
|
for (const node of sorted) {
|
|
792
803
|
active = active.filter((other) => other.x + other.width > node.x + EPSILON);
|
|
@@ -809,7 +820,70 @@ function nodeOverlaps(layout) {
|
|
|
809
820
|
const inner = ({ earlier, later }) => lineage(earlier).some((a) => lineage(later).some((b) => (a !== earlier || b !== later) && reported.has(namePair(a, b))));
|
|
810
821
|
return pairs
|
|
811
822
|
.filter((pair) => !inner(pair))
|
|
812
|
-
.map(({ earlier, later, width, height }) => new Finding('node-overlap', `node ${later.name} covers ${coveredPart(earlier, later)}node ${earlier.name}; ${size(width, height)}
|
|
823
|
+
.map(({ earlier, later, width, height }) => new Finding('node-overlap', `node ${later.name} covers ${coveredPart(earlier, later)}node ${earlier.name}; ${size(width, height)}` +
|
|
824
|
+
keepClear(earlier, later, width, height), later.line));
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* For a child placed against its container's side or corner over one of its
|
|
828
|
+
* siblings, the limit that would make the box room for it (design.org 5.6):
|
|
829
|
+
* toward the side it is against, which is the only way growing the box moves
|
|
830
|
+
* it, and of a corner's two, the one that clears the shorter overlap.
|
|
831
|
+
*/
|
|
832
|
+
function keepClear(a, b, width, height) {
|
|
833
|
+
const sides = framedSides;
|
|
834
|
+
if (a.parent === undefined || a.parent !== b.parent || (sides(a).length > 0) === (sides(b).length > 0))
|
|
835
|
+
return '';
|
|
836
|
+
const [own, other] = sides(a).length > 0 ? [a, b] : [b, a];
|
|
837
|
+
const words = { top: 'above', bottom: 'below', left: 'left of', right: 'right of' };
|
|
838
|
+
const ways = sides(own).map((side) => words[side]);
|
|
839
|
+
const way = ways.length === 1 ? ways[0] : height <= width ? ways.find((each) => each === 'above' || each === 'below') : ways.find((each) => each.endsWith('of'));
|
|
840
|
+
return ` — to keep ${own.name} clear of it, add "stays ${way} ${other.name}" to ${own.name}`;
|
|
841
|
+
}
|
|
842
|
+
/** The sides of its container a child is placed against: `inside c top-right` is top and right. */
|
|
843
|
+
function framedSides(node) {
|
|
844
|
+
const parent = node.parent;
|
|
845
|
+
if (parent === undefined)
|
|
846
|
+
return [];
|
|
847
|
+
const part = node.placements
|
|
848
|
+
.flatMap((placement) => placement.targets)
|
|
849
|
+
.find((target) => target.name === parent.name && target.part !== 'text')?.part;
|
|
850
|
+
if (part === undefined)
|
|
851
|
+
return [];
|
|
852
|
+
return part.split('-').filter((word) => ['top', 'bottom', 'left', 'right'].includes(word));
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* A child placed against its container's side or corner that the box has no
|
|
856
|
+
* room for there (design.org 5.6): less room on the far side than it keeps
|
|
857
|
+
* from the side it names, so it runs into the far side's padding or out of
|
|
858
|
+
* the box. The box never grows for it, so it is drawn as stated.
|
|
859
|
+
*/
|
|
860
|
+
function nodeOverflows(layout) {
|
|
861
|
+
const found = [];
|
|
862
|
+
for (const node of layout.nodes) {
|
|
863
|
+
const parent = node.parent;
|
|
864
|
+
if (parent === undefined || node.guide)
|
|
865
|
+
continue;
|
|
866
|
+
const short = [];
|
|
867
|
+
for (const side of framedSides(node)) {
|
|
868
|
+
const vertical = side === 'top' || side === 'bottom';
|
|
869
|
+
const start = vertical ? node.y : node.x;
|
|
870
|
+
const end = start + (vertical ? node.height : node.width);
|
|
871
|
+
const boxStart = vertical ? parent.y : parent.x;
|
|
872
|
+
const boxEnd = boxStart + (vertical ? parent.height : parent.width);
|
|
873
|
+
const kept = side === 'top' || side === 'left' ? start - boxStart : boxEnd - end;
|
|
874
|
+
const far = side === 'top' || side === 'left' ? boxEnd - end : start - boxStart;
|
|
875
|
+
if (far >= kept - EPSILON)
|
|
876
|
+
continue;
|
|
877
|
+
const room = (vertical ? parent.height : parent.width) - kept * 2;
|
|
878
|
+
const [measure, against] = vertical ? ['tall', 'height'] : ['wide', 'width'];
|
|
879
|
+
short.push(`it is ${units(vertical ? node.height : node.width)} ${measure} and ${parent.name}'s ${against} leaves ${room > 0 ? units(room) : 'nothing'} inside its padding`);
|
|
880
|
+
}
|
|
881
|
+
if (short.length === 0)
|
|
882
|
+
continue;
|
|
883
|
+
found.push(new Finding('node-overflow', `node ${node.name} has no room in the ${framedSides(node).join('-')} of ${parent.name}: ${short.join(', and ')} — ` +
|
|
884
|
+
`give ${parent.name} more room, or place ${node.name} against ${parent.name}'s text or its other children`, node.line));
|
|
885
|
+
}
|
|
886
|
+
return found;
|
|
813
887
|
}
|
|
814
888
|
function namePair(a, b) {
|
|
815
889
|
return a.name < b.name ? `${a.name}\n${b.name}` : `${b.name}\n${a.name}`;
|
package/dist/cli.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
|
-
import { dirname, resolve as resolvePath } from 'node:path';
|
|
4
|
+
import { basename, dirname, resolve as resolvePath } from 'node:path';
|
|
5
5
|
import { nearMatch, readChecks } from './checks.js';
|
|
6
6
|
import { SourceError } from './errors.js';
|
|
7
7
|
import { format } from './format.js';
|
|
8
8
|
import { DEFAULT_FORMAT, readFormatSettings, SETTINGS_FILE, settingsFileText } from './format-settings.js';
|
|
9
9
|
import { draw } from './index.js';
|
|
10
|
+
import { drawPage } from './page.js';
|
|
10
11
|
import { THEME_NAMES, THEMES } from './themes.js';
|
|
11
12
|
const USAGE = `reladraw — draw a diagram from stated placement
|
|
12
13
|
|
|
13
14
|
reladraw render <input.reladraw> [-o <output.svg>] [options]
|
|
15
|
+
reladraw render <input.reladraw> --page [-o <output.html>] [options]
|
|
14
16
|
reladraw check <input.reladraw> [options]
|
|
15
17
|
reladraw format <input.reladraw>... [--check]
|
|
16
18
|
reladraw format - [--check]
|
|
@@ -33,6 +35,11 @@ const USAGE = `reladraw — draw a diagram from stated placement
|
|
|
33
35
|
|
|
34
36
|
-o, --out where render writes the SVG. Defaults to the input path with
|
|
35
37
|
its extension replaced by .svg. Use - for standard output.
|
|
38
|
+
--page render writes a page for looking at the diagram instead: one
|
|
39
|
+
HTML file in the diagram's colors, headed with its title, that
|
|
40
|
+
pans, zooms, goes full screen, saves the SVG or a PNG, lists the
|
|
41
|
+
findings and names what is under the pointer. Defaults to the
|
|
42
|
+
input path with .html.
|
|
36
43
|
--theme render in this theme, whatever the file's \`diagram theme:\`
|
|
37
44
|
says. One of ${THEME_NAMES.join(', ')}.
|
|
38
45
|
--guides draw the guides: everything written with \`guide\` in front,
|
|
@@ -79,6 +86,7 @@ async function main(argv) {
|
|
|
79
86
|
let checks;
|
|
80
87
|
let noChecks = false;
|
|
81
88
|
let guides = false;
|
|
89
|
+
let page = false;
|
|
82
90
|
for (let i = 1; i < argv.length; i += 1) {
|
|
83
91
|
const arg = argv[i];
|
|
84
92
|
if ((arg === '-o' || arg === '--out') && command === 'render') {
|
|
@@ -119,6 +127,13 @@ async function main(argv) {
|
|
|
119
127
|
else if (arg === '--guides') {
|
|
120
128
|
guides = true;
|
|
121
129
|
}
|
|
130
|
+
else if (arg === '--page' && command === 'render') {
|
|
131
|
+
page = true;
|
|
132
|
+
}
|
|
133
|
+
else if (arg === '--page') {
|
|
134
|
+
process.stderr.write('reladraw: check writes nothing, so it takes no --page; reladraw render --page writes one\n');
|
|
135
|
+
return 1;
|
|
136
|
+
}
|
|
122
137
|
else if (arg.startsWith('-') && arg !== '-') {
|
|
123
138
|
process.stderr.write(`reladraw: unknown option ${arg}\n`);
|
|
124
139
|
return 1;
|
|
@@ -157,16 +172,24 @@ async function main(argv) {
|
|
|
157
172
|
overChecks = (fromFile) => readChecks(written, fromFile);
|
|
158
173
|
}
|
|
159
174
|
const source = await readFile(input, 'utf8');
|
|
175
|
+
const options = {
|
|
176
|
+
...(theme === undefined ? {} : { theme: THEMES[theme] }),
|
|
177
|
+
...(guides ? { guides: true } : {}),
|
|
178
|
+
// An icon's file is found from the diagram, not from wherever the tool
|
|
179
|
+
// happens to be run.
|
|
180
|
+
readIconFile: (path) => readFileSync(resolvePath(dirname(input), path), 'utf8'),
|
|
181
|
+
...(noChecks ? { checks: false } : overChecks ? { checks: overChecks } : {}),
|
|
182
|
+
};
|
|
160
183
|
let drawn;
|
|
161
184
|
try {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
185
|
+
if (page) {
|
|
186
|
+
const made = drawPage(source, basename(input).replace(/\.[^.]+$/, ''), options);
|
|
187
|
+
drawn = { text: made.html, findings: made.findings };
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
const made = draw(source, options);
|
|
191
|
+
drawn = { text: made.svg, findings: made.findings };
|
|
192
|
+
}
|
|
170
193
|
}
|
|
171
194
|
catch (error) {
|
|
172
195
|
if (error instanceof SourceError) {
|
|
@@ -180,12 +203,12 @@ async function main(argv) {
|
|
|
180
203
|
if (command === 'check')
|
|
181
204
|
return 0;
|
|
182
205
|
if (out === '-') {
|
|
183
|
-
process.stdout.write(drawn.
|
|
206
|
+
process.stdout.write(drawn.text);
|
|
184
207
|
return 0;
|
|
185
208
|
}
|
|
186
|
-
const target = resolvePath(out ?? input.replace(/\.[^.]+$/, '') + '.svg');
|
|
209
|
+
const target = resolvePath(out ?? input.replace(/\.[^.]+$/, '') + (page ? '.html' : '.svg'));
|
|
187
210
|
await mkdir(dirname(target), { recursive: true });
|
|
188
|
-
await writeFile(target, drawn.
|
|
211
|
+
await writeFile(target, drawn.text, 'utf8');
|
|
189
212
|
process.stderr.write(`${target}\n`);
|
|
190
213
|
return 0;
|
|
191
214
|
}
|
package/dist/constants.d.ts
CHANGED
|
@@ -55,6 +55,10 @@ export declare const ARROW_MARKER_WIDTH = 7;
|
|
|
55
55
|
export declare const ARROW_LENGTH: number;
|
|
56
56
|
/** What `thickness: thin | normal | thick` draw, in pixels. `normal` is the line as it always was. */
|
|
57
57
|
export declare const THICKNESS: Record<string, number>;
|
|
58
|
+
/** A box's border as it always was, in pixels: `border: (thickness: normal)`. */
|
|
59
|
+
export declare const BORDER_WIDTH = 1;
|
|
60
|
+
/** What a border's `thickness: thin | normal | thick` draw, in pixels. */
|
|
61
|
+
export declare const BORDER_THICKNESS: Record<string, number>;
|
|
58
62
|
/**
|
|
59
63
|
* How much of a line of this thickness its arrowhead covers. The marker scales
|
|
60
64
|
* with the stroke, so a thick line gets a bigger head, and the room an edge's
|
package/dist/constants.js
CHANGED
|
@@ -65,6 +65,14 @@ export const THICKNESS = {
|
|
|
65
65
|
normal: LINE_WIDTH,
|
|
66
66
|
thick: LINE_WIDTH * 2,
|
|
67
67
|
};
|
|
68
|
+
/** A box's border as it always was, in pixels: `border: (thickness: normal)`. */
|
|
69
|
+
export const BORDER_WIDTH = 1;
|
|
70
|
+
/** What a border's `thickness: thin | normal | thick` draw, in pixels. */
|
|
71
|
+
export const BORDER_THICKNESS = {
|
|
72
|
+
thin: BORDER_WIDTH / 2,
|
|
73
|
+
normal: BORDER_WIDTH,
|
|
74
|
+
thick: BORDER_WIDTH * 2,
|
|
75
|
+
};
|
|
68
76
|
/**
|
|
69
77
|
* How much of a line of this thickness its arrowhead covers. The marker scales
|
|
70
78
|
* with the stroke, so a thick line gets a bigger head, and the room an edge's
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Placement } from './ast.js';
|
|
2
|
+
/**
|
|
3
|
+
* The tightest arrangement satisfying a set of relative distances.
|
|
4
|
+
*
|
|
5
|
+
* Every placement in the language turns into the same shape of fact: one thing sits
|
|
6
|
+
* at least so far along an axis from another. Written as `pos[to] >= pos[from]
|
|
7
|
+
* + weight`, a whole diagram is a system of those, and the arrangement the
|
|
8
|
+
* author meant is the one where nothing is further apart than it has to be.
|
|
9
|
+
*
|
|
10
|
+
* That arrangement is unique and is found by longest paths, so no search
|
|
11
|
+
* happens and no alternative is ever weighed. Distances come out of the
|
|
12
|
+
* arithmetic; which side of what a thing sits on came from the author.
|
|
13
|
+
*/
|
|
14
|
+
export interface Constraint {
|
|
15
|
+
from: number;
|
|
16
|
+
to: number;
|
|
17
|
+
weight: number;
|
|
18
|
+
/** The placement this came from, so a failure can be reported in the author's words. */
|
|
19
|
+
placement?: Placement;
|
|
20
|
+
}
|
|
21
|
+
/** An exact distance, which is two inequalities pointing opposite ways. */
|
|
22
|
+
export declare function fix(from: number, to: number, distance: number, placement?: Placement): Constraint[];
|
|
23
|
+
export interface Contradiction {
|
|
24
|
+
/** The placements that cannot all hold. Empty if the loop is entirely implicit. */
|
|
25
|
+
placements: Placement[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Solve, or report the placements that fight.
|
|
29
|
+
*
|
|
30
|
+
* Longest path from a virtual source joined to everything at zero, which is
|
|
31
|
+
* Bellman-Ford with the comparison flipped. A distance that keeps growing after
|
|
32
|
+
* one pass per node means the constraints run in a circle that demands ever
|
|
33
|
+
* more room, and that circle is exactly the set of placements to quote back.
|
|
34
|
+
*/
|
|
35
|
+
export declare function tightest(count: number, constraints: Constraint[]): {
|
|
36
|
+
positions: number[];
|
|
37
|
+
} | {
|
|
38
|
+
contradiction: Contradiction;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Which members can be driven apart from which, along one axis.
|
|
42
|
+
*
|
|
43
|
+
* A constraint says `pos[to] >= pos[from] + weight`, so it bounds `to` from
|
|
44
|
+
* below and leaves it free to move further away. Follow those edges and the
|
|
45
|
+
* question "may this one end up beyond that one?" becomes plain reachability:
|
|
46
|
+
* if `j` is reachable from `i` and `i` is not reachable from `j`, then the file
|
|
47
|
+
* lets the distance from `i` to `j` grow without limit and never lets it be
|
|
48
|
+
* closed from the other side. Pushing `j` past `i` is then the one separation
|
|
49
|
+
* the file allows, and no choice was made.
|
|
50
|
+
*
|
|
51
|
+
* Reachable both ways means the two are pinned at a fixed distance, so they
|
|
52
|
+
* cannot be separated along this axis at all. Reachable neither way means the
|
|
53
|
+
* file said nothing that orders them, which is the case the caller must refuse
|
|
54
|
+
* rather than guess at.
|
|
55
|
+
*/
|
|
56
|
+
export declare function reachability(count: number, constraints: Constraint[]): boolean[][];
|