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 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.9.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.
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.9.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
- One statement per line. No multi-line statements, no line continuations, no blocks.
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
- 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. Indentation is ignored entirely — a formatter may add it for readability, and stale indentation cannot change what a file means.
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
- * Nothing in the language spans lines: `//` runs to the end of one, a string
23
- * closes on one, and there are no blocks. That is the property those editor
24
- * formats need and the reason a `.reladraw` grammar is small in all of them.
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
- * Nothing in the language spans lines: `//` runs to the end of one, a string
23
- * closes on one, and there are no blocks. That is the property those editor
24
- * formats need and the reason a `.reladraw` grammar is small in all of them.
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
- /** Source text in, SVG out. The whole pipeline in one call. */
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 { parse } from './parser.js';
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
- /** Source text in, SVG out. The whole pipeline in one call. */
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
- return render(resolve(parse(source), options), options);
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
- /** Parse a whole source file. One statement per line; blanks and comments drop out. */
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
- /** Parse a whole source file. One statement per line; blanks and comments drop out. */
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
- const tokens = tokenizeLine(line, lineNumber);
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
- statements.push(parseStatement(tokens, lineNumber));
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reladraw",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "A diagram language where you say where things go.",
5
5
  "type": "module",
6
6
  "bin": {