reladraw 0.5.0 → 0.7.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 +3 -3
- package/SYNTAX.md +140 -4
- package/dist/ast.d.ts +25 -0
- package/dist/ast.js +18 -0
- package/dist/constants.d.ts +9 -0
- package/dist/constants.js +15 -0
- package/dist/model.d.ts +28 -1
- package/dist/parser.js +50 -6
- package/dist/render.js +1292 -17
- package/dist/resolve.js +110 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,7 +68,7 @@ It is [plain Markdown](.claude/skills/reladraw/SKILL.md) with the syntax referen
|
|
|
68
68
|
|
|
69
69
|
## Status
|
|
70
70
|
|
|
71
|
-
Version 0.
|
|
71
|
+
Version 0.7.0. Early, but it runs: a parser, resolver and SVG renderer in TypeScript with no runtime dependencies, and a command-line tool that takes a text file and writes a standalone SVG. The comparison at the top of this page is that pipeline run on [`examples/arch.reladraw`](examples/arch.reladraw). What is still visibly off there is typography, not placement.
|
|
72
72
|
|
|
73
73
|
```
|
|
74
74
|
npm install -g reladraw
|
|
@@ -121,7 +121,7 @@ Three design problems decide how much machinery this needs, and the first outran
|
|
|
121
121
|
|
|
122
122
|
**What the engine is allowed to decide.** Auto-layout is refused, because a picture chosen by an algorithm is not predictable from its source, and that predictability is the entire point. Working out coordinates from an arrangement the author stated is a different thing and is simply the job. The test between them: the engine's freedom may affect distances and never relationships. If a default can change which side of something a node sits on, the language was short a statement and the tool should say so rather than guess.
|
|
123
123
|
|
|
124
|
-
**Overlap and edge routing.** Relative placement with default spacing collides as soon as two clusters grow toward each other
|
|
124
|
+
**Overlap and edge routing.** Relative placement with default spacing collides as soon as two clusters grow toward each other, and nodes are kept apart for exactly that reason. Edges are different: which side of a node a line passes is a relationship, so the tool does not pick a route around an obstacle by itself. You say where a line goes instead — the side it leaves and arrives on, the gap it runs through, the nodes it passes above or below — and the line is drawn curved, square or straight through those points. A line that crosses something it should not is one you have not said enough about yet. Whether the tool should at least point that out, naming the clause that would fix it, is an open question.
|
|
125
125
|
|
|
126
126
|
## Prior art
|
|
127
127
|
|
|
@@ -143,4 +143,4 @@ The license covers the code, not the name: it grants no rights to use "reladraw"
|
|
|
143
143
|
|
|
144
144
|
## Contributing
|
|
145
145
|
|
|
146
|
-
Issues are wanted — especially a diagram you tried to write and could not.
|
|
146
|
+
Issues are wanted — especially a diagram you tried to write and could not. While the language is changing quickly, an issue is more useful than a pull request; [CONTRIBUTING.md](CONTRIBUTING.md) says why.
|
package/SYNTAX.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Syntax reference — 0.
|
|
1
|
+
# Syntax reference — 0.7.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
|
|
|
@@ -422,6 +422,23 @@ You name a side and never a point on it. Alone on a side, an edge lands at its c
|
|
|
422
422
|
|
|
423
423
|
The space it leaves is deliberately small, and shrinks further if the side is too short to hold the whole group. On a side short enough, it shrinks to nothing and the attachments coincide. Their texts still come apart, because the lines bow in the middle to make up what the side could not give them, but the arrowheads themselves land on one point and nothing warns you — so a small node with several edges arriving on one side is worth a look.
|
|
424
424
|
|
|
425
|
+
#### Ends that face away from each other
|
|
426
|
+
|
|
427
|
+
```
|
|
428
|
+
node parser "Parser"
|
|
429
|
+
node resolver "Resolver" right of parser
|
|
430
|
+
node renderer "Renderer" right of resolver
|
|
431
|
+
edge parser -> renderer "test edge" from: left to: right
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
That line leaves Parser heading left and has to arrive at Renderer from the right, so it must turn back on itself somewhere. It turns back in the gap between its two nodes when there is one wide enough to hold the line and its text, and otherwise it goes over the top: out of its side, along the top of the row clear of the tallest node between its ends, and down into the far side. The text rides on that top stretch, so it never sits on a node. The same goes for `from: top to: bottom` in a column, which goes round the right, and for an edge from a node back to itself.
|
|
435
|
+
|
|
436
|
+
Several such lines over the same row take a lane each, the shorter inside the longer, so they nest instead of drawing on top of one another.
|
|
437
|
+
|
|
438
|
+
Sides at right angles can face away too. `from: left to: top` with the far node to the right leaves heading away from it, so the line steps out of the left, goes up past both nodes, runs along above them and comes straight down into the top. The side named at the far end says which way round: `to: bottom` takes it underneath instead.
|
|
439
|
+
|
|
440
|
+
Over the top is the rule, not a guess the tool makes each time — it does not look for the shorter way round. To send the line underneath or round the left, say which side of a node it passes: `below resolver`. See "Passing on one side of a node".
|
|
441
|
+
|
|
425
442
|
#### Several edges between the same two sides
|
|
426
443
|
|
|
427
444
|
Edges that run between the *same* pair of sides are a case of their own, because "where the far ends sit" cannot order them: every one of them goes to the same node.
|
|
@@ -487,6 +504,113 @@ Several edges may share one channel, and they take a lane each. As with attachme
|
|
|
487
504
|
|
|
488
505
|
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.
|
|
489
506
|
|
|
507
|
+
### Passing on one side of a node
|
|
508
|
+
|
|
509
|
+
`above`, `below`, `left of` and `right of`, written on an edge, say which side of a node the line passes.
|
|
510
|
+
|
|
511
|
+
```
|
|
512
|
+
node parser "Parser"
|
|
513
|
+
node resolver "Resolver" right of parser
|
|
514
|
+
node renderer "Renderer" right of resolver
|
|
515
|
+
edge parser -> renderer "underneath" from: left to: right below resolver
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
They are the placement words, and they mean what `between` means: only where the line passes that node. `below resolver` says the line is below Resolver where it goes by Resolver, not that the whole line is. That edge would go over the top of the row on its own; with the clause it leaves Parser, drops under the row and comes up into Renderer.
|
|
519
|
+
|
|
520
|
+
Write as many as you need, one for each node you have something to say about. The line meets them in the order they sit along its way, so no clause says an order:
|
|
521
|
+
|
|
522
|
+
```
|
|
523
|
+
node a "A"
|
|
524
|
+
node b "B" below a
|
|
525
|
+
edge a -> b from: top to: bottom left of a right of b
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
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.
|
|
529
|
+
|
|
530
|
+
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:
|
|
531
|
+
|
|
532
|
+
```
|
|
533
|
+
node t1 "T1"
|
|
534
|
+
node t2 "T2" right of t1 (gap: wide)
|
|
535
|
+
node peak "Peak" right of t1 left of t2 above t1
|
|
536
|
+
edge t1 -> t2 from: bottom to: bottom below t1 above peak below t2
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
Those three clauses let the line rise over Peak in the gap. Write `below t1 and t2 above peak` instead and it is refused, because Peak sits over the stretch `below t1 and t2` covers, and the line cannot be below the one and above the other at the same point.
|
|
540
|
+
|
|
541
|
+
One edge may pass some nodes above or below and others left or right. Passing above or below is done travelling across the page, and passing left or right travelling up or down it, so the line turns between the two, as few times as it can:
|
|
542
|
+
|
|
543
|
+
```
|
|
544
|
+
node a "A"
|
|
545
|
+
node m "M" right of a
|
|
546
|
+
node n "N" below m right of m
|
|
547
|
+
node b "B" below n right of n
|
|
548
|
+
edge a -> b below m left of n
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
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.
|
|
552
|
+
|
|
553
|
+
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.
|
|
554
|
+
|
|
555
|
+
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:
|
|
556
|
+
|
|
557
|
+
```
|
|
558
|
+
node a "A"
|
|
559
|
+
node p "P" right of a
|
|
560
|
+
node n "N" below p right of p
|
|
561
|
+
node m "M" below n right of n
|
|
562
|
+
node b "B" right of m
|
|
563
|
+
edge a -> b from: right to: bottom above p right of n below m
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
Each clause binds whichever legs pass its node, so a node wide enough to sit under two legs holds both of them to its side.
|
|
567
|
+
|
|
568
|
+
Refused, each by name:
|
|
569
|
+
|
|
570
|
+
- **Clauses that cannot all hold** at one point, as above.
|
|
571
|
+
- **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.
|
|
572
|
+
- **A node the line never passes.** `below z` on a line that never goes by Z says nothing.
|
|
573
|
+
- **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.
|
|
574
|
+
- **`between` with a side.** An edge passing between two things already has a side of each.
|
|
575
|
+
- **A part of a node, or a gap.** A line passes a whole box, and passes it as close as reads clearly.
|
|
576
|
+
|
|
577
|
+
An end with no side named leaves from the side facing where the line is going.
|
|
578
|
+
|
|
579
|
+
### How the line is drawn
|
|
580
|
+
|
|
581
|
+
Everything about the line itself goes in its bracket:
|
|
582
|
+
|
|
583
|
+
```
|
|
584
|
+
edge api -> db line: (path: square, corners: rounded)
|
|
585
|
+
edge api -> cache line: (pattern: dashed, thickness: thin)
|
|
586
|
+
edge web -> api line: (color: #d2904e, thickness: thick, crossing: arc)
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
`line: red` is short for `line: (color: red)`, and is still the way to write a color alone. Every other property needs the bracket, and `line: square` is refused with the bracket it meant. Like any attribute, the bracket may go on an edge, in a style, or on `default edge` — `default edge line: (path: square, corners: rounded)` draws a whole diagram with right angles.
|
|
590
|
+
|
|
591
|
+
| property | takes | says |
|
|
592
|
+
|---|---|---|
|
|
593
|
+
| `color` | a color | as `line:` alone |
|
|
594
|
+
| `path` | `curved` (the default), `square`, `straight` | how the line joins the points it has to meet |
|
|
595
|
+
| `corners` | `sharp` (the default), `rounded` | how a square or straight line turns |
|
|
596
|
+
| `crossing` | `none` (the default), `arc`, `gap`, `square` | how the line is drawn where it crosses an earlier one |
|
|
597
|
+
| `pattern` | `solid` (the default), `dashed`, `dotted`, `dash-dot` | |
|
|
598
|
+
| `thickness` | `thin`, `normal` (the default), `thick`, or a number of pixels | |
|
|
599
|
+
|
|
600
|
+
**A path is the rule for joining what the file fixed**: where the line leaves and which way it heads, where it arrives and from which direction, and any sides it passes. Out of A's top and into B's left, with B up and to the right:
|
|
601
|
+
|
|
602
|
+
- `curved` bends round from heading up to heading right. It is how every line was drawn before there was a choice, including the straight line an edge naming no sides has always been.
|
|
603
|
+
- `square` goes up, then across into B — one right angle, because leaving upward and arriving from the left fixes which way it turns. A square line naming no sides picks them: boxes sharing a column join top to bottom, boxes sharing a row side to side, and otherwise the line goes across first, then down. An end naming no side, beside one that does, takes whichever side needs the fewest turns.
|
|
604
|
+
- `straight` is one straight piece from A's top to B's left, ignoring which way each faces. Given `below c` or `between a and b`, it bends where that clause puts a point it has to pass, and nowhere else.
|
|
605
|
+
|
|
606
|
+
**Corners** apply wherever a line has them: every bend in a square line, and the bends a clause puts in a straight one. A curved line has none, so `corners: rounded` written on a curved edge is refused. From a style or a default it rounds the edges that have corners and leaves curved ones alone, as a style's `fill:` lands only on the things that have an inside.
|
|
607
|
+
|
|
608
|
+
**A crossing is drawn on the later line**: the edge written further down the file jumps the one written above it. `arc` is a small half-circle hop, `square` a three-sided jump, and `gap` a short break in the later line, as though it passed underneath. A line never jumps within reach of its own ends, where two lines meeting at a box are not passing each other.
|
|
609
|
+
|
|
610
|
+
**A pattern is measured in thicknesses**, so dashes keep their proportions on a thick line.
|
|
611
|
+
|
|
612
|
+
**A thick line takes room.** Its arrowhead grows with it, and the room an edge's text makes for itself counts the arrowhead it will actually have, so thickening a line with text between two close nodes can push them apart — the same way a larger text grows its node. Color, path, corners, crossings and pattern move nothing.
|
|
613
|
+
|
|
490
614
|
## Against a part of a node
|
|
491
615
|
|
|
492
616
|
```
|
|
@@ -627,7 +751,7 @@ Every attribute, and what takes one. The kinds here are what a node's **body** i
|
|
|
627
751
|
| `fill` | ✓ | | | | color — see "A color names the part it colors" |
|
|
628
752
|
| `border` | ✓ | | | | color |
|
|
629
753
|
| `text` | ✓ | ✓ | ✓ | ✓ | the text's properties, in brackets — a style's form of what a node or an edge writes after its own words |
|
|
630
|
-
| `line` | | | | ✓ | color |
|
|
754
|
+
| `line` | | | | ✓ | color, or everything about the line in brackets — see "How the line is drawn" |
|
|
631
755
|
| `url` | ✓ | ✓ | ✓ | ✓ | a destination to open when the thing is clicked |
|
|
632
756
|
|
|
633
757
|
The `diagram` statement has a vocabulary of its own — `theme`, `background` and `text` — which is checked the same way. Writing `background:` on a node is an error that points at `fill:`, and `theme:` on one points at `diagram theme:`. A `default` takes a narrower list for each kind; see "Defaults".
|
|
@@ -831,7 +955,7 @@ Deliberate omissions. What they protect is that the renderer never *chooses* an
|
|
|
831
955
|
- **Guessing an axis nobody constrained.** When two placements bind one axis and nothing binds the other, the tool refuses rather than picking a target to center on. Choosing there would decide which row a node shares, not how far it sits from something.
|
|
832
956
|
- **Placements that run in a circle.** A loop where each placement demands more room than the last cannot be satisfied and is an error naming the placements involved. A target does *not* have to be positioned before the node naming it — the whole system is solved at once — so ordinary mutual references are fine.
|
|
833
957
|
- **Edge waypoints.** A point a line must pass through is a coordinate wearing a hat. Saying a line goes between two named things is not one — it names things the diagram already contains, and it survives those things moving.
|
|
834
|
-
- **Choosing a route.** The tool will not find its own way around an obstacle. A line that crosses something it should not is a line you have not yet said enough about, and `between`
|
|
958
|
+
- **Choosing a route.** The tool will not find its own way around an obstacle. A line that crosses something it should not is a line you have not yet said enough about, and `between` and `below <node>` are how you say it.
|
|
835
959
|
- **Set-level placement.** Four siblings around a hub are four statements today. Whether a durable group that reflows when a member is added is worth the same-axis conflict it introduces is undecided.
|
|
836
960
|
|
|
837
961
|
Note what is *not* on this list: saying more about where something goes. A statement that lets you be more precise is not a step toward auto-layout, and the first version was short enough of them to render the benchmark wrong.
|
|
@@ -840,7 +964,7 @@ Note what is *not* on this list: saying more about where something goes. A state
|
|
|
840
964
|
|
|
841
965
|
Designed, decided, and absent from the code. Written down so the next version has somewhere to start.
|
|
842
966
|
|
|
843
|
-
**Nothing keeps an edge clear of a node on its own.** Non-overlap applies to nodes only. A line may still cut across a node it has nothing to do with, and an edge text may still land on top of one. `between`
|
|
967
|
+
**Nothing keeps an edge clear of a node on its own.** Non-overlap applies to nodes only. A line may still cut across a node it has nothing to do with, and an edge text may still land on top of one. `between` and `below <node>` are how you say where a line goes when that matters, and nothing checks the ones where you have not said. A check belongs on the diagnostics list, but finding a route by itself does not — see "What the language refuses".
|
|
844
968
|
|
|
845
969
|
**An icon outside the built-in seven.** The set is closed, and a diagram wanting a picture that is not in it has nowhere to go. The two shapes this could take are a declaration in the file, `icon <name> "<path data>"` beside `style`, and `icon: ./thing.svg` inlined by the tool at render time. Either keeps the output standalone, which is the constraint any answer has to meet.
|
|
846
970
|
|
|
@@ -882,10 +1006,21 @@ That one was found by testing the lexer, not by rendering — and it could not h
|
|
|
882
1006
|
|
|
883
1007
|
~~An edge text ignored the line break.~~ Fixed. ` / ` split a node's text and was never applied to an edge's, so the marker came out as a literal slash on an arrow and the benchmark's two-line captions had to be flattened to one. The measurer had always returned the split lines; the renderer was handing it the raw string and drawing that instead. The block now centers on the point the text already occupied, so a one-line text sits exactly where it did.
|
|
884
1008
|
|
|
1009
|
+
~~An edge whose ends faced away from each other was drawn through its own nodes.~~ Fixed. `from: left to: right` with the far node further right was one curve bending out at both ends, and with the two nodes in a row it flattened into a straight line through both of them and anything between, its text landing on whatever was in the middle. It was not a matter of exact alignment: a node stepped down a little drew a tilted version of the same line. Such an edge now turns back in the gap between its nodes if there is room, and goes over the top of the row if not. See "Ends that face away from each other".
|
|
1010
|
+
|
|
885
1011
|
## Changelog
|
|
886
1012
|
|
|
887
1013
|
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.
|
|
888
1014
|
|
|
1015
|
+
**0.7.0**
|
|
1016
|
+
|
|
1017
|
+
- `line:` takes a bracket for everything about how the line is drawn: `path: curved | square | straight`, `corners: sharp | rounded`, `crossing: none | arc | gap | square`, `pattern: solid | dashed | dotted | dash-dot` and `thickness: thin | normal | thick` or a number. `line: red` still works, as the short form of `line: (color: red)`. Every default is how lines were drawn before, so no existing file changes.
|
|
1018
|
+
|
|
1019
|
+
**0.6.0**
|
|
1020
|
+
|
|
1021
|
+
- An edge whose ends face away from each other, such as `from: left to: right` with the far node to the right, goes around instead of through its own nodes: through the gap between them if there is room, over the top of the row if not. Several over one row take a lane each. Sides at right angles, such as `from: left to: top`, go around the same way when one faces away.
|
|
1022
|
+
- `above`, `below`, `left of` and `right of` on an edge say which side of a node the line passes: `edge a -> b below c`. Any number, one per node; one naming several nodes covers the stretch between them too. One edge may mix the two kinds, `below m left of n`, and the line turns between them as often as they need. Clauses that cannot all be drawn are refused with the reason.
|
|
1023
|
+
|
|
889
1024
|
**0.5.0**
|
|
890
1025
|
|
|
891
1026
|
- Named themes: `diagram theme: nord`, one of thirteen, and `--theme` on the command line to render a file in another without editing it. The playground has a theme picker, which writes the `theme:` line into the source.
|
|
@@ -920,6 +1055,7 @@ Pre-1.0, so the minor number is where a breaking change goes. Every removal belo
|
|
|
920
1055
|
|
|
921
1056
|
Open questions the benchmark raised, recorded so a later session does not rediscover them.
|
|
922
1057
|
|
|
1058
|
+
- 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. Often the picture leaves only one order for the two legs; where two orders both fit, the tool must not pick one, and whether it asks for another clause or follows a standing rule is not decided.
|
|
923
1059
|
- 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.
|
|
924
1060
|
- 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.
|
|
925
1061
|
- 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
|
@@ -241,6 +241,24 @@ export type Attrs = Record<string, string>;
|
|
|
241
241
|
* `align` is how its lines range against each other once it is there.
|
|
242
242
|
*/
|
|
243
243
|
export declare const TEXT_KEYS: readonly ["color", "size", "wrap", "align", "at"];
|
|
244
|
+
/**
|
|
245
|
+
* What an edge's line takes in its bracket: `line: (path: square, pattern:
|
|
246
|
+
* dashed)`. The line is a part in the sense the text is, so every property of
|
|
247
|
+
* it goes in one bracket, as the text's do. `line: red` stays as the short form
|
|
248
|
+
* of `line: (color: red)`, and both are stored under the bare `line` key.
|
|
249
|
+
*/
|
|
250
|
+
export declare const LINE_KEYS: readonly ["color", "path", "corners", "crossing", "pattern", "thickness"];
|
|
251
|
+
/**
|
|
252
|
+
* The words each of the line's properties takes, the default first. Thickness
|
|
253
|
+
* takes a plain number of pixels as well.
|
|
254
|
+
*/
|
|
255
|
+
export declare const LINE_VALUES: {
|
|
256
|
+
readonly path: readonly ["curved", "square", "straight"];
|
|
257
|
+
readonly corners: readonly ["sharp", "rounded"];
|
|
258
|
+
readonly crossing: readonly ["none", "arc", "gap", "square"];
|
|
259
|
+
readonly pattern: readonly ["solid", "dashed", "dotted", "dash-dot"];
|
|
260
|
+
readonly thickness: readonly ["normal", "thin", "thick"];
|
|
261
|
+
};
|
|
244
262
|
export interface NodeStmt {
|
|
245
263
|
kind: 'node';
|
|
246
264
|
name: string;
|
|
@@ -269,6 +287,13 @@ export interface EdgeStmt {
|
|
|
269
287
|
textAttrs: Attrs;
|
|
270
288
|
/** `between desktop1 and laptop1` — the gap the line passes through. */
|
|
271
289
|
between?: Passage;
|
|
290
|
+
/**
|
|
291
|
+
* `below resolver`, `left of a and b` — which side of a node the line is on
|
|
292
|
+
* where it passes that node. The placement words, because it is the same
|
|
293
|
+
* statement about the picture; on an edge it binds only the stretch where the
|
|
294
|
+
* line is passing, as `between` does. Always `kind: 'offset'` with no gap.
|
|
295
|
+
*/
|
|
296
|
+
passes?: OffsetPlacement[];
|
|
272
297
|
attrs: Attrs;
|
|
273
298
|
line: number;
|
|
274
299
|
}
|
package/dist/ast.js
CHANGED
|
@@ -222,6 +222,24 @@ export function describeAxis(axis) {
|
|
|
222
222
|
* `align` is how its lines range against each other once it is there.
|
|
223
223
|
*/
|
|
224
224
|
export const TEXT_KEYS = ['color', 'size', 'wrap', 'align', 'at'];
|
|
225
|
+
/**
|
|
226
|
+
* What an edge's line takes in its bracket: `line: (path: square, pattern:
|
|
227
|
+
* dashed)`. The line is a part in the sense the text is, so every property of
|
|
228
|
+
* it goes in one bracket, as the text's do. `line: red` stays as the short form
|
|
229
|
+
* of `line: (color: red)`, and both are stored under the bare `line` key.
|
|
230
|
+
*/
|
|
231
|
+
export const LINE_KEYS = ['color', 'path', 'corners', 'crossing', 'pattern', 'thickness'];
|
|
232
|
+
/**
|
|
233
|
+
* The words each of the line's properties takes, the default first. Thickness
|
|
234
|
+
* takes a plain number of pixels as well.
|
|
235
|
+
*/
|
|
236
|
+
export const LINE_VALUES = {
|
|
237
|
+
path: ['curved', 'square', 'straight'],
|
|
238
|
+
corners: ['sharp', 'rounded'],
|
|
239
|
+
crossing: ['none', 'arc', 'gap', 'square'],
|
|
240
|
+
pattern: ['solid', 'dashed', 'dotted', 'dash-dot'],
|
|
241
|
+
thickness: ['normal', 'thin', 'thick'],
|
|
242
|
+
};
|
|
225
243
|
/**
|
|
226
244
|
* The attributes a `diagram` statement understands. `text` takes a bracket,
|
|
227
245
|
* `text: (color: …)`, and sets the text color of everything at once — the
|
package/dist/constants.d.ts
CHANGED
|
@@ -50,6 +50,15 @@ export declare const ARROW_MARKER_WIDTH = 7;
|
|
|
50
50
|
* agreed by coincidence is a number that drifts.
|
|
51
51
|
*/
|
|
52
52
|
export declare const ARROW_LENGTH: number;
|
|
53
|
+
/** What `thickness: thin | normal | thick` draw, in pixels. `normal` is the line as it always was. */
|
|
54
|
+
export declare const THICKNESS: Record<string, number>;
|
|
55
|
+
/**
|
|
56
|
+
* How much of a line of this thickness its arrowhead covers. The marker scales
|
|
57
|
+
* with the stroke, so a thick line gets a bigger head, and the room an edge's
|
|
58
|
+
* text makes for itself counts the head it will actually be drawn with — a
|
|
59
|
+
* thickness is a size, like a text's, and a size is allowed to take room.
|
|
60
|
+
*/
|
|
61
|
+
export declare function arrowLength(thickness: number): number;
|
|
53
62
|
/**
|
|
54
63
|
* Line left showing between an edge's text and the box at that end of the
|
|
55
64
|
* corridor it crosses.
|
package/dist/constants.js
CHANGED
|
@@ -56,6 +56,21 @@ export const ARROW_MARKER_WIDTH = 7;
|
|
|
56
56
|
* agreed by coincidence is a number that drifts.
|
|
57
57
|
*/
|
|
58
58
|
export const ARROW_LENGTH = ARROW_MARKER_WIDTH * LINE_WIDTH;
|
|
59
|
+
/** What `thickness: thin | normal | thick` draw, in pixels. `normal` is the line as it always was. */
|
|
60
|
+
export const THICKNESS = {
|
|
61
|
+
thin: 1,
|
|
62
|
+
normal: LINE_WIDTH,
|
|
63
|
+
thick: LINE_WIDTH * 2,
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* How much of a line of this thickness its arrowhead covers. The marker scales
|
|
67
|
+
* with the stroke, so a thick line gets a bigger head, and the room an edge's
|
|
68
|
+
* text makes for itself counts the head it will actually be drawn with — a
|
|
69
|
+
* thickness is a size, like a text's, and a size is allowed to take room.
|
|
70
|
+
*/
|
|
71
|
+
export function arrowLength(thickness) {
|
|
72
|
+
return ARROW_MARKER_WIDTH * thickness;
|
|
73
|
+
}
|
|
59
74
|
/**
|
|
60
75
|
* Line left showing between an edge's text and the box at that end of the
|
|
61
76
|
* corridor it crosses.
|
package/dist/model.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Attrs, Axis, Kind, Placement } from './ast.js';
|
|
1
|
+
import type { Attrs, Axis, Direction, Kind, 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`. */
|
|
@@ -7,6 +7,17 @@ export interface LayoutPassage {
|
|
|
7
7
|
/** Which gap, where the pair has two. Absent when the pair leaves no doubt. */
|
|
8
8
|
axis?: Axis;
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* `below resolver` on an edge with its targets resolved: the line is on that
|
|
12
|
+
* side of the box bounding `nodes`, where it passes them. Mirrors the edge's
|
|
13
|
+
* `passes` in `ast.ts`.
|
|
14
|
+
*/
|
|
15
|
+
export interface LayoutPass {
|
|
16
|
+
direction: Direction;
|
|
17
|
+
nodes: LayoutNode[];
|
|
18
|
+
/** As the author wrote it, for error messages. */
|
|
19
|
+
written: string;
|
|
20
|
+
}
|
|
10
21
|
/** A node with its geometry solved. Coordinates are absolute, origin top-left. */
|
|
11
22
|
/** A distance past each side of a box. */
|
|
12
23
|
export interface Reach {
|
|
@@ -116,10 +127,26 @@ export interface LayoutEdge {
|
|
|
116
127
|
* rather than solved for, so edges stay out of the constraint system entirely.
|
|
117
128
|
*/
|
|
118
129
|
between?: LayoutPassage;
|
|
130
|
+
/** Which side of which nodes the line passes. Measured in the renderer, like `between`. */
|
|
131
|
+
passes?: LayoutPass[];
|
|
132
|
+
/** What the line's bracket says, checked and with every default filled in. */
|
|
133
|
+
look: LineLook;
|
|
119
134
|
attrs: Attrs;
|
|
120
135
|
appearance: Attrs;
|
|
121
136
|
line: number;
|
|
122
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* How an edge's line is drawn: `line: (path: square, corners: rounded, …)`.
|
|
140
|
+
* Only `thickness` takes room; the rest is appearance and moves nothing.
|
|
141
|
+
*/
|
|
142
|
+
export interface LineLook {
|
|
143
|
+
path: 'curved' | 'square' | 'straight';
|
|
144
|
+
corners: 'sharp' | 'rounded';
|
|
145
|
+
crossing: 'none' | 'arc' | 'gap' | 'square';
|
|
146
|
+
pattern: 'solid' | 'dashed' | 'dotted' | 'dash-dot';
|
|
147
|
+
/** In pixels. */
|
|
148
|
+
thickness: number;
|
|
149
|
+
}
|
|
123
150
|
export interface Layout {
|
|
124
151
|
/** Every node, containers and children alike, in declaration order. */
|
|
125
152
|
nodes: LayoutNode[];
|
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, 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, OPPOSITE, isDirection, isPart, isPosition, listTargets, nameTarget, } from './ast.js';
|
|
2
2
|
import { SourceError } from './errors.js';
|
|
3
3
|
import { isAttrKey, tokenizeLine } from './lexer.js';
|
|
4
4
|
import { THEME_NAMES, THEMES } from './themes.js';
|
|
@@ -138,12 +138,25 @@ function parseTail(tokens, start, line, subject, other) {
|
|
|
138
138
|
const BRACKET_KEYS = {
|
|
139
139
|
text: TEXT_KEYS,
|
|
140
140
|
contents: CONTENTS_KEYS,
|
|
141
|
+
line: LINE_KEYS,
|
|
141
142
|
};
|
|
142
143
|
/** How each bracketed key's error quotes itself back, and what it is about. */
|
|
143
144
|
const BRACKET_ABOUT = {
|
|
144
145
|
text: { kind: 'a text', example: 'color: muted' },
|
|
145
146
|
contents: { kind: 'a `contents:` bracket', example: 'widths: match' },
|
|
147
|
+
line: { kind: 'a line', example: 'path: square' },
|
|
146
148
|
};
|
|
149
|
+
/**
|
|
150
|
+
* Which of the line's properties a word belongs to, so that `line: square` —
|
|
151
|
+
* the color key given a shape — can be pointed at the bracket.
|
|
152
|
+
*/
|
|
153
|
+
function linePropertyOf(word) {
|
|
154
|
+
for (const [property, words] of Object.entries(LINE_VALUES)) {
|
|
155
|
+
if (words.includes(word) && word !== 'normal')
|
|
156
|
+
return property;
|
|
157
|
+
}
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
147
160
|
/**
|
|
148
161
|
* The top-level keys that moved into the text's bracket in 0.3.0, and the
|
|
149
162
|
* substitution each one gets. They are properties of a node's *text* and never
|
|
@@ -186,7 +199,11 @@ function readAttr(tokens, at, attrs, line, subject) {
|
|
|
186
199
|
// Two brackets for one part are fine as long as they say different things;
|
|
187
200
|
// the same property in both is the same defect as `fill:` written twice.
|
|
188
201
|
for (const [inner, value] of Object.entries(read.values)) {
|
|
189
|
-
|
|
202
|
+
// A line's color is stored where `line: red` puts it, so the short form
|
|
203
|
+
// and the bracket are one key: a style's `line: red` merges under an
|
|
204
|
+
// edge's `line: (color: blue)`, and writing both on one line is a repeat.
|
|
205
|
+
const stored = key === 'line' && inner === 'color' ? 'line' : `${key}.${inner}`;
|
|
206
|
+
setOnce(attrs, stored, value, subject, line, { key: `${key}: (${inner}: …)`, value: (v) => v });
|
|
190
207
|
}
|
|
191
208
|
return read.next;
|
|
192
209
|
}
|
|
@@ -251,7 +268,7 @@ function readAttr(tokens, at, attrs, line, subject) {
|
|
|
251
268
|
});
|
|
252
269
|
return next;
|
|
253
270
|
}
|
|
254
|
-
if (bracketKeys !== undefined && key !== 'text') {
|
|
271
|
+
if (bracketKeys !== undefined && key !== 'text' && key !== 'line') {
|
|
255
272
|
// `contents: match` names the part and then says one of its two properties
|
|
256
273
|
// without saying which. The brackets are what make the level shift visible,
|
|
257
274
|
// so there is no unbracketed spelling to fall back to.
|
|
@@ -292,6 +309,15 @@ function readAttr(tokens, at, attrs, line, subject) {
|
|
|
292
309
|
? `\`text:\` is how a style says something about text, not how anything sets it — write the words in quotes after the name, as in \`node name ${quoteOf(valueToken.text)}\``
|
|
293
310
|
: `\`text:\` takes the text's properties in brackets — write \`text: (color: ${valueToken.text})\` in a style, and \`(color: ${valueToken.text})\` in the brackets after a node's or an edge's own text`, line);
|
|
294
311
|
}
|
|
312
|
+
if (key === 'line' && !valueToken.quoted) {
|
|
313
|
+
// `line: red` is the short form of the color, and the only one: every other
|
|
314
|
+
// property of the line needs the bracket to say which it is.
|
|
315
|
+
const property = linePropertyOf(valueToken.text) ?? (/^\d/.test(valueToken.text) ? 'thickness' : undefined);
|
|
316
|
+
if (property !== undefined) {
|
|
317
|
+
throw new SourceError(`\`line: ${valueToken.text}\` — \`line:\` on its own takes a color. The line's other properties go in its bracket: ` +
|
|
318
|
+
`\`line: (${property}: ${valueToken.text})\``, line);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
295
321
|
if (key === 'align' && valueToken.text === 'widths') {
|
|
296
322
|
// Removed in 0.3.0. It was a size operation wearing an alignment's name,
|
|
297
323
|
// and its value set had one member — a flag in a property's clothes. Its
|
|
@@ -461,9 +487,26 @@ function parseEdge(head, line) {
|
|
|
461
487
|
between = { targets: read.targets, ...(axis !== undefined ? { axis } : {}) };
|
|
462
488
|
return next;
|
|
463
489
|
});
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
490
|
+
// An edge is not placed, so a direction on its line says which side of that
|
|
491
|
+
// node the line passes. The other placements say where a thing *is*, and
|
|
492
|
+
// have no reading for a line.
|
|
493
|
+
const passes = [];
|
|
494
|
+
for (const placement of tail.placements) {
|
|
495
|
+
const written = describePlacement(placement);
|
|
496
|
+
if (placement.kind !== 'offset' || placement.written !== undefined) {
|
|
497
|
+
throw new SourceError(`${subject}: "${written}" places a node, and an edge is not placed — it joins two things ` +
|
|
498
|
+
'that are. On an edge, above, below, left of and right of say which side of a node the ' +
|
|
499
|
+
'line passes', line);
|
|
500
|
+
}
|
|
501
|
+
if (placement.gap !== undefined) {
|
|
502
|
+
throw new SourceError(`${subject}: "${written}" gives a gap, and a gap is kept between nodes — the line passes ` +
|
|
503
|
+
'as close as it reads clearly. Drop the brackets', line);
|
|
504
|
+
}
|
|
505
|
+
passes.push(placement);
|
|
506
|
+
}
|
|
507
|
+
if (passes.length > 0 && between) {
|
|
508
|
+
throw new SourceError(`${subject}: "between" and "${describePlacement(passes[0])}" on one edge — an edge passing ` +
|
|
509
|
+
'between two things already has a side of each, so say one or the other', line);
|
|
467
510
|
}
|
|
468
511
|
refuseTextKey(tail.attrs, subject, 'after the arrow', line);
|
|
469
512
|
return {
|
|
@@ -474,6 +517,7 @@ function parseEdge(head, line) {
|
|
|
474
517
|
both: arrow.text === '<->',
|
|
475
518
|
...(textToken ? { text: textToken.text } : {}),
|
|
476
519
|
...(between ? { between } : {}),
|
|
520
|
+
...(passes.length > 0 ? { passes } : {}),
|
|
477
521
|
attrs: tail.attrs,
|
|
478
522
|
line,
|
|
479
523
|
};
|