@dxos/diagram 0.11.1 → 0.13.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/dist/lib/chunk-parse.mjs +9396 -0
- package/dist/lib/chunk-parse.mjs.map +1 -0
- package/dist/lib/extension.mjs +204 -0
- package/dist/lib/extension.mjs.map +1 -0
- package/dist/lib/index.mjs +497 -2846
- package/dist/lib/index.mjs.map +1 -1
- package/dist/types/src/aesthetics.d.ts +12 -0
- package/dist/types/src/aesthetics.d.ts.map +1 -0
- package/dist/types/src/aesthetics.test.d.ts +2 -0
- package/dist/types/src/aesthetics.test.d.ts.map +1 -0
- package/dist/types/src/appeal.d.ts +55 -0
- package/dist/types/src/appeal.d.ts.map +1 -0
- package/dist/types/src/appeal.test.d.ts +2 -0
- package/dist/types/src/appeal.test.d.ts.map +1 -0
- package/dist/types/src/architecture.d.ts +86 -0
- package/dist/types/src/architecture.d.ts.map +1 -0
- package/dist/types/src/architecture.test.d.ts +2 -0
- package/dist/types/src/architecture.test.d.ts.map +1 -0
- package/dist/types/src/compact.d.ts +39 -0
- package/dist/types/src/compact.d.ts.map +1 -0
- package/dist/types/src/compact.test.d.ts +2 -0
- package/dist/types/src/compact.test.d.ts.map +1 -0
- package/dist/types/src/diagnostics.d.ts +70 -3
- package/dist/types/src/diagnostics.d.ts.map +1 -1
- package/dist/types/src/dsl/cli.d.ts +2 -0
- package/dist/types/src/dsl/cli.d.ts.map +1 -0
- package/dist/types/src/dsl/compile.d.ts +12 -0
- package/dist/types/src/dsl/compile.d.ts.map +1 -0
- package/dist/types/src/dsl/convert.d.ts +45 -0
- package/dist/types/src/dsl/convert.d.ts.map +1 -0
- package/dist/types/src/dsl/convert.test.d.ts +2 -0
- package/dist/types/src/dsl/convert.test.d.ts.map +1 -0
- package/dist/types/src/dsl/dsl.test.d.ts +2 -0
- package/dist/types/src/dsl/dsl.test.d.ts.map +1 -0
- package/dist/types/src/dsl/gen/diagram.d.ts +3 -0
- package/dist/types/src/dsl/gen/diagram.d.ts.map +1 -0
- package/dist/types/src/dsl/index.d.ts +7 -0
- package/dist/types/src/dsl/index.d.ts.map +1 -0
- package/dist/types/src/dsl/parse.d.ts +49 -0
- package/dist/types/src/dsl/parse.d.ts.map +1 -0
- package/dist/types/src/dsl/print.d.ts +19 -0
- package/dist/types/src/dsl/print.d.ts.map +1 -0
- package/dist/types/src/dsl/semantic.test.d.ts +2 -0
- package/dist/types/src/dsl/semantic.test.d.ts.map +1 -0
- package/dist/types/src/dsl/vocabulary.d.ts +61 -0
- package/dist/types/src/dsl/vocabulary.d.ts.map +1 -0
- package/dist/types/src/dx-svg.d.ts +12 -0
- package/dist/types/src/dx-svg.d.ts.map +1 -0
- package/dist/types/src/dx-svg.test.d.ts +2 -0
- package/dist/types/src/dx-svg.test.d.ts.map +1 -0
- package/dist/types/src/extension/complete.d.ts +3 -0
- package/dist/types/src/extension/complete.d.ts.map +1 -0
- package/dist/types/src/extension/extension.test.d.ts +2 -0
- package/dist/types/src/extension/extension.test.d.ts.map +1 -0
- package/dist/types/src/extension/highlight.d.ts +14 -0
- package/dist/types/src/extension/highlight.d.ts.map +1 -0
- package/dist/types/src/extension/index.d.ts +6 -0
- package/dist/types/src/extension/index.d.ts.map +1 -0
- package/dist/types/src/extension/language.d.ts +13 -0
- package/dist/types/src/extension/language.d.ts.map +1 -0
- package/dist/types/src/extension/lint.d.ts +21 -0
- package/dist/types/src/extension/lint.d.ts.map +1 -0
- package/dist/types/src/extension/syntax.d.ts +3 -0
- package/dist/types/src/extension/syntax.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +14 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/mermaid-engine.d.ts +40 -1
- package/dist/types/src/mermaid-engine.d.ts.map +1 -1
- package/dist/types/src/mermaid.d.ts +3 -3
- package/dist/types/src/mermaid.d.ts.map +1 -1
- package/dist/types/src/nudge.d.ts +30 -0
- package/dist/types/src/nudge.d.ts.map +1 -0
- package/dist/types/src/nudge.test.d.ts +2 -0
- package/dist/types/src/nudge.test.d.ts.map +1 -0
- package/dist/types/src/objective.d.ts +4 -0
- package/dist/types/src/objective.d.ts.map +1 -1
- package/dist/types/src/ortho-router.d.ts +13 -1
- package/dist/types/src/ortho-router.d.ts.map +1 -1
- package/dist/types/src/ports.d.ts +40 -0
- package/dist/types/src/ports.d.ts.map +1 -0
- package/dist/types/src/ports.test.d.ts +2 -0
- package/dist/types/src/ports.test.d.ts.map +1 -0
- package/dist/types/src/relations.test.d.ts +2 -0
- package/dist/types/src/relations.test.d.ts.map +1 -0
- package/dist/types/src/rules.d.ts +30 -0
- package/dist/types/src/rules.d.ts.map +1 -0
- package/dist/types/src/scene.d.ts +99 -65
- package/dist/types/src/scene.d.ts.map +1 -1
- package/dist/types/src/score.d.ts +54 -0
- package/dist/types/src/score.d.ts.map +1 -0
- package/dist/types/src/score.test.d.ts +2 -0
- package/dist/types/src/score.test.d.ts.map +1 -0
- package/dist/types/src/semantic-engine.d.ts +41 -0
- package/dist/types/src/semantic-engine.d.ts.map +1 -0
- package/dist/types/src/semantic-engine.test.d.ts +2 -0
- package/dist/types/src/semantic-engine.test.d.ts.map +1 -0
- package/dist/types/src/semantic-place.d.ts +81 -0
- package/dist/types/src/semantic-place.d.ts.map +1 -0
- package/dist/types/src/semantic-route.d.ts +133 -0
- package/dist/types/src/semantic-route.d.ts.map +1 -0
- package/dist/types/src/semantic.d.ts +164 -0
- package/dist/types/src/semantic.d.ts.map +1 -0
- package/dist/types/src/uml-grid.d.ts.map +1 -1
- package/dist/types/src/uml.d.ts +3 -5
- package/dist/types/src/uml.d.ts.map +1 -1
- package/dist/types/src/view.d.ts +53 -0
- package/dist/types/src/view.d.ts.map +1 -0
- package/dist/types/src/view.test.d.ts +2 -0
- package/dist/types/src/view.test.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +23 -5
- package/rules/DIAGRAM.mdl +388 -0
- package/src/aesthetics.test.ts +89 -0
- package/src/aesthetics.ts +148 -0
- package/src/appeal.test.ts +144 -0
- package/src/appeal.ts +586 -0
- package/src/architecture.test.ts +94 -0
- package/src/architecture.ts +215 -0
- package/src/compact.test.ts +67 -0
- package/src/compact.ts +219 -0
- package/src/diagnostics.test.ts +207 -1
- package/src/diagnostics.ts +481 -37
- package/src/dsl/cli.ts +46 -0
- package/src/dsl/compile.ts +28 -0
- package/src/dsl/convert.test.ts +51 -0
- package/src/dsl/convert.ts +98 -0
- package/src/dsl/diagram.grammar +195 -0
- package/src/dsl/dsl.test.ts +286 -0
- package/src/dsl/gen/diagram.ts +22 -0
- package/src/dsl/index.ts +44 -0
- package/src/dsl/parse.ts +1054 -0
- package/src/dsl/print.ts +198 -0
- package/src/dsl/semantic.test.ts +200 -0
- package/src/dsl/vocabulary.ts +150 -0
- package/src/dx-svg.test.ts +44 -0
- package/src/dx-svg.ts +77 -0
- package/src/extension/complete.ts +105 -0
- package/src/extension/extension.test.ts +135 -0
- package/src/extension/highlight.ts +23 -0
- package/src/extension/index.ts +9 -0
- package/src/extension/language.ts +31 -0
- package/src/extension/lint.ts +60 -0
- package/src/extension/syntax.ts +62 -0
- package/src/index.ts +14 -0
- package/src/mermaid-engine.test.ts +6 -6
- package/src/mermaid-engine.ts +569 -119
- package/src/mermaid.test.ts +17 -1
- package/src/mermaid.ts +30 -18
- package/src/nudge.test.ts +155 -0
- package/src/nudge.ts +385 -0
- package/src/objective.test.ts +2 -0
- package/src/objective.ts +17 -1
- package/src/ortho-router.ts +240 -80
- package/src/ports.test.ts +157 -0
- package/src/ports.ts +186 -0
- package/src/relations.test.ts +98 -0
- package/src/rules.ts +117 -0
- package/src/scene.ts +64 -3
- package/src/score.test.ts +64 -0
- package/src/score.ts +130 -0
- package/src/semantic-engine.test.ts +635 -0
- package/src/semantic-engine.ts +888 -0
- package/src/semantic-place.ts +1508 -0
- package/src/semantic-route.ts +1136 -0
- package/src/semantic.ts +278 -0
- package/src/typings.d.ts +8 -0
- package/src/uml-grid.ts +6 -1
- package/src/uml.test.ts +5 -1
- package/src/uml.ts +24 -11
- package/src/view.test.ts +139 -0
- package/src/view.ts +376 -0
package/src/dsl/cli.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
//
|
|
6
|
+
// Converts a diagram source into DSL text: `moon run diagram:convert -- <file>`, or stdin.
|
|
7
|
+
// A file rather than a library call because the point of the DSL is that a scene becomes something
|
|
8
|
+
// you check in, and the first step of doing that is getting the text out.
|
|
9
|
+
//
|
|
10
|
+
|
|
11
|
+
import * as Effect from 'effect/Effect';
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
|
|
14
|
+
import * as EffectEx from '@dxos/effect/EffectEx';
|
|
15
|
+
|
|
16
|
+
import { SOURCES, convert } from './convert.ts';
|
|
17
|
+
|
|
18
|
+
const USAGE = `Usage: convert [--source <${SOURCES.map(({ id }) => id).join('|')}>] [<file>]
|
|
19
|
+
|
|
20
|
+
Reads a diagram source (stdin when no file is given) and writes the equivalent DSL.
|
|
21
|
+
The output is the laid-out form: the dialect places everything, and the text is the result.
|
|
22
|
+
|
|
23
|
+
Sources:
|
|
24
|
+
${SOURCES.map(({ id, description }) => ` ${id.padEnd(16)} ${description}`).join('\n')}`;
|
|
25
|
+
|
|
26
|
+
const main = Effect.gen(function* () {
|
|
27
|
+
const args = process.argv.slice(2);
|
|
28
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
29
|
+
console.log(USAGE);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const at = args.indexOf('--source');
|
|
34
|
+
const source = at === -1 ? undefined : args[at + 1];
|
|
35
|
+
// `at + 1` is only a value index when the flag is present; `-1 + 1 === 0` would otherwise eat
|
|
36
|
+
// the first positional argument and silently read stdin instead of the named file.
|
|
37
|
+
const [file] = args.filter((arg, index) => !arg.startsWith('-') && !(at !== -1 && index === at + 1));
|
|
38
|
+
const text = readFileSync(file ?? 0, 'utf8');
|
|
39
|
+
|
|
40
|
+
process.stdout.write(yield* convert(text, { source }));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
EffectEx.runPromise(main).catch((error: unknown) => {
|
|
44
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Effect from 'effect/Effect';
|
|
6
|
+
|
|
7
|
+
import * as SemanticEngine from '../semantic-engine.ts';
|
|
8
|
+
import { type ParseResult, read, withLayout } from './parse.ts';
|
|
9
|
+
|
|
10
|
+
/** Options for {@link compile}. */
|
|
11
|
+
export type CompileOptions = Pick<SemanticEngine.SolveOptions, 'emitCandidate'>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Reads a document and lays out its semantic statements with the full search, which also tries the
|
|
15
|
+
* mermaid engine's placements when nothing was hinted. Asynchronous only because that engine's ELK
|
|
16
|
+
* is; a document of scene statements alone reads exactly as {@link parse} reads it.
|
|
17
|
+
*/
|
|
18
|
+
export const compile: (text: string, options?: CompileOptions) => Effect.Effect<ParseResult> = Effect.fn('Dsl.compile')(
|
|
19
|
+
function* (text: string, options: CompileOptions = {}) {
|
|
20
|
+
const reading = read(text);
|
|
21
|
+
const { diagram } = reading;
|
|
22
|
+
if (!diagram) {
|
|
23
|
+
return withLayout(reading);
|
|
24
|
+
}
|
|
25
|
+
const solution = yield* Effect.promise(() => SemanticEngine.compile(diagram, options));
|
|
26
|
+
return withLayout(reading, solution);
|
|
27
|
+
},
|
|
28
|
+
);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import * as EffectEx from '@dxos/effect/EffectEx';
|
|
8
|
+
|
|
9
|
+
import { BASIC, CLASS_DIAGRAM } from '../testing.ts';
|
|
10
|
+
import { UndetectedSourceError, UnknownSourceError, convert, detect } from './convert.ts';
|
|
11
|
+
import { parse } from './parse.ts';
|
|
12
|
+
import { printCommands } from './print.ts';
|
|
13
|
+
|
|
14
|
+
describe('convert', () => {
|
|
15
|
+
test('detects a flowchart and a classDiagram', ({ expect }) => {
|
|
16
|
+
expect(detect(BASIC.trim())?.id).toEqual('mermaid');
|
|
17
|
+
expect(detect(CLASS_DIAGRAM.trim())?.id).toEqual('uml');
|
|
18
|
+
expect(detect('not a diagram')).toBeUndefined();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
for (const [name, source] of Object.entries({ BASIC, CLASS_DIAGRAM })) {
|
|
22
|
+
test(`${name} converts to DSL that parses back cleanly`, async ({ expect }) => {
|
|
23
|
+
const text = await EffectEx.runPromise(convert(source.trim()));
|
|
24
|
+
const { commands, problems } = parse(text);
|
|
25
|
+
expect(problems).toEqual([]);
|
|
26
|
+
expect(commands.length).toBeGreaterThan(0);
|
|
27
|
+
// Output is canonical, so converting and reprinting is a fixed point.
|
|
28
|
+
expect(printCommands(commands)).toEqual(text);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
test('the emitted form is post-layout — every object carries an origin', async ({ expect }) => {
|
|
33
|
+
const { commands } = parse(await EffectEx.runPromise(convert(BASIC.trim())));
|
|
34
|
+
const upserts = commands.flatMap((command) => (command.op === 'upsert-object' ? [command.object] : []));
|
|
35
|
+
expect(upserts.length).toBeGreaterThan(0);
|
|
36
|
+
expect(upserts.every(({ origin }) => origin !== undefined)).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('an explicit source overrides detection', async ({ expect }) => {
|
|
40
|
+
const elk = await EffectEx.runPromise(convert(BASIC.trim()));
|
|
41
|
+
const layered = await EffectEx.runPromise(convert(BASIC.trim(), { source: 'mermaid-layered' }));
|
|
42
|
+
expect(parse(layered).problems).toEqual([]);
|
|
43
|
+
// Same graph, different placement engine, so the text must differ.
|
|
44
|
+
expect(layered).not.toEqual(elk);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('an unusable input fails with a typed error rather than throwing', async ({ expect }) => {
|
|
48
|
+
await expect(EffectEx.runPromise(convert('not a diagram'))).rejects.toThrow(UndetectedSourceError);
|
|
49
|
+
await expect(EffectEx.runPromise(convert(BASIC.trim(), { source: 'd2' }))).rejects.toThrow(UnknownSourceError);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
//
|
|
6
|
+
// Text in another diagram language, out as ours. Every source language already compiles to
|
|
7
|
+
// `Scene.Command[]`, and the DSL already prints those, so a converter is the two halves joined —
|
|
8
|
+
// which is why adding a dialect costs nothing here.
|
|
9
|
+
//
|
|
10
|
+
// The output is the POST-layout form, and there is no other option: the DSL has no unpositioned
|
|
11
|
+
// node to emit. That is the point of converting — you run mermaid once to get a layout, then keep
|
|
12
|
+
// the result as something you can hand-tune, diff and review. See `docs/DSL.md`.
|
|
13
|
+
//
|
|
14
|
+
|
|
15
|
+
import * as Data from 'effect/Data';
|
|
16
|
+
import * as Effect from 'effect/Effect';
|
|
17
|
+
|
|
18
|
+
import * as MermaidEngine from '../mermaid-engine.ts';
|
|
19
|
+
import * as Mermaid from '../mermaid.ts';
|
|
20
|
+
import type * as Scene from '../scene.ts';
|
|
21
|
+
import * as Uml from '../uml.ts';
|
|
22
|
+
import { printCommands } from './print.ts';
|
|
23
|
+
|
|
24
|
+
/** A source language that can be turned into DSL text. */
|
|
25
|
+
export type Source = {
|
|
26
|
+
id: string;
|
|
27
|
+
description: string;
|
|
28
|
+
/** True when a source string looks like this language, for `convert` to pick without being told. */
|
|
29
|
+
detect: (source: string) => boolean;
|
|
30
|
+
compile: (source: string) => Effect.Effect<readonly Scene.Command[]>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Mermaid `classDiagram`, laid out synchronously by the UML engine. */
|
|
34
|
+
export const umlSource: Source = {
|
|
35
|
+
id: 'uml',
|
|
36
|
+
description: 'Mermaid classDiagram.',
|
|
37
|
+
detect: Uml.isClassDiagram,
|
|
38
|
+
compile: (source) => Effect.sync(() => Uml.compile(source)),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Mermaid `flowchart`, laid out by the ELK-backed engine. ELK is promise-based, so the platform
|
|
43
|
+
* boundary sits here rather than leaking a Promise into the rest of the pipeline.
|
|
44
|
+
*/
|
|
45
|
+
export const mermaidSource: Source = {
|
|
46
|
+
id: 'mermaid',
|
|
47
|
+
description: 'Mermaid flowchart, including the `%% ref` and UML edge-token extensions.',
|
|
48
|
+
detect: (source) => /^\s*(flowchart|graph)\s+/m.test(source),
|
|
49
|
+
compile: (source) => Effect.promise(() => MermaidEngine.compile(source)),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** Mermaid flowchart without the ELK pass — the simple layered placement in `mermaid.ts`. */
|
|
53
|
+
export const mermaidLayeredSource: Source = {
|
|
54
|
+
id: 'mermaid-layered',
|
|
55
|
+
description: "Mermaid flowchart with the dialect's own layered placement rather than ELK.",
|
|
56
|
+
detect: () => false,
|
|
57
|
+
compile: (source) => Effect.sync(() => Mermaid.compile(source)),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const SOURCES: readonly Source[] = [umlSource, mermaidSource, mermaidLayeredSource];
|
|
61
|
+
|
|
62
|
+
/** The language a source string is written in, or undefined when nothing claims it. */
|
|
63
|
+
export const detect = (source: string): Source | undefined => SOURCES.find(({ detect }) => detect(source));
|
|
64
|
+
|
|
65
|
+
const known = () => SOURCES.map(({ id }) => id).join(', ');
|
|
66
|
+
|
|
67
|
+
/** Tagged so the failure channel stays discriminated rather than collapsing into bare `Error`. */
|
|
68
|
+
export class UnknownSourceError extends Data.TaggedError('DiagramUnknownSource')<{ id: string }> {
|
|
69
|
+
override get message(): string {
|
|
70
|
+
return `Unknown source language "${this.id}"; known: ${known()}.`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export class UndetectedSourceError extends Data.TaggedError('DiagramUndetectedSource')<{}> {
|
|
75
|
+
override get message(): string {
|
|
76
|
+
return `Could not tell what language this is; pass one of: ${known()}.`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type ConvertOptions = {
|
|
81
|
+
/** Force a language instead of detecting one. */
|
|
82
|
+
source?: string;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** Compiles `text` through its dialect and prints the result as DSL text. */
|
|
86
|
+
export const convert = (
|
|
87
|
+
text: string,
|
|
88
|
+
{ source }: ConvertOptions = {},
|
|
89
|
+
): Effect.Effect<string, UnknownSourceError | UndetectedSourceError> =>
|
|
90
|
+
Effect.gen(function* () {
|
|
91
|
+
const language = source === undefined ? detect(text) : SOURCES.find(({ id }) => id === source);
|
|
92
|
+
if (!language) {
|
|
93
|
+
return yield* Effect.fail(
|
|
94
|
+
source === undefined ? new UndetectedSourceError() : new UnknownSourceError({ id: source }),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return printCommands(yield* language.compile(text));
|
|
98
|
+
});
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
// Lezer grammar for the diagram DSL — the text form of `src/scene.ts`.
|
|
5
|
+
// Generate: lezer-generator --typeScript src/dsl/diagram.grammar -o src/dsl/gen/diagram.ts
|
|
6
|
+
//
|
|
7
|
+
// Two layers share one grammar. The scene statements (`object`, `elements`, `move`, `remove`) place
|
|
8
|
+
// every shape at a coordinate; the semantic statements (`diagram`, `group`, `node`, `edge`) state
|
|
9
|
+
// intent and leave placement and routing to `semantic-engine.ts`. A document may mix them.
|
|
10
|
+
//
|
|
11
|
+
// Brace-delimited and whitespace-insensitive, so an element's positional geometry
|
|
12
|
+
// (`0,0 100x50`) stays on one line without the newline carrying meaning. Attribute names and
|
|
13
|
+
// values are plain `name` tokens rather than keywords: the grammar accepts any `name=value`
|
|
14
|
+
// and `lint.ts` rejects the ones the schema does not define, which is what lets an unknown
|
|
15
|
+
// attribute report itself instead of failing the parse three tokens later.
|
|
16
|
+
//
|
|
17
|
+
|
|
18
|
+
@top Document { statement* }
|
|
19
|
+
|
|
20
|
+
@skip { space | Comment }
|
|
21
|
+
|
|
22
|
+
statement {
|
|
23
|
+
ObjectDecl |
|
|
24
|
+
ElementsDecl |
|
|
25
|
+
MoveStmt |
|
|
26
|
+
RemoveObjectStmt |
|
|
27
|
+
RemoveElementsStmt |
|
|
28
|
+
DiagramDecl |
|
|
29
|
+
GroupDecl |
|
|
30
|
+
NodeDecl |
|
|
31
|
+
EdgeDecl
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// object <id> [@ x,y] [scale=…] [index=…] [ref=…] { … }
|
|
35
|
+
ObjectDecl { kw<"object"> Id ObjectAttr* Body }
|
|
36
|
+
|
|
37
|
+
// elements <objectId> { … } — adds to an existing object.
|
|
38
|
+
ElementsDecl { kw<"elements"> Id Body }
|
|
39
|
+
|
|
40
|
+
MoveStmt { kw<"move"> Id Origin }
|
|
41
|
+
|
|
42
|
+
RemoveObjectStmt { kw<"remove"> kw<"object"> Id }
|
|
43
|
+
|
|
44
|
+
// remove elements <objectId> <elementId>+
|
|
45
|
+
RemoveElementsStmt { kw<"remove"> kw<"elements"> Id Id+ }
|
|
46
|
+
|
|
47
|
+
ObjectAttr { Origin | Attribute }
|
|
48
|
+
|
|
49
|
+
//
|
|
50
|
+
// Semantic statements.
|
|
51
|
+
//
|
|
52
|
+
|
|
53
|
+
// diagram [@ x,y] [flow=down] [grid=384x224] [box=192x96] [aspect=4:3]
|
|
54
|
+
DiagramDecl { kw<"diagram"> (Origin | Attribute)* }
|
|
55
|
+
|
|
56
|
+
// group <id> ["label"] [relation <group>]* [compact] [gap=…] [max-width=…] [color=…] { node … edge … }
|
|
57
|
+
// Groups do not nest; the grammar accepts a nested one so it can be reported rather than mis-parsed.
|
|
58
|
+
// `compact` is a keyword, not a name, because a bare name here would read as the start of a relation.
|
|
59
|
+
GroupDecl { kw<"group"> Id Label? (Relation | Attribute | Compact)* GroupBody }
|
|
60
|
+
Compact { kw<"compact"> }
|
|
61
|
+
GroupBody { "{" (NodeDecl | EdgeDecl | GroupDecl)* "}" }
|
|
62
|
+
|
|
63
|
+
// node <id> ["label"] [@ x,y | @cell(c,r)] [~]<relation> <node> … [ref=…] [color=…] [shape=…]
|
|
64
|
+
NodeDecl { kw<"node"> Id Label? (Pin | Relation | Attribute)* }
|
|
65
|
+
|
|
66
|
+
// `@ x,y` is exact, in scene units; `@cell(c,r)` names a grid cell.
|
|
67
|
+
Pin { "@" (Point | CellRef) }
|
|
68
|
+
CellRef { kw<"cell"> "(" Coord "," Coord ")" }
|
|
69
|
+
|
|
70
|
+
// The relation word is a plain name checked by the reader, so `left` or `below` stay usable as ids;
|
|
71
|
+
// LR(1) tells it from an attribute by the `=` that follows an attribute name.
|
|
72
|
+
Relation { Soft? RelationKind Id }
|
|
73
|
+
Soft { "~" }
|
|
74
|
+
RelationKind { name }
|
|
75
|
+
|
|
76
|
+
// edge A[:[~]sides] (, B…)* (-> | <-> | -- | <relationship>) C[:[~]sides] (, D…)* ["label"] [via …] [bus[=name]] [attr=…]*
|
|
77
|
+
// `~` before the sides makes them a preference the router weighs, not a rule.
|
|
78
|
+
EdgeDecl { kw<"edge"> EndList EdgeOp EndList Label? (Via | Bus | Attribute)* }
|
|
79
|
+
EndList { EdgeEnd ("," EdgeEnd)* }
|
|
80
|
+
EdgeEnd { Id (":" Soft? SideList)? }
|
|
81
|
+
SideList { Side ("|" Side)* }
|
|
82
|
+
Side { name }
|
|
83
|
+
// A relationship word in place of the arrow says what the edge means and sets its markers:
|
|
84
|
+
// `Dog extends Animal`, `Order composes LineItem`, `Team owns Member`, `Customer one-to-many Order`.
|
|
85
|
+
EdgeOp { "->" | "<->" | "--" | RelationWord }
|
|
86
|
+
RelationWord { name }
|
|
87
|
+
|
|
88
|
+
// Each waypoint pins one or both coordinates of the route, in scene units or as `cell(c,r)`, where
|
|
89
|
+
// a half cell (`cell(1.5,_)`) is the channel between two columns or rows.
|
|
90
|
+
Via { kw<"via"> Waypoint+ }
|
|
91
|
+
Waypoint { Coord "," Coord | CellRef }
|
|
92
|
+
Coord { Number | Unbound }
|
|
93
|
+
|
|
94
|
+
Bus { kw<"bus"> ("=" Id)? }
|
|
95
|
+
Origin { "@" Point }
|
|
96
|
+
|
|
97
|
+
Body { "{" Element* "}" }
|
|
98
|
+
|
|
99
|
+
Element {
|
|
100
|
+
BoxElement |
|
|
101
|
+
CircleElement |
|
|
102
|
+
PathElement |
|
|
103
|
+
ArcElement |
|
|
104
|
+
TextElement |
|
|
105
|
+
ArrowElement |
|
|
106
|
+
PortalElement
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
BoxElement { BoxKind Id Point Size Label? Attribute* }
|
|
110
|
+
CircleElement { kw<"circle"> Id Point Number Label? Attribute* }
|
|
111
|
+
PathElement { PathKind Id Point Point+ Attribute* }
|
|
112
|
+
ArcElement { kw<"arc"> Id Point Number Range Attribute* }
|
|
113
|
+
TextElement { kw<"text"> Id Point Label Attribute* }
|
|
114
|
+
ArrowElement { kw<"arrow"> Id Connection Label? Attribute* }
|
|
115
|
+
PortalElement { kw<"portal"> Id Point Size Label? Attribute* }
|
|
116
|
+
|
|
117
|
+
BoxKind { kw<"rect"> | kw<"ellipse"> | kw<"diamond"> | kw<"triangle"> }
|
|
118
|
+
PathKind { kw<"line"> | kw<"curve"> }
|
|
119
|
+
|
|
120
|
+
// Both ends are written. `from`/`to` are independently optional in the schema, and `_` is how an
|
|
121
|
+
// absent one is spelled — writing the omission rather than leaving a hole keeps the trailing end
|
|
122
|
+
// distinguishable from the attributes that follow it, which an optional operand is not.
|
|
123
|
+
Connection { Endpoint "->" Endpoint }
|
|
124
|
+
Endpoint { Point | Ref | Unbound }
|
|
125
|
+
Unbound { kw<"_"> }
|
|
126
|
+
|
|
127
|
+
// `element`, `object/element`, either with `#port` — the spelling `Scene.formatRef` emits.
|
|
128
|
+
// Quoted when an id in it cannot lex bare (a dialect may produce `1st/box` from a numeric node id).
|
|
129
|
+
Ref { name | String }
|
|
130
|
+
|
|
131
|
+
// Every attribute is `name=value`, with no bare flags: after `->` a bare word would otherwise be
|
|
132
|
+
// ambiguous between a ref endpoint and a flag, which LR(1) cannot settle, and `closed=true` is
|
|
133
|
+
// the only case that wanted one.
|
|
134
|
+
Attribute { AttrName "=" AttrValue }
|
|
135
|
+
AttrName { name }
|
|
136
|
+
AttrValue { Number | Size | Ratio | String | Word }
|
|
137
|
+
// `aspect=4:3`.
|
|
138
|
+
Ratio { Number ":" Number }
|
|
139
|
+
|
|
140
|
+
// `head=triangle`, `head=arrow` and `tail=circle` are schema literals that are also element-kind
|
|
141
|
+
// keywords, and a reserved word no longer lexes as a plain name. Listing every reserved word —
|
|
142
|
+
// not just the three that collide today — keeps this correct when the schema gains another.
|
|
143
|
+
Word {
|
|
144
|
+
name |
|
|
145
|
+
kw<"object"> | kw<"elements"> | kw<"move"> | kw<"remove"> |
|
|
146
|
+
kw<"rect"> | kw<"ellipse"> | kw<"diamond"> | kw<"triangle"> |
|
|
147
|
+
kw<"circle"> | kw<"line"> | kw<"curve"> | kw<"arc"> |
|
|
148
|
+
kw<"text"> | kw<"arrow"> | kw<"portal"> |
|
|
149
|
+
kw<"diagram"> | kw<"group"> | kw<"node"> | kw<"edge"> | kw<"cell"> | kw<"via"> | kw<"bus"> | kw<"compact">
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
Id { name | String }
|
|
153
|
+
Label { String }
|
|
154
|
+
Point { Number "," Number }
|
|
155
|
+
Range { Number ".." Number }
|
|
156
|
+
|
|
157
|
+
Number { number }
|
|
158
|
+
Size { size }
|
|
159
|
+
|
|
160
|
+
// Keyword specialization: the statement and element-kind words drive the structure, so they
|
|
161
|
+
// must not lex as ids. An id that collides is written quoted (the printer does it).
|
|
162
|
+
kw<term> { @specialize[@name={term}]<name, term> }
|
|
163
|
+
|
|
164
|
+
@tokens {
|
|
165
|
+
nameStart { $[a-zA-Z_] }
|
|
166
|
+
nameChar { $[a-zA-Z0-9_\-] }
|
|
167
|
+
|
|
168
|
+
// One token covers plain ids and refs: `#` is both the port separator and the comment
|
|
169
|
+
// character, and longest-match is what makes `A/box#left` a ref while ` # note` is a
|
|
170
|
+
// comment. A comment therefore needs whitespace in front of it.
|
|
171
|
+
name { nameStart nameChar* ("/" nameStart nameChar*)? ("#" nameStart nameChar*)? }
|
|
172
|
+
|
|
173
|
+
digits { @digit+ }
|
|
174
|
+
// The exponent is accepted because the printer emits `String(value)`, which switches to
|
|
175
|
+
// exponent notation outside roughly 1e-7..1e21 — without it, printing such a scene would
|
|
176
|
+
// produce text this grammar rejects.
|
|
177
|
+
exponent { $[eE] $[+\-]? digits }
|
|
178
|
+
decimal { "-"? digits ("." digits)? exponent? }
|
|
179
|
+
number { decimal }
|
|
180
|
+
// `100x50`, longer than the `100` a bare number would match, so the tokenizer prefers it.
|
|
181
|
+
size { decimal "x" decimal }
|
|
182
|
+
|
|
183
|
+
String { '"' (!["\\] | "\\" _)* '"' }
|
|
184
|
+
|
|
185
|
+
Comment { "#" ![\n]* }
|
|
186
|
+
|
|
187
|
+
space { $[ \t\r\n]+ }
|
|
188
|
+
|
|
189
|
+
"@" "->" "<->" "--" ".." "{" "}" "=" "," ":" "|" "~" "(" ")"
|
|
190
|
+
|
|
191
|
+
@precedence { size, number }
|
|
192
|
+
@precedence { name, Comment }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
@detectDelim
|