reladraw 0.13.0 → 0.14.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 +1 -1
- package/SYNTAX.md +58 -7
- package/dist/ast.d.ts +24 -2
- package/dist/ast.js +17 -1
- package/dist/grammar.d.ts +2 -10
- package/dist/grammar.js +8 -3
- package/dist/model.d.ts +6 -2
- package/dist/parser.js +100 -17
- package/dist/render.js +54 -19
- package/dist/resolve.js +41 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -82,7 +82,7 @@ That gets you a copy of the skill at the time you run it, so you'll need to re-r
|
|
|
82
82
|
|
|
83
83
|
## Status
|
|
84
84
|
|
|
85
|
-
Version 0.
|
|
85
|
+
Version 0.14.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.
|
|
86
86
|
|
|
87
87
|
The language isn't stable yet, so expect the syntax to change.
|
|
88
88
|
|
package/SYNTAX.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Syntax reference — 0.
|
|
1
|
+
# Syntax reference — 0.14.0
|
|
2
2
|
|
|
3
3
|
What the language accepts. The parser, resolver and SVG renderer implement all of it; the sections at the end record what is defective, unchecked or undecided.
|
|
4
4
|
|
|
@@ -396,12 +396,55 @@ Separation leaves a tight gap, deliberately small — enough to read as two node
|
|
|
396
396
|
## Edges
|
|
397
397
|
|
|
398
398
|
```
|
|
399
|
-
edge <from>
|
|
400
|
-
edge <to> <- <from> ["<text>"] [between <a> and <b> [vertically|horizontally]] [attributes]
|
|
401
|
-
edge <from> <-> <to> ["<text>"] [between <a> and <b> [vertically|horizontally]] [attributes]
|
|
399
|
+
edge <from> <arrow> <to> ["<text>"] [between <a> and <b> [vertically|horizontally]] [attributes]
|
|
402
400
|
```
|
|
403
401
|
|
|
404
|
-
|
|
402
|
+
```
|
|
403
|
+
edge a -> b // an arrow at b
|
|
404
|
+
edge a <- b // an arrow at a
|
|
405
|
+
edge a <-> b // one at each end
|
|
406
|
+
edge a -- b // a plain line
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
`a <- b` draws the same picture as `b -> a`. What changes is which name you write first, and that is worth having: the first name reads as the subject of the line, and plenty of edges are about the thing the arrow points at rather than the thing it leaves. **`from:` and `to:` follow the writing order**, never the arrow: `from:` is always the first name's side and `to:` the second's, whichever way the arrow points.
|
|
410
|
+
|
|
411
|
+
### What is drawn at each end
|
|
412
|
+
|
|
413
|
+
The arrow is a mark, a line, a mark. The line is `-` or `--`, the two meaning the same, and either mark may be left off. A mark is a word, or a glyph that stands for one:
|
|
414
|
+
|
|
415
|
+
| mark | draws | glyph |
|
|
416
|
+
|---|---|---|
|
|
417
|
+
| `arrow` | a filled arrowhead | `>` at the right end, `<` at the left |
|
|
418
|
+
| `oarrow` | an outlined arrowhead | `\|>` at the right end, `<\|` at the left |
|
|
419
|
+
| `dot` | a filled circle | `*` |
|
|
420
|
+
| `odot` | an outlined circle | `o` |
|
|
421
|
+
| `diamond` | a filled diamond | |
|
|
422
|
+
| `odiamond` | an outlined diamond | |
|
|
423
|
+
| `bar` | a stroke across the line | `\|` |
|
|
424
|
+
| `none` | nothing | |
|
|
425
|
+
|
|
426
|
+
The `o` in front means outlined, for every shape. A glyph that points is spelled the way it points, which is why `<->` reads as it does. So these are all the same arrow, and you write whichever reads best to you:
|
|
427
|
+
|
|
428
|
+
```
|
|
429
|
+
edge a o--> b
|
|
430
|
+
edge a odot--arrow b
|
|
431
|
+
edge a (odot)--(arrow) b
|
|
432
|
+
edge a [odot] -- [arrow] b
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
A mark touches the dashes or is in brackets. `edge a -- dot b` is an error, because it reads just as well as a plain line to a node named `dot`.
|
|
436
|
+
|
|
437
|
+
**Marks as attributes.** `from-mark:` and `to-mark:` say the same thing, one end at a time, and they are how a style carries marks:
|
|
438
|
+
|
|
439
|
+
```
|
|
440
|
+
style inherits to-mark: oarrow
|
|
441
|
+
edge dog -- animal style: inherits
|
|
442
|
+
edge cat -- animal style: inherits
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
**An end with nothing written is left open, not empty.** `dog -- animal` is plain until something says otherwise, and here the style does. `none` is the word for "nothing at this end, whatever the style says": `edge a none--> b`, or `from-mark: none`.
|
|
446
|
+
|
|
447
|
+
**A mark written in the arrow is final.** The edge's own `to-mark:` may repeat it but not contradict it — `edge a --> b to-mark: dot` is an error, since both halves are on one line and one of them would be dead text. A style fills only the ends the arrow leaves open: a style giving every end a `bar`, applied to `a --> b`, draws a bar at `a` and still the arrow at `b`.
|
|
405
448
|
|
|
406
449
|
Endpoints may be nested (`desktop1.files`). An edge never says where a node goes and routing is the renderer's problem, with one exception: an edge with text claims room in the gap it crosses, which is the next section.
|
|
407
450
|
|
|
@@ -454,7 +497,7 @@ The same rule covers every other case: sides at right angles where one faces awa
|
|
|
454
497
|
|
|
455
498
|
Several lines along the same stretch take a lane each, side by side, instead of drawing on top of one another, nested so that none crosses another where it need not. Where one of them carries its text on that stretch, the others keep clear of the text as well as the line. The text rides on the line's longest straight piece that it does not land on a box from, and never changes which way the line goes.
|
|
456
499
|
|
|
457
|
-
A curved line and a square one take the same way. The square one turns at corners. The curved one sweeps through them, each sweep as wide as the room around it allows without coming near a box or another line, and a line stepping sideways and carrying on the way it was going is one S. Where the room is tight a sweep comes close to a rounded corner; where there is room it flows. At an arrowhead, the line always finishes straight, so the
|
|
500
|
+
A curved line and a square one take the same way. The square one turns at corners. The curved one sweeps through them, each sweep as wide as the room around it allows without coming near a box or another line, and a line stepping sideways and carrying on the way it was going is one S. Where the room is tight a sweep comes close to a rounded corner; where there is room it flows. At an arrowhead or any other mark, the line always finishes straight, so the mark points square into the side.
|
|
458
501
|
|
|
459
502
|
When there is no way at all — the side you named is flush against another box — the file is refused with the reason: `edge a -> b: cannot leave a's right side, b is against it`.
|
|
460
503
|
|
|
@@ -769,6 +812,7 @@ Every attribute, and what takes one. The kinds here are what a node's **body** i
|
|
|
769
812
|
| `shape` | ✓ | | ✓ | | the outline the node is drawn with, `none` included |
|
|
770
813
|
| `icon` | | ✓ | | | the picture the node is drawn as |
|
|
771
814
|
| `from` `to` | | | | ✓ | which side the line leaves and arrives on |
|
|
815
|
+
| `from-mark` `to-mark` | | | | ✓ | what is drawn at each end — see "What is drawn at each end" |
|
|
772
816
|
| `fill` | ✓ | | | | color — see "A color names the part it colors" |
|
|
773
817
|
| `border` | ✓ | | | | color |
|
|
774
818
|
| `text` | ✓ | ✓ | ✓ | ✓ | the text's properties, in brackets — a style's form of what a node or an edge writes after its own words |
|
|
@@ -849,7 +893,7 @@ A color attribute says which part of a thing it colors, and a part exists only o
|
|
|
849
893
|
| `fill` | the area inside the outline | a node |
|
|
850
894
|
| `border` | the outline | a node |
|
|
851
895
|
| `text: (color: …)` | the text | every node, and an edge |
|
|
852
|
-
| `line` | the drawn line and its
|
|
896
|
+
| `line` | the drawn line and the marks at its ends | an edge |
|
|
853
897
|
|
|
854
898
|
A word written on a kind that has no such part is refused by name, and the error lists the parts that kind does have — `border:` on a node with no body is a mistake, not something to ignore, for the same reason an unknown `diagram` key is.
|
|
855
899
|
|
|
@@ -1068,6 +1112,13 @@ That one was found by testing the lexer, not by rendering — and it could not h
|
|
|
1068
1112
|
|
|
1069
1113
|
Pre-1.0, so the minor number is where a breaking change goes. Every removal below is refused by name with the replacement quoted, rather than dropped in silence — an older file stops with an error saying what to write instead.
|
|
1070
1114
|
|
|
1115
|
+
**0.14.0**
|
|
1116
|
+
|
|
1117
|
+
One breaking change: on an edge written with `<-`, `from:` and `to:` now name the sides of the first and second names written. They used to follow the arrow, so `disk <- writer from: bottom` meant Writer's bottom; it now means Disk's. Swap the two on any `<-` edge that names a side. No other file draws differently.
|
|
1118
|
+
|
|
1119
|
+
- A line can have no arrowhead: `edge a -- b`, or `edge a - b`.
|
|
1120
|
+
- Each end of a line can carry a mark: an arrowhead filled or outlined, a dot, a diamond or a bar, written in the arrow (`a o--> b`, `a diamond--arrow b`) or as `from-mark:` and `to-mark:`, which a style can carry. See "What is drawn at each end".
|
|
1121
|
+
|
|
1071
1122
|
**0.13.0**
|
|
1072
1123
|
|
|
1073
1124
|
No syntax changes, and no file that worked is refused. Lines move: every line that goes round something, and every line with `below`/`above`/`left of`/`right of` or `between`, may be drawn differently.
|
package/dist/ast.d.ts
CHANGED
|
@@ -276,12 +276,34 @@ export interface NodeStmt {
|
|
|
276
276
|
attrs: Attrs;
|
|
277
277
|
line: number;
|
|
278
278
|
}
|
|
279
|
+
/**
|
|
280
|
+
* What may be drawn at an end of a line. The `o` prefix is the outlined form of
|
|
281
|
+
* the shape, one rule for every shape; `none` says there is definitely nothing,
|
|
282
|
+
* where an end with no mark written leaves it to a style.
|
|
283
|
+
*/
|
|
284
|
+
export declare const MARKS: readonly ["arrow", "oarrow", "dot", "odot", "diamond", "odiamond", "bar", "none"];
|
|
285
|
+
export type Mark = (typeof MARKS)[number];
|
|
286
|
+
/**
|
|
287
|
+
* The glyphs that stand for a mark, by the end they are written at. A glyph
|
|
288
|
+
* that points is spelled the way it points, so `<` is the arrow at the left end
|
|
289
|
+
* and `>` the one at the right, which is what makes `<->` read. The diamonds
|
|
290
|
+
* have none: nothing would explain itself, and `<>` is two arrows.
|
|
291
|
+
*/
|
|
292
|
+
export declare const MARK_GLYPHS: Record<'from' | 'to', Record<string, Mark>>;
|
|
293
|
+
/** The marks written in an edge's arrow, by end. An end with none written is absent. */
|
|
294
|
+
export interface EdgeMarks {
|
|
295
|
+
from?: Mark;
|
|
296
|
+
to?: Mark;
|
|
297
|
+
}
|
|
279
298
|
export interface EdgeStmt {
|
|
280
299
|
kind: 'edge';
|
|
300
|
+
/** The first name written. `from:` and `to:` follow writing order, never the arrow. */
|
|
281
301
|
from: string;
|
|
282
302
|
to: string;
|
|
283
|
-
/**
|
|
284
|
-
|
|
303
|
+
/** The arrow as written, `->` or `o--odiamond`, for quoting back. */
|
|
304
|
+
arrow: string;
|
|
305
|
+
/** The marks the arrow writes at each end: `->` is an arrow at `to`, `<-` one at `from`. */
|
|
306
|
+
marks: EdgeMarks;
|
|
285
307
|
text?: string;
|
|
286
308
|
/** The text's bracketed modifiers, as written. Usually empty. */
|
|
287
309
|
textAttrs: Attrs;
|
package/dist/ast.js
CHANGED
|
@@ -240,6 +240,22 @@ export const LINE_VALUES = {
|
|
|
240
240
|
pattern: ['solid', 'dashed', 'dotted', 'dash-dot'],
|
|
241
241
|
thickness: ['normal', 'thin', 'thick'],
|
|
242
242
|
};
|
|
243
|
+
/**
|
|
244
|
+
* What may be drawn at an end of a line. The `o` prefix is the outlined form of
|
|
245
|
+
* the shape, one rule for every shape; `none` says there is definitely nothing,
|
|
246
|
+
* where an end with no mark written leaves it to a style.
|
|
247
|
+
*/
|
|
248
|
+
export const MARKS = ['arrow', 'oarrow', 'dot', 'odot', 'diamond', 'odiamond', 'bar', 'none'];
|
|
249
|
+
/**
|
|
250
|
+
* The glyphs that stand for a mark, by the end they are written at. A glyph
|
|
251
|
+
* that points is spelled the way it points, so `<` is the arrow at the left end
|
|
252
|
+
* and `>` the one at the right, which is what makes `<->` read. The diamonds
|
|
253
|
+
* have none: nothing would explain itself, and `<>` is two arrows.
|
|
254
|
+
*/
|
|
255
|
+
export const MARK_GLYPHS = {
|
|
256
|
+
from: { '<': 'arrow', '<|': 'oarrow', '*': 'dot', o: 'odot', '|': 'bar' },
|
|
257
|
+
to: { '>': 'arrow', '|>': 'oarrow', '*': 'dot', o: 'odot', '|': 'bar' },
|
|
258
|
+
};
|
|
243
259
|
/**
|
|
244
260
|
* The attributes a `diagram` statement understands. `text` takes a bracket,
|
|
245
261
|
* `text: (color: …)`, and sets the text color of everything at once — the
|
|
@@ -333,7 +349,7 @@ export const ATTR_KEYS = {
|
|
|
333
349
|
shape: ['style', 'gap', 'overlap', 'contents', 'badge', 'deck', 'shape', 'fill', 'border', 'text', 'url'],
|
|
334
350
|
icon: ['style', 'gap', 'overlap', 'icon', 'text', 'url'],
|
|
335
351
|
none: ['style', 'gap', 'overlap', 'shape', 'text', 'url'],
|
|
336
|
-
edge: ['style', 'from', 'to', 'line', 'text', 'url'],
|
|
352
|
+
edge: ['style', 'from', 'to', 'from-mark', 'to-mark', 'line', 'text', 'url'],
|
|
337
353
|
};
|
|
338
354
|
/**
|
|
339
355
|
* Every word that is an attribute *somewhere*, which is what separates a
|
package/dist/grammar.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export type TokenKind =
|
|
|
38
38
|
| 'keyword'
|
|
39
39
|
/** The name a statement declares, right after its keyword. */
|
|
40
40
|
| 'name'
|
|
41
|
-
/**
|
|
41
|
+
/** `->`, `<-`, `<->`, and `--` or `-` for a plain line. */
|
|
42
42
|
| 'arrow'
|
|
43
43
|
/** Placement and edge vocabulary: `right`, `of`, `level`, `with`, `and`, `between`, … */
|
|
44
44
|
| 'relation'
|
|
@@ -71,19 +71,11 @@ export declare const STATEMENT_KEYWORDS: readonly ["node", "edge", "style", "dia
|
|
|
71
71
|
* colors here without anybody remembering to come back.
|
|
72
72
|
*/
|
|
73
73
|
export declare const RELATION_WORDS: string[];
|
|
74
|
-
/**
|
|
75
|
-
* The patterns, as source strings, so a generator can emit them into another
|
|
76
|
-
* editor's grammar without reaching into a compiled RegExp. Each is written to
|
|
77
|
-
* match at the point the scanner has reached; the scanner adds the sticky flag.
|
|
78
|
-
*
|
|
79
|
-
* A name may contain dots (containment) and hyphens, which is why the word
|
|
80
|
-
* pattern is what it is rather than `\w+`.
|
|
81
|
-
*/
|
|
82
74
|
export declare const PATTERNS: {
|
|
83
75
|
readonly comment: "\\/\\/.*";
|
|
84
76
|
readonly string: "\"(?:\\\\.|[^\"\\\\])*\"?";
|
|
85
77
|
readonly keyword: `(?:${string})\\b`;
|
|
86
|
-
readonly arrow:
|
|
78
|
+
readonly arrow: `${string}?-{1,2}${string}?(?=[ \\t]|$)`;
|
|
87
79
|
readonly attribute: "[A-Za-z][A-Za-z0-9_-]*:";
|
|
88
80
|
readonly color: "#[0-9A-Fa-f]{3,8}\\b";
|
|
89
81
|
readonly relation: `(?:${string})\\b`;
|
package/dist/grammar.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* editor formats need and the reason a `.reladraw` grammar is small in all of
|
|
27
27
|
* them.
|
|
28
28
|
*/
|
|
29
|
-
import { DIRECTIONS, POSITIONS, SIDES, PASSAGE_AXES } from './ast.js';
|
|
29
|
+
import { DIRECTIONS, MARKS, POSITIONS, SIDES, PASSAGE_AXES } from './ast.js';
|
|
30
30
|
/**
|
|
31
31
|
* The words a statement may open with. `parseStatement` in `parser.ts` is the
|
|
32
32
|
* authority — its switch is what actually accepts them — and this list mirrors
|
|
@@ -71,6 +71,9 @@ function alternation(words) {
|
|
|
71
71
|
* A name may contain dots (containment) and hyphens, which is why the word
|
|
72
72
|
* pattern is what it is rather than `\w+`.
|
|
73
73
|
*/
|
|
74
|
+
/** One end of an arrow: a mark word or glyph, bare or bracketed. Words first, so `odot` is not `o`. */
|
|
75
|
+
const MARK_WORD = `(?:${[...MARKS].sort((a, b) => b.length - a.length).join('|')}|<\\||\\|>|[<>*o|])`;
|
|
76
|
+
const ARROW_MARK = `(?:\\[${MARK_WORD}\\]|\\(${MARK_WORD}\\)|${MARK_WORD})`;
|
|
74
77
|
export const PATTERNS = {
|
|
75
78
|
comment: '\\/\\/.*',
|
|
76
79
|
// The closing quote is optional: every string is unterminated for as long as
|
|
@@ -78,8 +81,10 @@ export const PATTERNS = {
|
|
|
78
81
|
// rest of the file on every keystroke.
|
|
79
82
|
string: '"(?:\\\\.|[^"\\\\])*"?',
|
|
80
83
|
keyword: `(?:${alternation(STATEMENT_KEYWORDS)})\\b`,
|
|
81
|
-
//
|
|
82
|
-
|
|
84
|
+
// A mark, one or two dashes, a mark — `->`, `o--odiamond`, `[dot]-`. It must
|
|
85
|
+
// end the token, so a hyphenated name stays a word. A mark written apart in
|
|
86
|
+
// brackets, `[dot] -- [arrow]`, colors as plain words either side of `--`.
|
|
87
|
+
arrow: `${ARROW_MARK}?-{1,2}${ARROW_MARK}?(?=[ \\t]|$)`,
|
|
83
88
|
attribute: '[A-Za-z][A-Za-z0-9_-]*:',
|
|
84
89
|
color: '#[0-9A-Fa-f]{3,8}\\b',
|
|
85
90
|
relation: `(?:${alternation(RELATION_WORDS)})\\b`,
|
package/dist/model.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Attrs, Axis, Direction, Kind, Placement } from './ast.js';
|
|
1
|
+
import type { Attrs, Axis, Direction, Kind, Mark, Placement } from './ast.js';
|
|
2
2
|
import type { Line } from './text.js';
|
|
3
3
|
import type { Body } from './icons.js';
|
|
4
4
|
/** A `between` clause with its targets resolved. Mirrors `Passage` in `ast.ts`. */
|
|
@@ -115,7 +115,11 @@ export interface LayoutNode {
|
|
|
115
115
|
export interface LayoutEdge {
|
|
116
116
|
from: LayoutNode;
|
|
117
117
|
to: LayoutNode;
|
|
118
|
-
|
|
118
|
+
/** What is drawn at each end, settled: the arrow, then the edge's own attributes, then its style. */
|
|
119
|
+
marks: {
|
|
120
|
+
from: Mark;
|
|
121
|
+
to: Mark;
|
|
122
|
+
};
|
|
119
123
|
text?: string;
|
|
120
124
|
/** The text split into the lines that will be drawn, each as its runs. */
|
|
121
125
|
lines?: Line[];
|
package/dist/parser.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { COLOR_KEYS, DEFAULT_KEYS, DEFAULT_TARGETS, DIAGRAM_KEYS, DIRECTIONS, describePlacement, SIDE_AXIS, SIDES, PASSAGE_AXES, TEXT_KEYS, LINE_KEYS, LINE_VALUES, CONTENTS_KEYS, PLACEMENT_KEYS, BOUNDARY_PARTS, INWARD, OPPOSITE, isDirection, isPart, isPosition, listTargets, nameTarget, } from './ast.js';
|
|
1
|
+
import { COLOR_KEYS, DEFAULT_KEYS, DEFAULT_TARGETS, DIAGRAM_KEYS, DIRECTIONS, describePlacement, SIDE_AXIS, SIDES, PASSAGE_AXES, TEXT_KEYS, LINE_KEYS, LINE_VALUES, CONTENTS_KEYS, PLACEMENT_KEYS, BOUNDARY_PARTS, INWARD, MARKS, MARK_GLYPHS, OPPOSITE, isDirection, isPart, isPosition, listTargets, nameTarget, } from './ast.js';
|
|
2
2
|
import { SourceError } from './errors.js';
|
|
3
3
|
import { STATEMENT_KEYWORDS } from './grammar.js';
|
|
4
4
|
import { isAttrKey, tokenizeLine } from './lexer.js';
|
|
@@ -516,27 +516,109 @@ function parseNode(head, line) {
|
|
|
516
516
|
line,
|
|
517
517
|
};
|
|
518
518
|
}
|
|
519
|
+
const NEEDS_ARROW = 'an edge needs a line between its endpoints: "->", "<-", "<->" or "--"';
|
|
519
520
|
/**
|
|
520
|
-
*
|
|
521
|
-
*
|
|
521
|
+
* The arrow between an edge's two names: a mark, a shaft, a mark. The shaft is
|
|
522
|
+
* `-` or `--`, and either mark may be left off, so `--` is a plain line and
|
|
523
|
+
* `<->` an arrow at each end.
|
|
522
524
|
*
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
*
|
|
525
|
+
* A mark is a word or a glyph, bare or in `()` or `[]`, and it must touch the
|
|
526
|
+
* dashes or be bracketed: `edge a -- dot b` would otherwise read like a line to
|
|
527
|
+
* a node named `dot`. So a bracketed mark may stand apart, which is why the
|
|
528
|
+
* arrow can span several tokens — `[dot] -- [arrow]` is three, and `(dot)` is
|
|
529
|
+
* three on its own, since the lexer splits a bracket that opens a token.
|
|
530
|
+
*/
|
|
531
|
+
function readArrow(head, line) {
|
|
532
|
+
let at = 2;
|
|
533
|
+
const pieces = [];
|
|
534
|
+
const bracketed = () => {
|
|
535
|
+
const token = head[at];
|
|
536
|
+
if (!token || token.quoted)
|
|
537
|
+
return;
|
|
538
|
+
if (token.text === '(' && head[at + 2]?.text === ')' && !head[at + 1].quoted) {
|
|
539
|
+
pieces.push(`(${head[at + 1].text})`);
|
|
540
|
+
at += 3;
|
|
541
|
+
}
|
|
542
|
+
else if (/^\[[^\]]*\]$/.test(token.text)) {
|
|
543
|
+
pieces.push(token.text);
|
|
544
|
+
at += 1;
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
bracketed();
|
|
548
|
+
const shaft = head[at];
|
|
549
|
+
if (!shaft || shaft.quoted || !shaft.text.includes('-'))
|
|
550
|
+
throw new SourceError(NEEDS_ARROW, line);
|
|
551
|
+
pieces.push(shaft.text);
|
|
552
|
+
at += 1;
|
|
553
|
+
if (shaft.text.endsWith('-'))
|
|
554
|
+
bracketed();
|
|
555
|
+
const text = pieces.join('');
|
|
556
|
+
const parts = /^([^-]*)(-+)([^-]*)$/.exec(text);
|
|
557
|
+
if (!parts) {
|
|
558
|
+
throw new SourceError(`"${text}" is not an arrow — the line is one run of dashes, with a mark at either end`, line);
|
|
559
|
+
}
|
|
560
|
+
if (parts[2].length > 2) {
|
|
561
|
+
throw new SourceError(`"${text}" has ${parts[2].length} dashes — the line is "-" or "--"`, line);
|
|
562
|
+
}
|
|
563
|
+
const from = readMark(parts[1], 'from', text, line);
|
|
564
|
+
const to = readMark(parts[3], 'to', text, line);
|
|
565
|
+
return {
|
|
566
|
+
text,
|
|
567
|
+
marks: { ...(from !== undefined ? { from } : {}), ...(to !== undefined ? { to } : {}) },
|
|
568
|
+
next: at,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
/** One end of an arrow, as written: a mark word or glyph, perhaps bracketed, or nothing. */
|
|
572
|
+
function readMark(written, end, arrow, line) {
|
|
573
|
+
if (written === '')
|
|
574
|
+
return undefined;
|
|
575
|
+
const inner = /^\((.*)\)$/.exec(written)?.[1] ?? /^\[(.*)\]$/.exec(written)?.[1] ?? written;
|
|
576
|
+
if (MARKS.includes(inner))
|
|
577
|
+
return inner;
|
|
578
|
+
const glyph = MARK_GLYPHS[end][inner];
|
|
579
|
+
if (glyph !== undefined)
|
|
580
|
+
return glyph;
|
|
581
|
+
const other = end === 'from' ? 'to' : 'from';
|
|
582
|
+
const turned = MARK_GLYPHS[other][inner];
|
|
583
|
+
if (turned !== undefined) {
|
|
584
|
+
// A pointing glyph written at the end it points away from: `>--` or `--<`.
|
|
585
|
+
const right = Object.entries(MARK_GLYPHS[end]).find(([, mark]) => mark === turned)[0];
|
|
586
|
+
throw new SourceError(`"${arrow}": "${inner}" points the other way — at the ${end === 'from' ? 'left' : 'right'} end ` +
|
|
587
|
+
`the ${turned} is "${right}", or write the word "${turned}"`, line);
|
|
588
|
+
}
|
|
589
|
+
throw new SourceError(`"${arrow}": "${inner}" is not a mark. The marks are ${MARKS.join(', ')}, and the glyphs ` +
|
|
590
|
+
`${Object.keys(MARK_GLYPHS.to).join(' ')} and ${Object.keys(MARK_GLYPHS.from).filter((g) => !(g in MARK_GLYPHS.to)).join(' ')}`, line);
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* `edge <from> <arrow> <to> ["<text>"] [between <a> and <b>]`. The arrow is
|
|
594
|
+
* read by `readArrow`.
|
|
595
|
+
*
|
|
596
|
+
* `from` and `to` are the order the names are written in, never the arrow's
|
|
597
|
+
* direction: `a <- b` is `a` then `b` with an arrow at `a`. With a mark at each
|
|
598
|
+
* end many lines have no direction at all, and the written order is the one
|
|
599
|
+
* every edge has — so `from:` names the first name's side on every edge.
|
|
526
600
|
*/
|
|
527
|
-
const ARROWS = ['->', '<->', '<-'];
|
|
528
601
|
function parseEdge(head, line) {
|
|
529
602
|
const left = requireName(head[1], 'edge', line);
|
|
530
|
-
const arrow = head
|
|
531
|
-
|
|
532
|
-
throw new SourceError('an edge needs "->", "<-" or "<->" between its endpoints', line);
|
|
533
|
-
}
|
|
534
|
-
const rightToken = head[3];
|
|
603
|
+
const arrow = readArrow(head, line);
|
|
604
|
+
const rightToken = head[arrow.next];
|
|
535
605
|
if (!rightToken || rightToken.quoted) {
|
|
536
606
|
throw new SourceError('an edge needs a node on the right of the arrow', line);
|
|
537
607
|
}
|
|
538
|
-
|
|
539
|
-
|
|
608
|
+
let at = arrow.next + 1;
|
|
609
|
+
const stray = head[at];
|
|
610
|
+
if (stray &&
|
|
611
|
+
!stray.quoted &&
|
|
612
|
+
!isAttrKey(stray) &&
|
|
613
|
+
stray.text !== '(' &&
|
|
614
|
+
stray.text !== 'between' &&
|
|
615
|
+
!isDirection(stray.text) &&
|
|
616
|
+
(MARKS.includes(rightToken.text) || Object.hasOwn(MARK_GLYPHS.to, rightToken.text))) {
|
|
617
|
+
// `edge a -- dot b`: a mark standing apart, which reads as a node named
|
|
618
|
+
// `dot`. Said outright rather than left to the "not a direction" error.
|
|
619
|
+
throw new SourceError(`edge ${left} ${arrow.text} ${rightToken.text} ${stray.text}: a mark touches the dashes or is ` +
|
|
620
|
+
`in brackets — write "${arrow.text}${rightToken.text}" or "${arrow.text} [${rightToken.text}]"`, line);
|
|
621
|
+
}
|
|
540
622
|
const textToken = head[at]?.quoted ? head[at] : undefined;
|
|
541
623
|
if (textToken)
|
|
542
624
|
at += 1;
|
|
@@ -603,9 +685,10 @@ function parseEdge(head, line) {
|
|
|
603
685
|
return {
|
|
604
686
|
kind: 'edge',
|
|
605
687
|
textAttrs: bracket.values,
|
|
606
|
-
from:
|
|
607
|
-
to:
|
|
608
|
-
|
|
688
|
+
from: left,
|
|
689
|
+
to: rightToken.text,
|
|
690
|
+
arrow: arrow.text,
|
|
691
|
+
marks: arrow.marks,
|
|
609
692
|
...(textToken ? { text: textToken.text } : {}),
|
|
610
693
|
...(between ? { between } : {}),
|
|
611
694
|
...(passes.length > 0 ? { passes } : {}),
|
package/dist/render.js
CHANGED
|
@@ -50,7 +50,7 @@ export function render(layout, options = {}) {
|
|
|
50
50
|
const coming = [...routes]
|
|
51
51
|
.filter(([other]) => other !== edge && !sweeps.has(other))
|
|
52
52
|
.map(([, way]) => way.points);
|
|
53
|
-
const d = sweptPath(route.points, sweepObstacles(edge, layout.nodes), [...swept, ...coming], arrowLength(edge.look.thickness), edge.
|
|
53
|
+
const d = sweptPath(route.points, sweepObstacles(edge, layout.nodes), [...swept, ...coming], arrowLength(edge.look.thickness), edge.marks);
|
|
54
54
|
sweeps.set(edge, d);
|
|
55
55
|
swept.push(traceOf(`d="${d}"`));
|
|
56
56
|
}
|
|
@@ -91,11 +91,19 @@ export function render(layout, options = {}) {
|
|
|
91
91
|
width: Math.ceil(ink.maxX) - Math.floor(ink.minX),
|
|
92
92
|
height: Math.ceil(ink.maxY) - Math.floor(ink.minY),
|
|
93
93
|
};
|
|
94
|
-
|
|
94
|
+
// One marker for each mark in each line color the drawing actually uses.
|
|
95
|
+
const markers = new Map();
|
|
96
|
+
for (const edge of layout.edges) {
|
|
97
|
+
const color = lineOf(edge.appearance, theme, theme.edge);
|
|
98
|
+
for (const mark of [edge.marks.from, edge.marks.to]) {
|
|
99
|
+
if (mark !== 'none')
|
|
100
|
+
markers.set(markerId(mark, color), [mark, color]);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
95
103
|
const svg = [
|
|
96
104
|
`<svg xmlns="http://www.w3.org/2000/svg" width="${canvas.width}" height="${canvas.height}" viewBox="${canvas.x} ${canvas.y} ${canvas.width} ${canvas.height}" font-family=${quote(measurer.fontFamily)} font-size="${fontSize}px">`,
|
|
97
105
|
' <defs>',
|
|
98
|
-
...[...
|
|
106
|
+
...[...markers.values()].map(([mark, color]) => markMarker(mark, color, theme.background)),
|
|
99
107
|
// Each mask covers the whole page, in page units: the default region is the
|
|
100
108
|
// line's own bounding box, which a straight level line gives no height, and
|
|
101
109
|
// the line would vanish entirely.
|
|
@@ -346,8 +354,8 @@ function drawEdge(edge, ends, route, sweep, nodes, theme, measurer, fontSize, ma
|
|
|
346
354
|
const { start, end } = ends;
|
|
347
355
|
const color = lineOf(edge.appearance, theme, theme.edge);
|
|
348
356
|
const stroke = strokeOf(color, edge.look);
|
|
349
|
-
const markerEnd = ` marker-end="url(#${markerId(color)})"
|
|
350
|
-
const markerStart = edge.
|
|
357
|
+
const markerEnd = edge.marks.to !== 'none' ? ` marker-end="url(#${markerId(edge.marks.to, color)})"` : '';
|
|
358
|
+
const markerStart = edge.marks.from !== 'none' ? ` marker-start="url(#${markerId(edge.marks.from, color)})"` : '';
|
|
351
359
|
// A named side is a statement about how the line should leave or arrive, so
|
|
352
360
|
// it is drawn as a curve that actually does leave and arrive that way. With
|
|
353
361
|
// neither side named there is nothing to honor and the line stays straight.
|
|
@@ -2382,8 +2390,8 @@ function sideCurve(edge, { start, end, bow }) {
|
|
|
2382
2390
|
const tip = (anchor, arrowed) => arrowed && anchor.side !== undefined
|
|
2383
2391
|
? { x: anchor.x + anchor.tx * head, y: anchor.y + anchor.ty * head }
|
|
2384
2392
|
: { x: anchor.x, y: anchor.y };
|
|
2385
|
-
const p0 = tip(start, edge.
|
|
2386
|
-
const p3 = tip(end,
|
|
2393
|
+
const p0 = tip(start, edge.marks.from !== 'none');
|
|
2394
|
+
const p3 = tip(end, edge.marks.to !== 'none');
|
|
2387
2395
|
const reach = controlReach(start, end);
|
|
2388
2396
|
// A bundle whose sides were too short to spread it takes the rest of the
|
|
2389
2397
|
// room in the middle — see `bowBundles`. Displacing both control points
|
|
@@ -2503,7 +2511,7 @@ const SWEEP_AIR = 5;
|
|
|
2503
2511
|
* has. So where the room is tight the line looks nearly square, and where there
|
|
2504
2512
|
* is room it flows.
|
|
2505
2513
|
*/
|
|
2506
|
-
function sweptPath(points, boxes, neighbors, arrow,
|
|
2514
|
+
function sweptPath(points, boxes, neighbors, arrow, marks) {
|
|
2507
2515
|
const kappa = 0.5523;
|
|
2508
2516
|
const count = points.length;
|
|
2509
2517
|
const legLength = (leg) => Math.hypot(points[leg + 1].x - points[leg].x, points[leg + 1].y - points[leg].y);
|
|
@@ -2515,9 +2523,9 @@ function sweptPath(points, boxes, neighbors, arrow, both) {
|
|
|
2515
2523
|
const room = (leg) => {
|
|
2516
2524
|
const length = legLength(leg);
|
|
2517
2525
|
if (leg === 0)
|
|
2518
|
-
return Math.max(0, length - (
|
|
2526
|
+
return Math.max(0, length - (marks.from !== 'none' ? arrow : 0));
|
|
2519
2527
|
if (leg === count - 2)
|
|
2520
|
-
return Math.max(0, length - arrow);
|
|
2528
|
+
return Math.max(0, length - (marks.to !== 'none' ? arrow : 0));
|
|
2521
2529
|
return length / 2;
|
|
2522
2530
|
};
|
|
2523
2531
|
const cubicAt = (p0, c1, c2, p3, t) => {
|
|
@@ -2742,19 +2750,46 @@ function impliedSide(edge, key) {
|
|
|
2742
2750
|
// Across first, then down: the start leaves across and the end is reached down.
|
|
2743
2751
|
return key === 'from' ? across : upDown;
|
|
2744
2752
|
}
|
|
2745
|
-
|
|
2746
|
-
|
|
2753
|
+
/**
|
|
2754
|
+
* The shape of each mark, in a 10 by 10 box whose right edge is the end of the
|
|
2755
|
+
* line, pointing along it. The marker scales with the stroke, so a unit is 0.7
|
|
2756
|
+
* of the line's width and an outline of 1.4 units is as thick as the line.
|
|
2757
|
+
*
|
|
2758
|
+
* An outlined mark is filled with the page's background so the line running
|
|
2759
|
+
* under it does not show through. Over a filled container that is the wrong
|
|
2760
|
+
* color inside the outline; nothing has hit it yet.
|
|
2761
|
+
*/
|
|
2762
|
+
function markShape(mark, color, background) {
|
|
2763
|
+
const outline = `fill="${background}" stroke="${color}" stroke-width="1.4"`;
|
|
2764
|
+
switch (mark) {
|
|
2765
|
+
case 'arrow':
|
|
2766
|
+
return `<path d="M 0 0 L 10 5 L 0 10 z" fill="${color}"/>`;
|
|
2767
|
+
case 'oarrow':
|
|
2768
|
+
return `<path d="M 0.7 1.2 L 9.3 5 L 0.7 8.8 z" ${outline}/>`;
|
|
2769
|
+
case 'dot':
|
|
2770
|
+
return `<circle cx="5" cy="5" r="4" fill="${color}"/>`;
|
|
2771
|
+
case 'odot':
|
|
2772
|
+
return `<circle cx="5.3" cy="5" r="3.4" ${outline}/>`;
|
|
2773
|
+
case 'diamond':
|
|
2774
|
+
return `<path d="M 0 5 L 5 1.2 L 10 5 L 5 8.8 z" fill="${color}"/>`;
|
|
2775
|
+
case 'odiamond':
|
|
2776
|
+
return `<path d="M 0.9 5 L 5 1.9 L 9.1 5 L 5 8.1 z" ${outline}/>`;
|
|
2777
|
+
case 'bar':
|
|
2778
|
+
return `<rect x="5.5" y="0.5" width="1.4" height="9" fill="${color}"/>`;
|
|
2779
|
+
case 'none':
|
|
2780
|
+
return '';
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
/** One marker, drawn at either end: `auto-start-reverse` turns it round at the start. */
|
|
2784
|
+
function markMarker(mark, color, background) {
|
|
2747
2785
|
return [
|
|
2748
|
-
` <marker id="${
|
|
2749
|
-
`
|
|
2750
|
-
' </marker>',
|
|
2751
|
-
` <marker id="${id}-back" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="${ARROW_MARKER_WIDTH}" markerHeight="${ARROW_MARKER_WIDTH}" orient="auto-start-reverse">`,
|
|
2752
|
-
` <path d="M 0 0 L 10 5 L 0 10 z" fill="${color}"/>`,
|
|
2786
|
+
` <marker id="${markerId(mark, color)}" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="${ARROW_MARKER_WIDTH}" markerHeight="${ARROW_MARKER_WIDTH}" orient="auto-start-reverse">`,
|
|
2787
|
+
` ${markShape(mark, color, background)}`,
|
|
2753
2788
|
' </marker>',
|
|
2754
2789
|
].join('\n');
|
|
2755
2790
|
}
|
|
2756
|
-
function markerId(color) {
|
|
2757
|
-
return
|
|
2791
|
+
function markerId(mark, color) {
|
|
2792
|
+
return `${mark}-${color.replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
2758
2793
|
}
|
|
2759
2794
|
/** The rectangle actually drawn. Differs from the node box only for a deck. */
|
|
2760
2795
|
function faceOf(node) {
|
package/dist/resolve.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ALL_ATTR_KEYS, ATTR_KEYS, PART_SIDES, COLOR_KEYS, COLOR_PARTS, CONTENT_ALIGNMENTS, CONTENT_WIDTHS, LINE_VALUES, describePlacement, nameTarget, } from './ast.js';
|
|
1
|
+
import { ALL_ATTR_KEYS, ATTR_KEYS, MARKS, PART_SIDES, COLOR_KEYS, COLOR_PARTS, CONTENT_ALIGNMENTS, CONTENT_WIDTHS, LINE_VALUES, describePlacement, nameTarget, } from './ast.js';
|
|
2
2
|
import { BARE_EDGE_RUN, CHILD_GAP, DECK_STEP, DEFAULT_FONT_SIZE, DEFAULT_MARGIN, GAPS, HEADER_GAP, ICON_GAP, ICON_LINES, TEXT_CLEARANCE, PAD, SEPARATION_GAP, THICKNESS, arrowLength, fontSizeFor, leafTop, textExtent, textStyleFor, widestLine, } from './constants.js';
|
|
3
3
|
import { fix, reachability, tightest } from './constrain.js';
|
|
4
4
|
import { SourceError } from './errors.js';
|
|
@@ -430,6 +430,34 @@ function appearanceOf(attrs, styles, line) {
|
|
|
430
430
|
}
|
|
431
431
|
return { ...merged, ...attrs };
|
|
432
432
|
}
|
|
433
|
+
/**
|
|
434
|
+
* What is drawn at one end of an edge. A mark written in the arrow is final:
|
|
435
|
+
* the edge's own `from-mark:` or `to-mark:` may repeat it but not contradict
|
|
436
|
+
* it, since both halves are on one line and whichever lost would be dead text.
|
|
437
|
+
* An end the arrow leaves blank is unsaid, and the edge's attribute, then its
|
|
438
|
+
* style, then nothing, fills it. Layering is between places, never within one.
|
|
439
|
+
*/
|
|
440
|
+
function markAt(end, stmt, appearance, what) {
|
|
441
|
+
const key = `${end}-mark`;
|
|
442
|
+
const check = (value) => {
|
|
443
|
+
if (!MARKS.includes(value)) {
|
|
444
|
+
throw new SourceError(`edge ${what}: "${key}: ${value}" is not a mark — use ${MARKS.join(', ')}`, stmt.line);
|
|
445
|
+
}
|
|
446
|
+
return value;
|
|
447
|
+
};
|
|
448
|
+
const written = stmt.marks[end];
|
|
449
|
+
const own = stmt.attrs[key];
|
|
450
|
+
if (written !== undefined) {
|
|
451
|
+
if (own !== undefined && check(own) !== written) {
|
|
452
|
+
throw new SourceError(`edge ${what}: the arrow already puts ${written === 'none' ? 'nothing' : `a${/^[aeiou]/.test(written) ? 'n' : ''} ${written}`} ` +
|
|
453
|
+
`at ${stmt[end]}, and "${key}: ${own}" says otherwise. Say it once: leave that end of the ` +
|
|
454
|
+
`arrow blank and keep "${key}: ${own}", or drop "${key}:"`, stmt.line);
|
|
455
|
+
}
|
|
456
|
+
return written;
|
|
457
|
+
}
|
|
458
|
+
const given = appearance[key];
|
|
459
|
+
return given === undefined ? 'none' : check(given);
|
|
460
|
+
}
|
|
433
461
|
function buildEdges(statements, byName, styles, defaults) {
|
|
434
462
|
const edges = [];
|
|
435
463
|
for (const stmt of statements) {
|
|
@@ -477,7 +505,7 @@ function buildEdges(statements, byName, styles, defaults) {
|
|
|
477
505
|
};
|
|
478
506
|
});
|
|
479
507
|
const appearance = { ...defaults.get('edge'), ...appearanceOf(stmt.attrs, styles, stmt.line) };
|
|
480
|
-
const what = `${stmt.from}
|
|
508
|
+
const what = `${stmt.from} ${stmt.arrow} ${stmt.to}`;
|
|
481
509
|
checkAttrs('edge', what, stmt.attrs, stmt.line);
|
|
482
510
|
checkStyleUse('edge', what, stmt.attrs, styles, stmt.line);
|
|
483
511
|
if (stmt.attrs['url'] !== undefined && stmt.text === undefined) {
|
|
@@ -492,7 +520,10 @@ function buildEdges(statements, byName, styles, defaults) {
|
|
|
492
520
|
edges.push({
|
|
493
521
|
from,
|
|
494
522
|
to,
|
|
495
|
-
|
|
523
|
+
marks: {
|
|
524
|
+
from: markAt('from', stmt, appearance, what),
|
|
525
|
+
to: markAt('to', stmt, appearance, what),
|
|
526
|
+
},
|
|
496
527
|
textAttrs,
|
|
497
528
|
...(stmt.text !== undefined
|
|
498
529
|
? { text: stmt.text, lines: linesFor(stmt.text, textAttrs, `edge ${what}`, stmt.line) }
|
|
@@ -1468,10 +1499,11 @@ function corridorsIn(edges, locate, measurer, fontSize) {
|
|
|
1468
1499
|
const to = locate(edge.to);
|
|
1469
1500
|
if (!from || !to || from.index === to.index)
|
|
1470
1501
|
continue;
|
|
1471
|
-
// With no text, the gap still has to show a run of line behind each
|
|
1502
|
+
// With no text, the gap still has to show a run of line behind each mark,
|
|
1472
1503
|
// or two stacked children with an arrow between them draw as a head alone.
|
|
1504
|
+
const marked = [edge.marks.from, edge.marks.to].filter((mark) => mark !== 'none').length;
|
|
1473
1505
|
if (edge.text === undefined) {
|
|
1474
|
-
const bare = BARE_EDGE_RUN + arrowLength(edge.look.thickness) *
|
|
1506
|
+
const bare = BARE_EDGE_RUN + arrowLength(edge.look.thickness) * marked;
|
|
1475
1507
|
corridors.push({
|
|
1476
1508
|
edge,
|
|
1477
1509
|
from,
|
|
@@ -1486,9 +1518,11 @@ function corridorsIn(edges, locate, measurer, fontSize) {
|
|
|
1486
1518
|
// at either end. The arrowhead is charged on both sides for the same reason:
|
|
1487
1519
|
// it covers the head's length of the line it arrives on, and reserving that
|
|
1488
1520
|
// at one end only would move the midpoint rather than lengthen the run. A
|
|
1489
|
-
// thick line's head is longer, and takes the room it needs.
|
|
1521
|
+
// thick line's head is longer, and takes the room it needs. A line with no
|
|
1522
|
+
// mark at either end is charged for none.
|
|
1523
|
+
const head = marked > 0 ? arrowLength(edge.look.thickness) : 0;
|
|
1490
1524
|
const extent = (axis) => textExtent(edge.lines, edge.textAttrs, axis, measurer, fontSize, edge.line) +
|
|
1491
|
-
(TEXT_CLEARANCE +
|
|
1525
|
+
(TEXT_CLEARANCE + head) * 2;
|
|
1492
1526
|
corridors.push({
|
|
1493
1527
|
edge,
|
|
1494
1528
|
from,
|