reladraw 0.12.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 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.12.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.
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.12.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> -> <to> ["<text>"] [between <a> and <b> [vertically|horizontally]] [attributes]
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
- `a <- b` is exactly `b -> a` — same arrow, same picture. What changes is which name you write first, and that is worth having: the first name reads as the subject of the line, and plenty of edges are about the thing the arrow points at rather than the thing it leaves. `from:` and `to:` follow the arrow, not the writing order, so they still name the tail and the head.
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
 
@@ -439,7 +482,7 @@ The space it leaves is deliberately small, and shrinks further if the side is to
439
482
 
440
483
  #### Going round boxes
441
484
 
442
- A line never passes through a box. It takes the shortest way between its ends that goes through none, leaving a named side heading straight out and arriving at one heading straight in, with as few turns as that length allows. When the direct line is clear, that is the line you get. When it is not, the line goes round whatever is in the way. A line you have steered with `between` or `below <node>` keeps to what you said instead.
485
+ A line never passes through a box. It takes the shortest way between its ends that goes through none, leaving a named side heading straight out and arriving at one heading straight in, with as few turns as that length allows. When the direct line is clear, that is the line you get. When it is not, the line goes round whatever is in the way. A line you have steered with `between` or `below <node>` finds its way the same way, keeping to what you said as well.
443
486
 
444
487
  ```
445
488
  node parser "Parser"
@@ -452,9 +495,9 @@ That line leaves Parser heading left and has to arrive at Renderer from the righ
452
495
 
453
496
  The same rule covers every other case: sides at right angles where one faces away from the other end, two ends naming the same side with one node behind the other, a node back to itself, and a line with no side named that has a box between its ends. Each goes the shortest way round. An end with no side named leaves or arrives by whichever of its box's sides makes the way shortest.
454
497
 
455
- Several lines along the same stretch take a lane each, side by side, instead of drawing on top of one another. 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.
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 curved one sweeps its turns. At an arrowhead, the line always finishes straight, so the head points square into the side.
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
 
@@ -507,7 +550,7 @@ The bow is the shortfall, not a style — nothing bends until the side is full,
507
550
  edge dumps.db1 -> hub "rclone" between desktop1 and laptop1 from: right to: left
508
551
  ```
509
552
 
510
- 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.
553
+ 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 takes the shortest way into the gap that goes round every box and keeps its distance from them, runs straight along it, and takes the shortest way out to its far end, as any line does (see "Going round boxes"). Its text rides in the gap.
511
554
 
512
555
  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.
513
556
 
@@ -521,7 +564,7 @@ edge c -> d "threaded" between a and b vertically
521
564
 
522
565
  Leave it out on a diagonal pair and the error asks for it, in your own node names. Write it where it was not needed and it is checked rather than quietly dropped, so a pair that is only apart vertically will tell you that `horizontally` is wrong. A pair that touches or overlaps has no gap at all, whatever you write, and naming a pair the edge never actually passes is an error too.
523
566
 
524
- Several edges may share one channel, and they take a lane each. As with attachments on a side, which edge gets which lane is derived from where their ends sit, so lines through a channel come out in the order their ends are in and do not cross. The lanes are spaced by what is actually running along them: a text's depth where an edge with text runs, an arrow's width where none does.
567
+ Several edges may share one channel, and they take a lane each, found as every line's lane is (see "Going round boxes"): side by side, arranged so they do not cross where they need not. The lanes are spaced by what is actually running along them: clear of a text where an edge with text runs, an arrow's width apart where none does.
525
568
 
526
569
  A named channel does not widen. It is measured off the layout you described, so if you name a gap too narrow for the lines you put through it they crowd together rather than pushing the two nodes apart. That is the difference between this and a text making room for itself, above: there, the corridor is the gap between the edge's own two ends, and opening it moves them apart exactly as anything else put between them would. Here the pair is named by an edge merely passing through, and nothing yet lets an edge bid into a gap it is only a visitor in. It is the remaining half and it is not built.
527
570
 
@@ -546,7 +589,7 @@ node b "B" below a
546
589
  edge a -> b from: top to: bottom left of a right of b
547
590
  ```
548
591
 
549
- That line leaves the top of A, goes round A's left, crosses over in the gap between the two, passes B on the right, and turns up into B's bottom. The line is drawn as straight stretches joined by rounded corners, each stretch as close to the two ends as its clauses allow, and pushed further out, the way its clause already points, by any node sitting on it.
592
+ That line leaves the top of A, goes round A's left, crosses over in the gap between the two, passes B on the right, and turns up into B's bottom. It finds its way as any line does (see "Going round boxes"): each clause closes off one side of its node — `below resolver` everything above Resolver, within Resolver's width — and the line takes the shortest way that keeps out of those as well as out of every box. Lines told the same things take a lane each rather than one path between them.
550
593
 
551
594
  One clause may name several nodes, and it is not shorthand for several clauses. `below a and b` is below the box bounding both, so it covers the stretch between them too:
552
595
 
@@ -563,15 +606,15 @@ One edge may pass some nodes above or below and others left or right. Passing ab
563
606
 
564
607
  ```
565
608
  node a "A"
566
- node m "M" right of a
567
- node n "N" below m right of m
568
- node b "B" below n right of n
569
- edge a -> b below m left of n
609
+ node n "N" below a
610
+ node m "M" below n
611
+ node b "B" right of m (gap: 80)
612
+ edge a -> b left of n above m
570
613
  ```
571
614
 
572
- That line leaves the bottom of A, runs across just under M, turns down on N's left, and runs across into B. The clauses read as before: `left of n` binds only where the line is level with N, so the last stretch passing under N is not held to it.
615
+ With no clauses that line would run straight from A to B. `left of n` sends it down N's left, and `above m` brings it across between N and M into B, rather than round under M. The clauses read as before: `left of n` binds only where the line is level with N, so the stretch running across below N is not held to it.
573
616
 
574
- A side named at an end says which way the line goes first: `from: right` starts it across, `from: bottom` starts it down. With no side named at either end it goes across first, then down.
617
+ A side named at an end says which way the line goes first: `from: right` starts it across, `from: bottom` starts it down. With no side named, the line takes whichever way keeps the clauses with the fewest turns, and a straight line that already keeps them stays straight.
575
618
 
576
619
  Clauses of one kind may sit on different legs. Here the line goes over P on the way out, down past N, and under M on the way in, turning twice:
577
620
 
@@ -591,11 +634,11 @@ Refused, each by name:
591
634
  - **Clauses that cannot all hold** at one point, as above.
592
635
  - **A crossing with no room.** Two clauses on opposite sides need a gap between their nodes to cross over in. Placed flush — `gap: 0` — there is none, and the error says to give the placement a gap or drop a clause. The gap is never opened for the line: you wrote how far apart those two are.
593
636
  - **A node the line never passes.** `below z` on a line that never goes by Z says nothing.
594
- - **Both kinds where no way of turning keeps them all.** The error names the clauses, or the one reason if every way fails for the same one.
637
+ - **No way keeps them all.** Where the clauses and the boxes between them leave no way through, the error names every clause.
595
638
  - **`between` with a side.** An edge passing between two things already has a side of each.
596
639
  - **A part of a node, or a gap.** A line passes a whole box, and passes it as close as reads clearly.
597
640
 
598
- An end with no side named leaves from the side facing where the line is going.
641
+ An end with no side named leaves by whichever side makes the way shortest.
599
642
 
600
643
  ### How the line is drawn
601
644
 
@@ -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 arrowheads | an edge |
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,21 @@ 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
+
1122
+ **0.13.0**
1123
+
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.
1125
+
1126
+ - A fix, no syntax change. Lines told which side of a node to pass (`below a`) or which gap to go through (`between a and b`) now find their way as every other line does, so they take lanes and keep their distance from boxes, and two lines through one gap no longer cross where they need not. Several identical `below a` lines used to be drawn on one path, reading as one line with three arrowheads, and a `between` line could run a couple of pixels from a box and come into its target down the target's own edge.
1127
+ - A curved line that goes round something now sweeps through its turns instead of cornering, as widely as the room allows; a line that steps sideways and carries on is one S. `path: square, corners: rounded` is the way to get the cornered look. Existing diagrams with a line going round a box, a clause or a `between` will look different.
1128
+ - A line with `below`/`left of` clauses that its direct shape already keeps is drawn direct, as a line without them would be, rather than always with corners. Where it does have to turn, it takes the fewest turns that keep every clause; the old "across first, then down" rule no longer decides. A line told `below m` that would otherwise never reach M's underside is now sent round under M.
1129
+
1071
1130
  **0.12.0**
1072
1131
 
1073
1132
  - Diagrams can be written straight into a web page. Load `dist/element.js` from the package, and each `<reladraw-diagram>` element is drawn in place from the source inside it, with `theme=` choosing the colors. See "In a web page" in the README.
@@ -1156,7 +1215,7 @@ Pre-1.0, so the minor number is where a breaking change goes. Every removal belo
1156
1215
 
1157
1216
  Open questions the benchmark raised, recorded so a later session does not rediscover them.
1158
1217
 
1159
- - ~~One edge passing some nodes above or below and others left or right — an L-shaped line, across under one node and then down past another — is refused today.~~ Answered in 0.6.0: such an edge turns between the two kinds as few times as it can, and where two orders both fit, a standing rule picks — a side named at an end says which way the line goes first, and with none named it goes across first. See "Passing on one side of a node" above.
1218
+ - ~~One edge passing some nodes above or below and others left or right — an L-shaped line, across under one node and then down past another — is refused today.~~ Answered in 0.6.0: such an edge turns between the two kinds as few times as it can, and where two orders both fit, a standing rule picks — a side named at an end says which way the line goes first, and with none named it goes across first. Since 0.13.0, such a line takes whichever way keeps its clauses with the fewest turns, and the across-first rule no longer applies. See "Passing on one side of a node" above.
1160
1219
  - 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.
1161
1220
  - Four machines each holding a `files` child with the same text means writing the same line four times. This is the strongest case for a set-level declaration, for terseness rather than for placement.
1162
1221
  - 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.
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
- /** `<->` rather than `->`. */
284
- both: boolean;
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
- /** `->` and `<->`. */
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
- // `<->` first, or `<-` would match its opening half and leave a stray `>`.
82
- arrow: '<->|->|<-',
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
- both: boolean;
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
- * `edge <from> -> <to> ["<text>"] [between <a> and <b>]`, with `<->` for a
521
- * two-headed arrow and `<-` for one pointing the other way.
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
- * `a <- b` is exactly `b -> a` and carries no meaning of its own downstream.
524
- * What it buys is the ordering: the name written first is the one the line is
525
- * about, and plenty of edges have the target as their subject.
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[2];
531
- if (!arrow || arrow.quoted || !ARROWS.includes(arrow.text)) {
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
- const back = arrow.text === '<-';
539
- let at = 4;
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: back ? rightToken.text : left,
607
- to: back ? left : rightToken.text,
608
- both: arrow.text === '<->',
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 } : {}),