reladraw 0.9.0 → 0.11.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 +43 -16
- package/dist/ast.d.ts +1 -1
- package/dist/ast.js +1 -1
- package/dist/grammar.d.ts +6 -3
- package/dist/grammar.js +6 -3
- package/dist/index.d.ts +5 -1
- package/dist/index.js +14 -3
- package/dist/lexer.d.ts +4 -1
- package/dist/lexer.js +4 -2
- package/dist/parser.d.ts +26 -1
- package/dist/parser.js +92 -3
- package/dist/render.js +431 -330
- package/dist/search.d.ts +69 -0
- package/dist/search.js +339 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -66,7 +66,7 @@ That gets you a copy of the skill at the time you run it, so you'll need to re-r
|
|
|
66
66
|
|
|
67
67
|
## Status
|
|
68
68
|
|
|
69
|
-
Version 0.
|
|
69
|
+
Version 0.11.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.
|
|
70
70
|
|
|
71
71
|
The language isn't stable yet, so expect the syntax to change.
|
|
72
72
|
|
package/SYNTAX.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Syntax reference — 0.
|
|
1
|
+
# Syntax reference — 0.11.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
|
|
|
@@ -6,9 +6,19 @@ The worked example is `examples/arch.reladraw`, transcribed from the reference r
|
|
|
6
6
|
|
|
7
7
|
## Shape of the file
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
A statement starts at the beginning of a line. A long one may continue onto the lines after it by indenting them:
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
```
|
|
12
|
+
node desktop1 "Desktop 1 / Linux"
|
|
13
|
+
above-left of hub gap: tight
|
|
14
|
+
badge: desktop style: synced
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
A line that starts with whitespace continues the statement above it, and a blank line or the next unindented line ends it. How much whitespace makes no difference — one space and a tab mean the same — so nothing can depend on indentation you cannot see. A bracket may stay open across lines, as in `line: (color: red,` with `pattern: dashed)` on the next. There are no blocks: indentation continues a statement, and never puts one thing inside another.
|
|
18
|
+
|
|
19
|
+
An indented line with no statement to continue — at the top of the file, or after a blank line or an unindented comment — is an error rather than a statement of its own, and so is an indented line starting with a statement keyword such as `node`. Both are almost always a stray indent, and both say to remove it.
|
|
20
|
+
|
|
21
|
+
Blank lines and `//` comments are ignored. A comment runs to the end of the line and may trail a statement, so `node a "Docker" // the one that matters` is fine. An indented comment line sits inside the statement it follows; an unindented one ends it.
|
|
12
22
|
|
|
13
23
|
A lone `/` is an ordinary character rather than the start of a comment, and `#` is ordinary too — it opens a hex color. Comments were spelled `#` in an earlier version and moved to `//` so that `fill: #14532d` could be written the way every other tool writes a color.
|
|
14
24
|
|
|
@@ -421,13 +431,15 @@ An edge with no text asks for nothing, since every gap is wide enough for an arr
|
|
|
421
431
|
edge desktop1.files <-> hub from: right to: top
|
|
422
432
|
```
|
|
423
433
|
|
|
424
|
-
That line leaves the right side of `desktop1.files` heading right, and arrives at the top of `hub` heading down. Naming a side is a statement about how the line should leave or arrive, so the edge is drawn as a curve that actually does. An edge naming neither side
|
|
434
|
+
That line leaves the right side of `desktop1.files` heading right, and arrives at the top of `hub` heading down. Naming a side is a statement about how the line should leave or arrive, so the edge is drawn as a curve that actually does. An edge naming neither side is a straight line from center to center, unless a box is in the way (see "Going round boxes"). Either end may be named on its own; the unnamed one aims at wherever its partner ended up.
|
|
425
435
|
|
|
426
436
|
You name a side and never a point on it. Alone on a side, an edge lands at its center. Sharing a side with other edges, the attachments space themselves apart, and which one goes where is derived from where the far ends actually sit — of two edges arriving at one top side, the one coming from further left arrives further left. Move a node and the order follows it. This is the same rule as nodes not overlapping: the tool separates things by default and reads the direction off the solved layout rather than asking you.
|
|
427
437
|
|
|
428
438
|
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.
|
|
429
439
|
|
|
430
|
-
####
|
|
440
|
+
#### Going round boxes
|
|
441
|
+
|
|
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.
|
|
431
443
|
|
|
432
444
|
```
|
|
433
445
|
node parser "Parser"
|
|
@@ -436,17 +448,17 @@ node renderer "Renderer" right of resolver
|
|
|
436
448
|
edge parser -> renderer "test edge" from: left to: right
|
|
437
449
|
```
|
|
438
450
|
|
|
439
|
-
That line leaves Parser heading left and has to arrive at Renderer from the right, so it must turn back on itself somewhere.
|
|
451
|
+
That line leaves Parser heading left and has to arrive at Renderer from the right, so it must turn back on itself somewhere. The shortest way is out of Parser's left, over the top of the row, and down into Renderer's right. Under the row would be exactly as long, and when two ways round tie, the line goes over the top, or round the right in a column. A node stepped down far enough leaves a gap between the two, and the line turns back through the gap, because that is shorter.
|
|
440
452
|
|
|
441
|
-
|
|
453
|
+
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.
|
|
442
454
|
|
|
443
|
-
|
|
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.
|
|
444
456
|
|
|
445
|
-
|
|
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.
|
|
446
458
|
|
|
447
|
-
|
|
459
|
+
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`.
|
|
448
460
|
|
|
449
|
-
|
|
461
|
+
To send a line round the other way, say which side of a node it passes: `below resolver`. See "Passing on one side of a node".
|
|
450
462
|
|
|
451
463
|
#### Several edges between the same two sides
|
|
452
464
|
|
|
@@ -602,7 +614,7 @@ edge web -> api line: (color: #d2904e, thickness: thick, crossing: arc)
|
|
|
602
614
|
| `color` | a color | as `line:` alone |
|
|
603
615
|
| `path` | `curved` (the default), `square`, `straight` | how the line joins the points it has to meet |
|
|
604
616
|
| `corners` | `sharp` (the default), `rounded` | how a square or straight line turns |
|
|
605
|
-
| `crossing` | `
|
|
617
|
+
| `crossing` | `gap` (the default), `none`, `arc`, `square` | how the line is drawn where it crosses an earlier one |
|
|
606
618
|
| `pattern` | `solid` (the default), `dashed`, `dotted`, `dash-dot` | |
|
|
607
619
|
| `thickness` | `thin`, `normal` (the default), `thick`, or a number of pixels | |
|
|
608
620
|
|
|
@@ -614,7 +626,7 @@ edge web -> api line: (color: #d2904e, thickness: thick, crossing: arc)
|
|
|
614
626
|
|
|
615
627
|
**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.
|
|
616
628
|
|
|
617
|
-
**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 `
|
|
629
|
+
**A crossing is drawn on the later line**: the edge written further down the file jumps the one written above it. `gap`, the default, is a short break in the later line, as though it passed underneath; `arc` is a small half-circle hop, `square` a three-sided jump, and `none` draws the two lines straight across each other. A line never jumps within reach of its own ends, where two lines meeting at a box are not passing each other.
|
|
618
630
|
|
|
619
631
|
**A pattern is measured in thicknesses**, so dashes keep their proportions on a thick line.
|
|
620
632
|
|
|
@@ -999,7 +1011,7 @@ Deliberate omissions. What they protect is that the renderer never *chooses* an
|
|
|
999
1011
|
- **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.
|
|
1000
1012
|
- **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.
|
|
1001
1013
|
- **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.
|
|
1002
|
-
- **Choosing a
|
|
1014
|
+
- **Choosing where a line goes when you said.** The tool finds its own way round a box in a line's way, but only as the shortest way through the open space, so where a line goes follows from where the boxes are. When you want a particular way instead, `between` and `below <node>` say it, and the tool keeps to what they say.
|
|
1003
1015
|
- **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.
|
|
1004
1016
|
|
|
1005
1017
|
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.
|
|
@@ -1050,12 +1062,27 @@ That one was found by testing the lexer, not by rendering — and it could not h
|
|
|
1050
1062
|
|
|
1051
1063
|
~~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.
|
|
1052
1064
|
|
|
1053
|
-
~~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 "
|
|
1065
|
+
~~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 "Going round boxes".
|
|
1054
1066
|
|
|
1055
1067
|
## Changelog
|
|
1056
1068
|
|
|
1057
1069
|
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.
|
|
1058
1070
|
|
|
1071
|
+
**0.11.0**
|
|
1072
|
+
|
|
1073
|
+
- A line never passes through a box. Every edge takes the shortest way between its ends that goes through none, and goes round whatever is in its way, including edges that name no side. This replaces the separate rules for lines going over a row, turning back and going round a node behind, which kept missing cases; a fan of three from one side into a column was the latest. See "Going round boxes".
|
|
1074
|
+
- Of two ways round that are equally short, the line goes over the top; otherwise the shorter way wins. It used to go over the top always.
|
|
1075
|
+
- A curve finishes straight at an arrowhead, so the head points square into its side. Most diagrams with a named side look slightly different.
|
|
1076
|
+
- Where two lines cross, the later one now breaks for a short gap, as though it passed underneath. It used to draw straight across; `crossing: none` keeps that. See "How the line is drawn".
|
|
1077
|
+
- Lines that turn down the same gap, such as two into one side of a box, take a lane each in the order that keeps them from crossing, instead of curving across each other.
|
|
1078
|
+
- A line's text no longer lands on another line's text: where two would overlap, the later one slides along its own line to the nearest clear spot.
|
|
1079
|
+
- **Breaking:** a line whose named side is flush against another box, with no way out, is refused with the reason. It used to be drawn through the box.
|
|
1080
|
+
|
|
1081
|
+
**0.10.0**
|
|
1082
|
+
|
|
1083
|
+
- A statement may continue onto the lines after it by indenting them. See "Shape of the file". An error in such a statement names the line the mistake is on.
|
|
1084
|
+
- **Breaking:** indentation used to be ignored, so an indented `node` line was a statement of its own. It now continues the statement above and is refused, with a message saying to remove the indentation.
|
|
1085
|
+
|
|
1059
1086
|
**0.9.0**
|
|
1060
1087
|
|
|
1061
1088
|
- Theme colors: `theme-page`, `theme-text`, `theme-muted`, `theme-fill`, `theme-border` and `theme-line` name the theme's own color for each part, and `theme-primary` and `theme-secondary`, with `-subtle` and `theme-on-` forms, name its two accents. Any attribute that takes a color takes them, and they follow the theme when it changes, so a diagram colored with them reads in light and dark themes alike. See "Theme colors".
|
|
@@ -1121,7 +1148,7 @@ Pre-1.0, so the minor number is where a breaking change goes. Every removal belo
|
|
|
1121
1148
|
|
|
1122
1149
|
Open questions the benchmark raised, recorded so a later session does not rediscover them.
|
|
1123
1150
|
|
|
1124
|
-
- 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
|
|
1151
|
+
- ~~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.
|
|
1125
1152
|
- 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.
|
|
1126
1153
|
- 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.
|
|
1127
1154
|
- 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
|
@@ -255,7 +255,7 @@ export declare const LINE_KEYS: readonly ["color", "path", "corners", "crossing"
|
|
|
255
255
|
export declare const LINE_VALUES: {
|
|
256
256
|
readonly path: readonly ["curved", "square", "straight"];
|
|
257
257
|
readonly corners: readonly ["sharp", "rounded"];
|
|
258
|
-
readonly crossing: readonly ["
|
|
258
|
+
readonly crossing: readonly ["gap", "none", "arc", "square"];
|
|
259
259
|
readonly pattern: readonly ["solid", "dashed", "dotted", "dash-dot"];
|
|
260
260
|
readonly thickness: readonly ["normal", "thin", "thick"];
|
|
261
261
|
};
|
package/dist/ast.js
CHANGED
|
@@ -236,7 +236,7 @@ export const LINE_KEYS = ['color', 'path', 'corners', 'crossing', 'pattern', 'th
|
|
|
236
236
|
export const LINE_VALUES = {
|
|
237
237
|
path: ['curved', 'square', 'straight'],
|
|
238
238
|
corners: ['sharp', 'rounded'],
|
|
239
|
-
crossing: ['
|
|
239
|
+
crossing: ['gap', 'none', 'arc', 'square'],
|
|
240
240
|
pattern: ['solid', 'dashed', 'dotted', 'dash-dot'],
|
|
241
241
|
thickness: ['normal', 'thin', 'thick'],
|
|
242
242
|
};
|
package/dist/grammar.d.ts
CHANGED
|
@@ -19,9 +19,12 @@
|
|
|
19
19
|
* The scanner is the JavaScript consumer of the same spec, and the playground is
|
|
20
20
|
* its only caller.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* No token spans lines: `//` runs to the end of one, a string closes on one,
|
|
23
|
+
* and there are no blocks. A statement may continue onto indented lines, but
|
|
24
|
+
* each of those is colored by the same rules, and a word that is a keyword at
|
|
25
|
+
* the start of one is refused by the parser anyway. That is the property those
|
|
26
|
+
* editor formats need and the reason a `.reladraw` grammar is small in all of
|
|
27
|
+
* them.
|
|
25
28
|
*/
|
|
26
29
|
/** What a span of source is, for coloring. */
|
|
27
30
|
export type TokenKind =
|
package/dist/grammar.js
CHANGED
|
@@ -19,9 +19,12 @@
|
|
|
19
19
|
* The scanner is the JavaScript consumer of the same spec, and the playground is
|
|
20
20
|
* its only caller.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* No token spans lines: `//` runs to the end of one, a string closes on one,
|
|
23
|
+
* and there are no blocks. A statement may continue onto indented lines, but
|
|
24
|
+
* each of those is colored by the same rules, and a word that is a keyword at
|
|
25
|
+
* the start of one is refused by the parser anyway. That is the property those
|
|
26
|
+
* editor formats need and the reason a `.reladraw` grammar is small in all of
|
|
27
|
+
* them.
|
|
25
28
|
*/
|
|
26
29
|
import { DIRECTIONS, POSITIONS, SIDES, PASSAGE_AXES } from './ast.js';
|
|
27
30
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -10,5 +10,9 @@ export { render, type RenderOptions } from './render.js';
|
|
|
10
10
|
export { DARK_THEME, DEFAULT_THEME, THEMES, THEME_NAMES, type Theme } from './themes.js';
|
|
11
11
|
import { type RenderOptions } from './render.js';
|
|
12
12
|
import { type ResolveOptions } from './resolve.js';
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Source text in, SVG out. The whole pipeline in one call. An error in a
|
|
15
|
+
* statement written over several lines names the line at fault, not the one
|
|
16
|
+
* the statement starts on; see `blame`.
|
|
17
|
+
*/
|
|
14
18
|
export declare function compile(source: string, options?: ResolveOptions & RenderOptions): string;
|
package/dist/index.js
CHANGED
|
@@ -8,10 +8,21 @@ export { parse } from './parser.js';
|
|
|
8
8
|
export { resolve } from './resolve.js';
|
|
9
9
|
export { render } from './render.js';
|
|
10
10
|
export { DARK_THEME, DEFAULT_THEME, THEMES, THEME_NAMES } from './themes.js';
|
|
11
|
-
import {
|
|
11
|
+
import { SourceError } from './errors.js';
|
|
12
|
+
import { blame, parse } from './parser.js';
|
|
12
13
|
import { render } from './render.js';
|
|
13
14
|
import { resolve } from './resolve.js';
|
|
14
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Source text in, SVG out. The whole pipeline in one call. An error in a
|
|
17
|
+
* statement written over several lines names the line at fault, not the one
|
|
18
|
+
* the statement starts on; see `blame`.
|
|
19
|
+
*/
|
|
15
20
|
export function compile(source, options = {}) {
|
|
16
|
-
|
|
21
|
+
const run = (text) => render(resolve(parse(text), options), options);
|
|
22
|
+
try {
|
|
23
|
+
return run(source);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
throw error instanceof SourceError ? blame(source, error, run) : error;
|
|
27
|
+
}
|
|
17
28
|
}
|
package/dist/lexer.d.ts
CHANGED
|
@@ -18,8 +18,11 @@ export interface Token {
|
|
|
18
18
|
* opening bracket counts only where a token starts, and a closing one only
|
|
19
19
|
* while a group is open, so an unquoted `rgb(20,20,20)` stays a single token.
|
|
20
20
|
*
|
|
21
|
+
* `depth` is how many brackets are still open from the lines before, when a
|
|
22
|
+
* statement continues onto this one.
|
|
23
|
+
*
|
|
21
24
|
* Returns an empty array for a blank or comment-only line.
|
|
22
25
|
*/
|
|
23
|
-
export declare function tokenizeLine(line: string, lineNumber: number): Token[];
|
|
26
|
+
export declare function tokenizeLine(line: string, lineNumber: number, depth?: number): Token[];
|
|
24
27
|
/** A bare token ending in `:` opens the attribute section of a statement. */
|
|
25
28
|
export declare function isAttrKey(token: Token): boolean;
|
package/dist/lexer.js
CHANGED
|
@@ -14,12 +14,14 @@ import { SourceError } from './errors.js';
|
|
|
14
14
|
* opening bracket counts only where a token starts, and a closing one only
|
|
15
15
|
* while a group is open, so an unquoted `rgb(20,20,20)` stays a single token.
|
|
16
16
|
*
|
|
17
|
+
* `depth` is how many brackets are still open from the lines before, when a
|
|
18
|
+
* statement continues onto this one.
|
|
19
|
+
*
|
|
17
20
|
* Returns an empty array for a blank or comment-only line.
|
|
18
21
|
*/
|
|
19
|
-
export function tokenizeLine(line, lineNumber) {
|
|
22
|
+
export function tokenizeLine(line, lineNumber, depth = 0) {
|
|
20
23
|
const tokens = [];
|
|
21
24
|
let i = 0;
|
|
22
|
-
let depth = 0;
|
|
23
25
|
while (i < line.length) {
|
|
24
26
|
const ch = line[i];
|
|
25
27
|
if (ch === ' ' || ch === '\t') {
|
package/dist/parser.d.ts
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
1
|
import type { Document } from './ast.js';
|
|
2
|
-
|
|
2
|
+
import { SourceError } from './errors.js';
|
|
3
|
+
/**
|
|
4
|
+
* Parse a whole source file. A statement starts on an unindented line and runs
|
|
5
|
+
* on through any line that starts with whitespace; a blank line or the next
|
|
6
|
+
* unindented one ends it. How much whitespace never matters, only whether
|
|
7
|
+
* there is any, so two lines that look alike cannot mean different things.
|
|
8
|
+
*
|
|
9
|
+
* An indented comment stays inside the statement and an unindented one ends
|
|
10
|
+
* it. An indented line with nothing open above it is refused rather than read
|
|
11
|
+
* as a statement of its own: it is a stray indent or a deleted first line, and
|
|
12
|
+
* both are worth hearing about.
|
|
13
|
+
*/
|
|
3
14
|
export declare function parse(source: string): Document;
|
|
15
|
+
/**
|
|
16
|
+
* Find the line at fault for an error reported on the first line of a
|
|
17
|
+
* continued statement. Everything after the parser reports a statement by the
|
|
18
|
+
* line it starts on, which for a continued one is often not where the mistake
|
|
19
|
+
* is, so this asks which line brings the error in: `run` is tried on the
|
|
20
|
+
* source with the statement cut after each of its lines in turn, and the first
|
|
21
|
+
* cut that fails with the same message names the line. A cut that fails
|
|
22
|
+
* differently — a bracket not yet closed, a placement not yet written — is not
|
|
23
|
+
* the culprit and is passed over. Lines keep their numbers, because what is cut
|
|
24
|
+
* is blanked rather than removed.
|
|
25
|
+
*
|
|
26
|
+
* This costs a few extra runs, and only when there is an error to report.
|
|
27
|
+
*/
|
|
28
|
+
export declare function blame(source: string, error: SourceError, run: (source: string) => unknown): SourceError;
|
package/dist/parser.js
CHANGED
|
@@ -1,19 +1,108 @@
|
|
|
1
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
|
+
import { STATEMENT_KEYWORDS } from './grammar.js';
|
|
3
4
|
import { isAttrKey, tokenizeLine } from './lexer.js';
|
|
4
5
|
import { THEME_COLORS, THEME_NAMES, THEMES } from './themes.js';
|
|
5
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Parse a whole source file. A statement starts on an unindented line and runs
|
|
8
|
+
* on through any line that starts with whitespace; a blank line or the next
|
|
9
|
+
* unindented one ends it. How much whitespace never matters, only whether
|
|
10
|
+
* there is any, so two lines that look alike cannot mean different things.
|
|
11
|
+
*
|
|
12
|
+
* An indented comment stays inside the statement and an unindented one ends
|
|
13
|
+
* it. An indented line with nothing open above it is refused rather than read
|
|
14
|
+
* as a statement of its own: it is a stray indent or a deleted first line, and
|
|
15
|
+
* both are worth hearing about.
|
|
16
|
+
*/
|
|
6
17
|
export function parse(source) {
|
|
7
18
|
const statements = [];
|
|
19
|
+
/** The statement being gathered, and the line it starts on. */
|
|
20
|
+
let open = null;
|
|
21
|
+
// Brackets may close on a later line, and the lexer only reads `)` as a
|
|
22
|
+
// bracket while one is open, so the depth is carried from line to line.
|
|
23
|
+
let depth = 0;
|
|
24
|
+
const close = () => {
|
|
25
|
+
if (open)
|
|
26
|
+
statements.push(parseStatement(open.tokens, open.line));
|
|
27
|
+
open = null;
|
|
28
|
+
depth = 0;
|
|
29
|
+
};
|
|
8
30
|
source.split(/\r?\n/).forEach((line, index) => {
|
|
9
31
|
const lineNumber = index + 1;
|
|
10
|
-
|
|
32
|
+
if (line.trim() === '') {
|
|
33
|
+
close();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const indented = line[0] === ' ' || line[0] === '\t';
|
|
37
|
+
if (!indented)
|
|
38
|
+
close();
|
|
39
|
+
const tokens = tokenizeLine(line, lineNumber, depth);
|
|
11
40
|
if (tokens.length === 0)
|
|
12
41
|
return;
|
|
13
|
-
|
|
42
|
+
if (!indented) {
|
|
43
|
+
open = { tokens, line: lineNumber };
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const first = tokens[0];
|
|
47
|
+
if (!open) {
|
|
48
|
+
throw new SourceError('this line is indented, which continues the statement above, but there is none to ' +
|
|
49
|
+
'continue — remove the indentation to start a new statement', lineNumber);
|
|
50
|
+
}
|
|
51
|
+
if (!first.quoted && STATEMENT_KEYWORDS.includes(first.text)) {
|
|
52
|
+
throw new SourceError(`\`${first.text}\` cannot continue the statement above — an indented line continues ` +
|
|
53
|
+
`the one before it, so remove the indentation to start a new statement`, lineNumber);
|
|
54
|
+
}
|
|
55
|
+
open.tokens.push(...tokens);
|
|
56
|
+
}
|
|
57
|
+
for (const token of tokens) {
|
|
58
|
+
if (token.quoted)
|
|
59
|
+
continue;
|
|
60
|
+
if (token.text === '(')
|
|
61
|
+
depth += 1;
|
|
62
|
+
else if (token.text === ')')
|
|
63
|
+
depth -= 1;
|
|
64
|
+
}
|
|
14
65
|
});
|
|
66
|
+
close();
|
|
15
67
|
return { statements };
|
|
16
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Find the line at fault for an error reported on the first line of a
|
|
71
|
+
* continued statement. Everything after the parser reports a statement by the
|
|
72
|
+
* line it starts on, which for a continued one is often not where the mistake
|
|
73
|
+
* is, so this asks which line brings the error in: `run` is tried on the
|
|
74
|
+
* source with the statement cut after each of its lines in turn, and the first
|
|
75
|
+
* cut that fails with the same message names the line. A cut that fails
|
|
76
|
+
* differently — a bracket not yet closed, a placement not yet written — is not
|
|
77
|
+
* the culprit and is passed over. Lines keep their numbers, because what is cut
|
|
78
|
+
* is blanked rather than removed.
|
|
79
|
+
*
|
|
80
|
+
* This costs a few extra runs, and only when there is an error to report.
|
|
81
|
+
*/
|
|
82
|
+
export function blame(source, error, run) {
|
|
83
|
+
const lines = source.split(/\r?\n/);
|
|
84
|
+
const start = error.line - 1;
|
|
85
|
+
const first = lines[start];
|
|
86
|
+
if (first === undefined || first.trim() === '' || first[0] === ' ' || first[0] === '\t')
|
|
87
|
+
return error;
|
|
88
|
+
let end = start;
|
|
89
|
+
while (end + 1 < lines.length && lines[end + 1].trim() !== '' && /^[ \t]/.test(lines[end + 1]))
|
|
90
|
+
end += 1;
|
|
91
|
+
if (end === start)
|
|
92
|
+
return error;
|
|
93
|
+
for (let last = start; last < end; last++) {
|
|
94
|
+
const cut = lines.map((line, index) => (index > last && index <= end ? '' : line)).join('\n');
|
|
95
|
+
try {
|
|
96
|
+
run(cut);
|
|
97
|
+
}
|
|
98
|
+
catch (partial) {
|
|
99
|
+
if (partial instanceof SourceError && partial.message === error.message) {
|
|
100
|
+
return last === start ? error : new SourceError(error.message, last + 1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return new SourceError(error.message, end + 1);
|
|
105
|
+
}
|
|
17
106
|
function parseStatement(tokens, line) {
|
|
18
107
|
const keyword = tokens[0];
|
|
19
108
|
if (!keyword || keyword.quoted) {
|