reladraw 0.9.0 → 0.10.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 +18 -3
- 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/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.10.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.10.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
|
|
|
@@ -1056,6 +1066,11 @@ That one was found by testing the lexer, not by rendering — and it could not h
|
|
|
1056
1066
|
|
|
1057
1067
|
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
1068
|
|
|
1069
|
+
**0.10.0**
|
|
1070
|
+
|
|
1071
|
+
- 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.
|
|
1072
|
+
- **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.
|
|
1073
|
+
|
|
1059
1074
|
**0.9.0**
|
|
1060
1075
|
|
|
1061
1076
|
- 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".
|
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) {
|