@marianoguerra/railroad-diagrams 0.0.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/CHANGELOG.md +16 -0
- package/LICENSE +9 -0
- package/README.md +97 -0
- package/bin/railroad-diagrams.js +70 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/layout.d.ts +70 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +185 -0
- package/dist/layout.js.map +1 -0
- package/dist/model.d.ts +38 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +12 -0
- package/dist/model.js.map +1 -0
- package/dist/ohm.d.ts +33 -0
- package/dist/ohm.d.ts.map +1 -0
- package/dist/ohm.js +158 -0
- package/dist/ohm.js.map +1 -0
- package/dist/svg.d.ts +5 -0
- package/dist/svg.d.ts.map +1 -0
- package/dist/svg.js +72 -0
- package/dist/svg.js.map +1 -0
- package/package.json +58 -0
- package/scripts/grammar-stages.js +71 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes will be documented here, following [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-07-23
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Width-aware railroad diagram modeling, layout, and standalone SVG rendering.
|
|
12
|
+
- Optional Ohm grammar conversion through the `/ohm` package subpath.
|
|
13
|
+
- RTL layout, wrapping, justification, styling hooks, and accessible SVG metadata.
|
|
14
|
+
|
|
15
|
+
[Unreleased]: https://github.com/marianoguerra/railroad-diagrams/compare/v0.1.0...HEAD
|
|
16
|
+
[0.1.0]: https://github.com/marianoguerra/railroad-diagrams/releases/tag/v0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mariano Guerra
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# @marianoguerra/railroad-diagrams
|
|
2
|
+
|
|
3
|
+
A dependency-free core TypeScript implementation of the align → wrap → justify algorithm from [*Automatic layout of railroad diagrams*](https://arxiv.org/abs/2509.15834), informed by the authors' [librrd reference implementation](https://github.com/epfl-systemf/librrd), producing standalone SVG strings.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
The package is ESM-only and supports Node.js 18 or newer. It also works in modern browsers through an ESM-aware bundler.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @marianoguerra/railroad-diagrams
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { sequence, terminal, nonterminal, zeroOrMore, renderSvg } from "@marianoguerra/railroad-diagrams";
|
|
15
|
+
|
|
16
|
+
const diagram = sequence(
|
|
17
|
+
terminal("["),
|
|
18
|
+
zeroOrMore(nonterminal("value"), terminal(",")),
|
|
19
|
+
terminal("]"),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const svg = renderSvg(diagram, { width: 480 });
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The diagram language has terminals, nonterminals, n-ary sequences, positive stacks (`choice`, `alternatives`, `optional`) and reverse-flow negative stacks (`loop`, `oneOrMore`, `zeroOrMore`). `renderSvg` accepts a target `width`, `ltr`/`rtl` direction, vertical alignment and direction-aware justification policies, gaps, fonts, continuation markers, and a custom text measurement callback.
|
|
26
|
+
|
|
27
|
+
### Core API
|
|
28
|
+
|
|
29
|
+
| Export | Purpose |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| `terminal`, `nonterminal` | Create labeled stations |
|
|
32
|
+
| `sequence` | Connect diagrams in order |
|
|
33
|
+
| `choice`, `alternatives`, `optional` | Create forward-flow branches |
|
|
34
|
+
| `loop`, `oneOrMore`, `zeroOrMore` | Create reverse-flow repetition branches |
|
|
35
|
+
| `contentWidths`, `layout` | Measure and lay out diagrams without rendering |
|
|
36
|
+
| `renderSvg` / `toSVG` | Return a standalone SVG string |
|
|
37
|
+
|
|
38
|
+
Important rendering options include `width`, `direction`, `align`, `justify`, `gap`, `rowGap`, `fontSize`, `fontFamily`, `continuationMarker`, `measureText`, `accessibleLabel`, and `accessibleDescription`. Invalid numeric options throw `RangeError`.
|
|
39
|
+
|
|
40
|
+
The generated SVG can be themed with `--rrd-stroke`, `--rrd-fill`, and `--rrd-terminal`. User-provided text and metadata are XML-escaped. For non-default fonts, supply `measureText` when accurate wrapping is important.
|
|
41
|
+
|
|
42
|
+
Layout is also available independently with `layout`, and `contentWidths` exposes the min/max-content measurements used for wrapping. Narrow sequences enumerate their row partitions and prefer fewer, shallower wraps while minimizing max-content overflow, following the paper and librrd's local heuristic.
|
|
43
|
+
|
|
44
|
+
Run `npm test` or `npm run example`.
|
|
45
|
+
|
|
46
|
+
### CLI
|
|
47
|
+
|
|
48
|
+
The package installs a `railroad-diagrams` command with SVG, JSON, and gallery subcommands:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
railroad-diagrams svg grammar.ohm --rule Main --width 900 -o Main.svg
|
|
52
|
+
railroad-diagrams svg grammar.ohm --rule Main --full -o Main.full.svg
|
|
53
|
+
railroad-diagrams json grammar.ohm -o grammar.json
|
|
54
|
+
railroad-diagrams stages grammars/01-start.ohm grammars/02-expressions.ohm -o grammar-stages
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Omit `-o` from `svg` or `json` to write to standard output. The first rule is used when `--rule` is omitted. `stages` accepts any ordered list of Ohm grammar files and creates a self-contained HTML gallery; each filename becomes its stage name.
|
|
58
|
+
|
|
59
|
+
## Ohm grammars
|
|
60
|
+
|
|
61
|
+
The optional Ohm integration uses a semantics over Ohm's own grammar language to turn grammar rules into diagrams. Install its peer dependency and import the dedicated subpath:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
npm install ohm-js
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { renderOhmGrammar } from "@marianoguerra/railroad-diagrams/ohm";
|
|
69
|
+
|
|
70
|
+
const diagrams = renderOhmGrammar(`
|
|
71
|
+
NopLang {
|
|
72
|
+
Main = ""
|
|
73
|
+
}
|
|
74
|
+
`);
|
|
75
|
+
|
|
76
|
+
const mainSvg = diagrams.get("Main");
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`railroadGrammar` returns the intermediate diagram model instead. Sequences, alternatives, `*`, `+`, `?`, literals, ranges, and rule applications are supported.
|
|
80
|
+
|
|
81
|
+
`renderOhmRuleFull` recursively inlines user-defined rule references to produce a full-depth diagram. Recursive cycles remain as nonterminal stations at the point where the cycle closes, keeping the output finite.
|
|
82
|
+
|
|
83
|
+
For large grammars, pass `{preserveSharedRules: true, maxNodes: 1200}` as its expansion options. Single-use rules are inlined, while shared low-level rules, built-ins, recursive cycles, and expansions beyond the structural budget remain factored as nonterminal stations.
|
|
84
|
+
|
|
85
|
+
The complete evolution of the Wafer grammar from *WebAssembly from the Ground Up* is generated under `examples/wafer-stages`. To regenerate it from a checkout of `wasmgroundup/code`:
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
npm run wafer-diagrams -- /path/to/wasmgroundup-code
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Every stage includes an `overview.html` rule atlas. It places the selectively expanded start rule first, then renders every retained user-defined nonterminal below it once in breadth-first dependency order.
|
|
92
|
+
|
|
93
|
+
## Development and releases
|
|
94
|
+
|
|
95
|
+
Run `npm test` for the clean build and test suite, or `npm run release:check` to additionally build and install the exact npm tarball in a temporary project. See [CONTRIBUTING.md](CONTRIBUTING.md) and [RELEASING.md](RELEASING.md).
|
|
96
|
+
|
|
97
|
+
This project is available under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {mkdir, readFile, writeFile} from "node:fs/promises";
|
|
3
|
+
import {basename, dirname, extname, resolve} from "node:path";
|
|
4
|
+
import {railroadGrammar, renderOhmRuleFull} from "../dist/ohm.js";
|
|
5
|
+
import {renderSvg} from "../dist/index.js";
|
|
6
|
+
import {generateGrammarStages} from "../scripts/grammar-stages.js";
|
|
7
|
+
|
|
8
|
+
const [command, ...arguments_] = process.argv.slice(2);
|
|
9
|
+
const usage = `Usage:
|
|
10
|
+
railroad-diagrams svg <grammar.ohm> [-o output.svg] [--rule NAME] [--width PX] [--full]
|
|
11
|
+
railroad-diagrams json <grammar.ohm> [-o output.json]
|
|
12
|
+
railroad-diagrams stages <grammar.ohm>... -o <directory>
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
17
|
+
process.stdout.write(usage);
|
|
18
|
+
} else if (command === "svg") {
|
|
19
|
+
const {input, output, options} = parse(arguments_);
|
|
20
|
+
if (!input) throw new Error("svg requires an Ohm grammar file");
|
|
21
|
+
const source = await readFile(input, "utf8");
|
|
22
|
+
const grammar = railroadGrammar(source);
|
|
23
|
+
const rule = options.rule ?? grammar.rules[0]?.name;
|
|
24
|
+
if (!rule) throw new Error("The grammar contains no rules");
|
|
25
|
+
const width = numberOption(options.width, 900, "width");
|
|
26
|
+
const svg = options.full ? renderOhmRuleFull(source, rule, {width}) : renderSvg({...requiredRule(grammar, rule).diagram, label: rule}, {width});
|
|
27
|
+
await emit(svg, output);
|
|
28
|
+
} else if (command === "json") {
|
|
29
|
+
const {input, output} = parse(arguments_);
|
|
30
|
+
if (!input) throw new Error("json requires an Ohm grammar file");
|
|
31
|
+
await emit(`${JSON.stringify(railroadGrammar(await readFile(input, "utf8")), null, 2)}\n`, output);
|
|
32
|
+
} else if (command === "stages") {
|
|
33
|
+
const {inputs, output} = parse(arguments_, {multiple: true});
|
|
34
|
+
if (!inputs.length) throw new Error(`${command} requires one or more Ohm grammar files`);
|
|
35
|
+
if (!output) throw new Error(`${command} requires --output`);
|
|
36
|
+
const stages = await Promise.all(inputs.map(async input => ({
|
|
37
|
+
name: basename(input, extname(input)),
|
|
38
|
+
source: input,
|
|
39
|
+
grammarSource: await readFile(input, "utf8"),
|
|
40
|
+
})));
|
|
41
|
+
await generateGrammarStages(stages, resolve(output));
|
|
42
|
+
} else {
|
|
43
|
+
throw new Error(`Unknown command ${JSON.stringify(command)}`);
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
process.stderr.write(`railroad-diagrams: ${error.message}\n\n${usage}`);
|
|
47
|
+
process.exitCode = 1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parse(args, {multiple = false} = {}) {
|
|
51
|
+
let input;
|
|
52
|
+
const inputs = [];
|
|
53
|
+
let output;
|
|
54
|
+
const options = {};
|
|
55
|
+
for (let index = 0; index < args.length; index++) {
|
|
56
|
+
const arg = args[index];
|
|
57
|
+
if (arg === "-o" || arg === "--output") output = valueAfter(args, ++index, arg);
|
|
58
|
+
else if (arg === "--rule" || arg === "--width") options[arg.slice(2)] = valueAfter(args, ++index, arg);
|
|
59
|
+
else if (arg === "--full") options.full = true;
|
|
60
|
+
else if (arg.startsWith("-")) throw new Error(`Unknown option ${arg}`);
|
|
61
|
+
else if (multiple) inputs.push(arg);
|
|
62
|
+
else if (input) throw new Error(`Unexpected argument ${arg}`);
|
|
63
|
+
else input = arg;
|
|
64
|
+
}
|
|
65
|
+
return {input, inputs, output, options};
|
|
66
|
+
}
|
|
67
|
+
function valueAfter(args, index, option) { if (!args[index]) throw new Error(`${option} requires a value`); return args[index]; }
|
|
68
|
+
function numberOption(value, fallback, name) { const number = value === undefined ? fallback : Number(value); if (!(number > 0)) throw new Error(`--${name} must be positive`); return number; }
|
|
69
|
+
function requiredRule(grammar, name) { const rule = grammar.rules.find(candidate => candidate.name === name); if (!rule) throw new Error(`Unknown rule ${JSON.stringify(name)}`); return rule; }
|
|
70
|
+
async function emit(content, output) { if (!output) process.stdout.write(content); else { await mkdir(dirname(resolve(output)), {recursive: true}); await writeFile(output, content); } }
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC","sourcesContent":["export * from \"./model.js\";\nexport * from \"./layout.js\";\nexport * from \"./svg.js\";\n"]}
|
package/dist/layout.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Align, Diagram, Direction, Justify, Metadata } from "./model.js";
|
|
2
|
+
export interface LayoutOptions {
|
|
3
|
+
width?: number;
|
|
4
|
+
direction?: Direction;
|
|
5
|
+
align?: Align;
|
|
6
|
+
justify?: Justify;
|
|
7
|
+
gap?: number;
|
|
8
|
+
rowGap?: number;
|
|
9
|
+
fontSize?: number;
|
|
10
|
+
fontFamily?: string;
|
|
11
|
+
paddingX?: number;
|
|
12
|
+
paddingY?: number;
|
|
13
|
+
radius?: number;
|
|
14
|
+
continuationMarker?: string;
|
|
15
|
+
flexAbsorb?: number;
|
|
16
|
+
measureText?: (text: string, fontSize: number, fontFamily: string) => number;
|
|
17
|
+
/** Accessible name placed on the generated SVG. */
|
|
18
|
+
accessibleLabel?: string;
|
|
19
|
+
/** Longer accessible description placed in the generated SVG. */
|
|
20
|
+
accessibleDescription?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface ResolvedOptions {
|
|
23
|
+
width: number;
|
|
24
|
+
direction: Direction;
|
|
25
|
+
align: Align;
|
|
26
|
+
justify: Justify;
|
|
27
|
+
gap: number;
|
|
28
|
+
rowGap: number;
|
|
29
|
+
fontSize: number;
|
|
30
|
+
fontFamily: string;
|
|
31
|
+
paddingX: number;
|
|
32
|
+
paddingY: number;
|
|
33
|
+
radius: number;
|
|
34
|
+
continuationMarker: string;
|
|
35
|
+
flexAbsorb: number;
|
|
36
|
+
measureText: (text: string, fontSize: number, fontFamily: string) => number;
|
|
37
|
+
accessibleLabel: string;
|
|
38
|
+
accessibleDescription: string;
|
|
39
|
+
}
|
|
40
|
+
export interface LayoutNode extends Metadata {
|
|
41
|
+
kind: "empty" | "station" | "row" | "wrapped" | "stack";
|
|
42
|
+
width: number;
|
|
43
|
+
height: number;
|
|
44
|
+
entryY: number;
|
|
45
|
+
exitY: number;
|
|
46
|
+
direction: Direction;
|
|
47
|
+
terminal?: boolean;
|
|
48
|
+
text?: string;
|
|
49
|
+
children?: PositionedLayout[];
|
|
50
|
+
rows?: PositionedLayout[];
|
|
51
|
+
top?: PositionedLayout;
|
|
52
|
+
bottom?: PositionedLayout;
|
|
53
|
+
polarity?: "positive" | "negative";
|
|
54
|
+
}
|
|
55
|
+
export interface PositionedLayout {
|
|
56
|
+
x: number;
|
|
57
|
+
y: number;
|
|
58
|
+
node: LayoutNode;
|
|
59
|
+
}
|
|
60
|
+
export interface Widths {
|
|
61
|
+
min: number;
|
|
62
|
+
max: number;
|
|
63
|
+
}
|
|
64
|
+
export declare function resolveOptions(options?: LayoutOptions): ResolvedOptions;
|
|
65
|
+
export declare function contentWidths(d: Diagram, options?: LayoutOptions | ResolvedOptions): Widths;
|
|
66
|
+
export declare function layout(diagram: Diagram, options?: LayoutOptions): {
|
|
67
|
+
node: LayoutNode;
|
|
68
|
+
options: ResolvedOptions;
|
|
69
|
+
};
|
|
70
|
+
//# sourceMappingURL=layout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../src/layout.ts"],"names":[],"mappings":"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"}
|
package/dist/layout.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
const defaults = {
|
|
2
|
+
width: 640, direction: "ltr", align: "baseline", justify: "start",
|
|
3
|
+
gap: 20, rowGap: 24, fontSize: 15, fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
4
|
+
paddingX: 10, paddingY: 7, radius: 10, continuationMarker: "", flexAbsorb: 0.1,
|
|
5
|
+
measureText: (text, size) => [...text].reduce((n, c) => n + (/[^\x00-\xff]/.test(c) ? 1 : .61), 0) * size,
|
|
6
|
+
accessibleLabel: "Railroad diagram", accessibleDescription: "",
|
|
7
|
+
};
|
|
8
|
+
export function resolveOptions(options = {}) {
|
|
9
|
+
const o = { ...defaults, ...options };
|
|
10
|
+
for (const key of ["width", "gap", "rowGap", "fontSize", "paddingX", "paddingY", "radius"])
|
|
11
|
+
if (!Number.isFinite(o[key]) || o[key] < 0)
|
|
12
|
+
throw new RangeError(`${key} must be a finite non-negative number`);
|
|
13
|
+
if (o.flexAbsorb < 0 || o.flexAbsorb > 1)
|
|
14
|
+
throw new RangeError("flexAbsorb must be between 0 and 1");
|
|
15
|
+
return o;
|
|
16
|
+
}
|
|
17
|
+
function stationWidth(d, o) {
|
|
18
|
+
return Math.max(2 * o.radius, o.measureText(d.text, o.fontSize, o.fontFamily) + 2 * o.paddingX) + 2 * o.radius;
|
|
19
|
+
}
|
|
20
|
+
export function contentWidths(d, options = {}) {
|
|
21
|
+
const o = "measureText" in options && "continuationMarker" in options ? options : resolveOptions(options);
|
|
22
|
+
switch (d.type) {
|
|
23
|
+
case "terminal":
|
|
24
|
+
case "nonterminal": {
|
|
25
|
+
const w = stationWidth(d, o);
|
|
26
|
+
return { min: w, max: w };
|
|
27
|
+
}
|
|
28
|
+
case "sequence": {
|
|
29
|
+
if (!d.items.length)
|
|
30
|
+
return { min: 0, max: 0 };
|
|
31
|
+
const ws = d.items.map(x => contentWidths(x, o));
|
|
32
|
+
return { min: Math.max(...ws.map(x => x.min)) + 4 * o.radius,
|
|
33
|
+
max: ws.reduce((n, x) => n + x.max, o.gap * (ws.length - 1)) };
|
|
34
|
+
}
|
|
35
|
+
case "stack": {
|
|
36
|
+
const a = contentWidths(d.top, o), b = contentWidths(d.bottom, o);
|
|
37
|
+
return { min: Math.max(a.min, b.min) + 6 * o.radius, max: Math.max(a.max, b.max) + 6 * o.radius };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function partitions(items) {
|
|
42
|
+
if (!items.length)
|
|
43
|
+
return [[[]]];
|
|
44
|
+
let out = [[[items[0]]]];
|
|
45
|
+
for (const item of items.slice(1))
|
|
46
|
+
out = out.flatMap(p => [
|
|
47
|
+
[...p, [item]],
|
|
48
|
+
[...p.slice(0, -1), [...p[p.length - 1], item]],
|
|
49
|
+
]);
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
function rowBounds(items, o) {
|
|
53
|
+
if (!items.length)
|
|
54
|
+
return { min: 0, max: 0 };
|
|
55
|
+
const ws = items.map(x => contentWidths(x, o));
|
|
56
|
+
return { min: ws.reduce((n, x) => n + x.min, o.gap * (items.length - 1)),
|
|
57
|
+
max: ws.reduce((n, x) => n + x.max, o.gap * (items.length - 1)) };
|
|
58
|
+
}
|
|
59
|
+
function choosePartition(items, width, depth, o) {
|
|
60
|
+
const marker = o.continuationMarker ? o.measureText(o.continuationMarker, o.fontSize, o.fontFamily) + o.radius : 2 * o.radius;
|
|
61
|
+
const score = (rows) => {
|
|
62
|
+
const bs = rows.map(r => rowBounds(r, o));
|
|
63
|
+
const min = Math.max(...bs.map((b, i) => b.min + (rows.length > 1 ? (i === 0 || i === rows.length - 1 ? marker : 2 * marker) : 0)));
|
|
64
|
+
const max = Math.max(...bs.map(b => b.max));
|
|
65
|
+
const wrapPenalty = rows.length * 10 * 2 ** (2 * depth);
|
|
66
|
+
return { rows, min, max, score: wrapPenalty + Math.max(0, max - width) ** 2 };
|
|
67
|
+
};
|
|
68
|
+
// Exact enumeration is useful for short rows but exponential. For long
|
|
69
|
+
// sequences, dynamic programming keeps one best prefix for each row count.
|
|
70
|
+
const candidates = items.length <= 12 ? partitions(items).map(score) : (() => {
|
|
71
|
+
const best = Array.from({ length: items.length + 1 }, () => []);
|
|
72
|
+
best[0][0] = [];
|
|
73
|
+
for (let end = 1; end <= items.length; end++) {
|
|
74
|
+
for (let start = 0; start < end; start++) {
|
|
75
|
+
for (const prefix of best[start]) {
|
|
76
|
+
if (!prefix)
|
|
77
|
+
continue;
|
|
78
|
+
const candidate = [...prefix, items.slice(start, end)];
|
|
79
|
+
const rows = candidate.length;
|
|
80
|
+
const current = best[end][rows];
|
|
81
|
+
if (!current || score(candidate).score < score(current).score)
|
|
82
|
+
best[end][rows] = candidate;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return best[items.length].filter((rows) => Boolean(rows)).map(score);
|
|
87
|
+
})();
|
|
88
|
+
const fitting = candidates.filter(c => c.min <= width + .001);
|
|
89
|
+
return (fitting.length ? fitting : candidates).sort((a, b) => a.score - b.score || a.max - b.max)[0].rows;
|
|
90
|
+
}
|
|
91
|
+
function distribute(total, count, minGap, policy, direction) {
|
|
92
|
+
if (!count)
|
|
93
|
+
return [total];
|
|
94
|
+
const innerBase = minGap * Math.max(0, count - 1), extra = Math.max(0, total - innerBase);
|
|
95
|
+
const start = policy === "start" ? 0 : policy === "end" ? extra : policy === "center" ? extra / 2 :
|
|
96
|
+
policy === "space-around" ? extra / (2 * count) : policy === "space-evenly" ? extra / (count + 1) : 0;
|
|
97
|
+
const end = policy === "start" ? extra : policy === "end" ? 0 : policy === "center" ? extra / 2 : start;
|
|
98
|
+
let inner = count <= 1 ? 0 : minGap;
|
|
99
|
+
if (policy === "space-between" && count > 1)
|
|
100
|
+
inner += extra / (count - 1);
|
|
101
|
+
if (policy === "space-around")
|
|
102
|
+
inner += extra / count;
|
|
103
|
+
if (policy === "space-evenly")
|
|
104
|
+
inner += extra / (count + 1);
|
|
105
|
+
const rails = [start, ...Array(Math.max(0, count - 1)).fill(inner), end];
|
|
106
|
+
return direction === "rtl" ? rails.reverse() : rails;
|
|
107
|
+
}
|
|
108
|
+
function layoutRow(items, target, depth, o, direction) {
|
|
109
|
+
if (!items.length)
|
|
110
|
+
return { kind: "empty", width: target, height: 2, entryY: 1, exitY: 1, direction };
|
|
111
|
+
const bounds = items.map(x => contentWidths(x, o));
|
|
112
|
+
const base = bounds.reduce((n, x) => n + x.min, 0);
|
|
113
|
+
let available = Math.max(0, target - base - o.gap * (items.length - 1));
|
|
114
|
+
const growth = bounds.map(x => x.max - x.min), sumGrowth = growth.reduce((a, b) => a + b, 0);
|
|
115
|
+
const grown = Math.min(available, sumGrowth);
|
|
116
|
+
const widths = bounds.map((b, i) => b.min + (sumGrowth ? grown * growth[i] / sumGrowth : 0));
|
|
117
|
+
available -= grown;
|
|
118
|
+
const absorbed = available * o.flexAbsorb;
|
|
119
|
+
const rails = distribute(o.gap * (items.length - 1) + absorbed, items.length, o.gap, o.justify, direction);
|
|
120
|
+
const leftover = available - absorbed;
|
|
121
|
+
if (leftover && sumGrowth)
|
|
122
|
+
widths.forEach((_, i) => widths[i] += leftover * Math.max(1, bounds[i].max) / bounds.reduce((n, b) => n + Math.max(1, b.max), 0));
|
|
123
|
+
else
|
|
124
|
+
rails[direction === "ltr" ? rails.length - 1 : 0] += leftover;
|
|
125
|
+
const childNodes = items.map((d, i) => layoutRec(d, widths[i], depth + 1, o, direction));
|
|
126
|
+
const baseline = Math.max(...childNodes.map(n => n.entryY));
|
|
127
|
+
const ys = childNodes.map(n => baseline - n.entryY);
|
|
128
|
+
const height = Math.max(...childNodes.map((n, i) => ys[i] + n.height));
|
|
129
|
+
const visual = direction === "ltr" ? childNodes.map((n, i) => ({ n, i })) : childNodes.map((n, i) => ({ n, i })).reverse();
|
|
130
|
+
let x = rails[0];
|
|
131
|
+
const children = [];
|
|
132
|
+
visual.forEach(({ n, i }, vi) => { children.push({ x, y: ys[i], node: n }); x += n.width + rails[vi + 1]; });
|
|
133
|
+
const first = children[0], last = children[children.length - 1];
|
|
134
|
+
const start = direction === "ltr" ? first : last;
|
|
135
|
+
const end = direction === "ltr" ? last : first;
|
|
136
|
+
return { kind: "row", width: target, height, entryY: start.y + start.node.entryY,
|
|
137
|
+
exitY: end.y + end.node.exitY, direction, children };
|
|
138
|
+
}
|
|
139
|
+
function layoutSequence(d, width, depth, o, direction) {
|
|
140
|
+
if (!d.items.length)
|
|
141
|
+
return { ...d, kind: "empty", width, height: 2, entryY: 1, exitY: 1, direction };
|
|
142
|
+
const rows = choosePartition(d.items, width, depth, o);
|
|
143
|
+
if (rows.length === 1)
|
|
144
|
+
return { ...layoutRow(rows[0], width, depth, o, direction), ...d, kind: "row" };
|
|
145
|
+
const laid = rows.map(r => layoutRow(r, width - 4 * o.radius, depth, o, direction));
|
|
146
|
+
const positioned = [];
|
|
147
|
+
let y = 0;
|
|
148
|
+
laid.forEach(n => { positioned.push({ x: 2 * o.radius, y, node: n }); y += n.height + o.rowGap; });
|
|
149
|
+
const height = y - o.rowGap;
|
|
150
|
+
return { ...d, kind: "wrapped", width, height, entryY: positioned[0].node.entryY,
|
|
151
|
+
exitY: positioned.at(-1).y + positioned.at(-1).node.exitY, direction, rows: positioned };
|
|
152
|
+
}
|
|
153
|
+
function layoutRec(d, width, depth, o, direction) {
|
|
154
|
+
switch (d.type) {
|
|
155
|
+
case "terminal":
|
|
156
|
+
case "nonterminal": {
|
|
157
|
+
const natural = stationWidth(d, o), h = o.fontSize + 2 * o.paddingY;
|
|
158
|
+
const station = { ...d, kind: "station", terminal: d.type === "terminal", width: natural, height: h,
|
|
159
|
+
entryY: h / 2, exitY: h / 2, direction };
|
|
160
|
+
if (width <= natural + .001)
|
|
161
|
+
return station;
|
|
162
|
+
const [before] = distribute(width - natural, 1, 0, o.justify, direction);
|
|
163
|
+
return { kind: "row", width, height: h, entryY: h / 2, exitY: h / 2, direction,
|
|
164
|
+
children: [{ x: before, y: 0, node: station }] };
|
|
165
|
+
}
|
|
166
|
+
case "sequence": return layoutSequence(d, width, depth, o, direction);
|
|
167
|
+
case "stack": {
|
|
168
|
+
const innerWidth = Math.max(0, width - 6 * o.radius);
|
|
169
|
+
const top = layoutRec(d.top, innerWidth, depth + 1, o, direction);
|
|
170
|
+
const polarity = d.polarity ?? "positive";
|
|
171
|
+
const bottom = layoutRec(d.bottom, innerWidth, depth + 1, o, polarity === "negative" ? (direction === "ltr" ? "rtl" : "ltr") : direction);
|
|
172
|
+
const bottomY = top.height + o.rowGap, height = bottomY + bottom.height;
|
|
173
|
+
const entryY = o.align === "top" ? top.entryY : o.align === "bottom" ? bottomY + bottom.entryY :
|
|
174
|
+
o.align === "center" ? height / 2 : (d.top.type === "sequence" && !d.top.items.length ? bottomY + bottom.entryY : top.entryY);
|
|
175
|
+
return { ...d, kind: "stack", width, height, entryY, exitY: entryY, direction, polarity,
|
|
176
|
+
top: { x: 3 * o.radius, y: 0, node: top }, bottom: { x: 3 * o.radius, y: bottomY, node: bottom } };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
export function layout(diagram, options = {}) {
|
|
181
|
+
const o = resolveOptions(options), bounds = contentWidths(diagram, o);
|
|
182
|
+
const width = Math.max(o.width <= 1 ? bounds.max * o.width : o.width, bounds.min);
|
|
183
|
+
return { node: layoutRec(diagram, width, 0, o, o.direction), options: o };
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=layout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layout.js","sourceRoot":"","sources":["../src/layout.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Align, Diagram, Direction, Justify, Metadata } from \"./model.js\";\n\nexport interface LayoutOptions {\n width?: number;\n direction?: Direction;\n align?: Align;\n justify?: Justify;\n gap?: number;\n rowGap?: number;\n fontSize?: number;\n fontFamily?: string;\n paddingX?: number;\n paddingY?: number;\n radius?: number;\n continuationMarker?: string;\n flexAbsorb?: number;\n measureText?: (text: string, fontSize: number, fontFamily: string) => number;\n /** Accessible name placed on the generated SVG. */\n accessibleLabel?: string;\n /** Longer accessible description placed in the generated SVG. */\n accessibleDescription?: string;\n}\n\nexport interface ResolvedOptions {\n width: number; direction: Direction; align: Align; justify: Justify;\n gap: number; rowGap: number; fontSize: number; fontFamily: string;\n paddingX: number; paddingY: number; radius: number;\n continuationMarker: string; flexAbsorb: number;\n measureText: (text: string, fontSize: number, fontFamily: string) => number;\n accessibleLabel: string; accessibleDescription: string;\n}\n\nexport interface LayoutNode extends Metadata {\n kind: \"empty\" | \"station\" | \"row\" | \"wrapped\" | \"stack\";\n width: number;\n height: number;\n entryY: number;\n exitY: number;\n direction: Direction;\n terminal?: boolean;\n text?: string;\n children?: PositionedLayout[];\n rows?: PositionedLayout[];\n top?: PositionedLayout;\n bottom?: PositionedLayout;\n polarity?: \"positive\" | \"negative\";\n}\nexport interface PositionedLayout { x: number; y: number; node: LayoutNode }\nexport interface Widths { min: number; max: number }\n\nconst defaults: ResolvedOptions = {\n width: 640, direction: \"ltr\", align: \"baseline\", justify: \"start\",\n gap: 20, rowGap: 24, fontSize: 15, fontFamily: \"ui-monospace, SFMono-Regular, Menlo, monospace\",\n paddingX: 10, paddingY: 7, radius: 10, continuationMarker: \"\", flexAbsorb: 0.1,\n measureText: (text, size) => [...text].reduce((n, c) => n + (/[^\\x00-\\xff]/.test(c) ? 1 : .61), 0) * size,\n accessibleLabel: \"Railroad diagram\", accessibleDescription: \"\",\n};\n\nexport function resolveOptions(options: LayoutOptions = {}): ResolvedOptions {\n const o = { ...defaults, ...options };\n for (const key of [\"width\", \"gap\", \"rowGap\", \"fontSize\", \"paddingX\", \"paddingY\", \"radius\"] as const)\n if (!Number.isFinite(o[key]) || o[key] < 0) throw new RangeError(`${key} must be a finite non-negative number`);\n if (o.flexAbsorb < 0 || o.flexAbsorb > 1) throw new RangeError(\"flexAbsorb must be between 0 and 1\");\n return o;\n}\n\nfunction stationWidth(d: Extract<Diagram, {type: \"terminal\" | \"nonterminal\"}>, o: ResolvedOptions): number {\n return Math.max(2 * o.radius, o.measureText(d.text, o.fontSize, o.fontFamily) + 2 * o.paddingX) + 2 * o.radius;\n}\n\nexport function contentWidths(d: Diagram, options: LayoutOptions | ResolvedOptions = {}): Widths {\n const o = \"measureText\" in options && \"continuationMarker\" in options ? options as ResolvedOptions : resolveOptions(options);\n switch (d.type) {\n case \"terminal\": case \"nonterminal\": { const w = stationWidth(d, o); return { min: w, max: w }; }\n case \"sequence\": {\n if (!d.items.length) return { min: 0, max: 0 };\n const ws = d.items.map(x => contentWidths(x, o));\n return { min: Math.max(...ws.map(x => x.min)) + 4 * o.radius,\n max: ws.reduce((n, x) => n + x.max, o.gap * (ws.length - 1)) };\n }\n case \"stack\": {\n const a = contentWidths(d.top, o), b = contentWidths(d.bottom, o);\n return { min: Math.max(a.min, b.min) + 6 * o.radius, max: Math.max(a.max, b.max) + 6 * o.radius };\n }\n }\n}\n\ntype Partition = Diagram[][];\nfunction partitions(items: Diagram[]): Partition[] {\n if (!items.length) return [[[]]];\n let out: Partition[] = [[[items[0]!]]];\n for (const item of items.slice(1)) out = out.flatMap(p => [\n [...p, [item]],\n [...p.slice(0, -1), [...p[p.length - 1]!, item]],\n ]);\n return out;\n}\n\nfunction rowBounds(items: Diagram[], o: ResolvedOptions): Widths {\n if (!items.length) return { min: 0, max: 0 };\n const ws = items.map(x => contentWidths(x, o));\n return { min: ws.reduce((n, x) => n + x.min, o.gap * (items.length - 1)),\n max: ws.reduce((n, x) => n + x.max, o.gap * (items.length - 1)) };\n}\n\nfunction choosePartition(items: Diagram[], width: number, depth: number, o: ResolvedOptions): Partition {\n const marker = o.continuationMarker ? o.measureText(o.continuationMarker, o.fontSize, o.fontFamily) + o.radius : 2 * o.radius;\n const score = (rows: Partition) => {\n const bs = rows.map(r => rowBounds(r, o));\n const min = Math.max(...bs.map((b, i) => b.min + (rows.length > 1 ? (i === 0 || i === rows.length - 1 ? marker : 2 * marker) : 0)));\n const max = Math.max(...bs.map(b => b.max));\n const wrapPenalty = rows.length * 10 * 2 ** (2 * depth);\n return { rows, min, max, score: wrapPenalty + Math.max(0, max - width) ** 2 };\n };\n // Exact enumeration is useful for short rows but exponential. For long\n // sequences, dynamic programming keeps one best prefix for each row count.\n const candidates = items.length <= 12 ? partitions(items).map(score) : (() => {\n const best: Array<Array<Partition | undefined>> = Array.from({ length: items.length + 1 }, () => []);\n best[0]![0] = [];\n for (let end = 1; end <= items.length; end++) {\n for (let start = 0; start < end; start++) {\n for (const prefix of best[start]!) {\n if (!prefix) continue;\n const candidate = [...prefix, items.slice(start, end)];\n const rows = candidate.length;\n const current = best[end]![rows];\n if (!current || score(candidate).score < score(current).score) best[end]![rows] = candidate;\n }\n }\n }\n return best[items.length]!.filter((rows): rows is Partition => Boolean(rows)).map(score);\n })();\n const fitting = candidates.filter(c => c.min <= width + .001);\n return (fitting.length ? fitting : candidates).sort((a, b) => a.score - b.score || a.max - b.max)[0]!.rows;\n}\n\nfunction distribute(total: number, count: number, minGap: number, policy: Justify, direction: Direction): number[] {\n if (!count) return [total];\n const innerBase = minGap * Math.max(0, count - 1), extra = Math.max(0, total - innerBase);\n const start = policy === \"start\" ? 0 : policy === \"end\" ? extra : policy === \"center\" ? extra / 2 :\n policy === \"space-around\" ? extra / (2 * count) : policy === \"space-evenly\" ? extra / (count + 1) : 0;\n const end = policy === \"start\" ? extra : policy === \"end\" ? 0 : policy === \"center\" ? extra / 2 : start;\n let inner = count <= 1 ? 0 : minGap;\n if (policy === \"space-between\" && count > 1) inner += extra / (count - 1);\n if (policy === \"space-around\") inner += extra / count;\n if (policy === \"space-evenly\") inner += extra / (count + 1);\n const rails = [start, ...Array(Math.max(0, count - 1)).fill(inner), end];\n return direction === \"rtl\" ? rails.reverse() : rails;\n}\n\nfunction layoutRow(items: Diagram[], target: number, depth: number, o: ResolvedOptions, direction: Direction): LayoutNode {\n if (!items.length) return { kind: \"empty\", width: target, height: 2, entryY: 1, exitY: 1, direction };\n const bounds = items.map(x => contentWidths(x, o));\n const base = bounds.reduce((n, x) => n + x.min, 0);\n let available = Math.max(0, target - base - o.gap * (items.length - 1));\n const growth = bounds.map(x => x.max - x.min), sumGrowth = growth.reduce((a, b) => a + b, 0);\n const grown = Math.min(available, sumGrowth);\n const widths = bounds.map((b, i) => b.min + (sumGrowth ? grown * growth[i]! / sumGrowth : 0));\n available -= grown;\n const absorbed = available * o.flexAbsorb;\n const rails = distribute(o.gap * (items.length - 1) + absorbed, items.length, o.gap, o.justify, direction);\n const leftover = available - absorbed;\n if (leftover && sumGrowth) widths.forEach((_, i) => widths[i]! += leftover * Math.max(1, bounds[i]!.max) / bounds.reduce((n, b) => n + Math.max(1, b.max), 0));\n else rails[direction === \"ltr\" ? rails.length - 1 : 0]! += leftover;\n const childNodes = items.map((d, i) => layoutRec(d, widths[i]!, depth + 1, o, direction));\n const baseline = Math.max(...childNodes.map(n => n.entryY));\n const ys = childNodes.map(n => baseline - n.entryY);\n const height = Math.max(...childNodes.map((n, i) => ys[i]! + n.height));\n const visual = direction === \"ltr\" ? childNodes.map((n, i) => ({ n, i })) : childNodes.map((n, i) => ({ n, i })).reverse();\n let x = rails[0]!;\n const children: PositionedLayout[] = [];\n visual.forEach(({n, i}, vi) => { children.push({ x, y: ys[i]!, node: n }); x += n.width + rails[vi + 1]!; });\n const first = children[0]!, last = children[children.length - 1]!;\n const start = direction === \"ltr\" ? first : last;\n const end = direction === \"ltr\" ? last : first;\n return { kind: \"row\", width: target, height, entryY: start.y + start.node.entryY,\n exitY: end.y + end.node.exitY, direction, children };\n}\n\nfunction layoutSequence(d: Extract<Diagram, {type: \"sequence\"}>, width: number, depth: number, o: ResolvedOptions, direction: Direction): LayoutNode {\n if (!d.items.length) return { ...d, kind: \"empty\", width, height: 2, entryY: 1, exitY: 1, direction };\n const rows = choosePartition(d.items, width, depth, o);\n if (rows.length === 1) return { ...layoutRow(rows[0]!, width, depth, o, direction), ...d, kind: \"row\" };\n const laid = rows.map(r => layoutRow(r, width - 4 * o.radius, depth, o, direction));\n const positioned: PositionedLayout[] = [];\n let y = 0;\n laid.forEach(n => { positioned.push({ x: 2 * o.radius, y, node: n }); y += n.height + o.rowGap; });\n const height = y - o.rowGap;\n return { ...d, kind: \"wrapped\", width, height, entryY: positioned[0]!.node.entryY,\n exitY: positioned.at(-1)!.y + positioned.at(-1)!.node.exitY, direction, rows: positioned };\n}\n\nfunction layoutRec(d: Diagram, width: number, depth: number, o: ResolvedOptions, direction: Direction): LayoutNode {\n switch (d.type) {\n case \"terminal\": case \"nonterminal\": {\n const natural = stationWidth(d, o), h = o.fontSize + 2 * o.paddingY;\n const station: LayoutNode = { ...d, kind: \"station\", terminal: d.type === \"terminal\", width: natural, height: h,\n entryY: h / 2, exitY: h / 2, direction };\n if (width <= natural + .001) return station;\n const [before] = distribute(width - natural, 1, 0, o.justify, direction);\n return { kind: \"row\", width, height: h, entryY: h / 2, exitY: h / 2, direction,\n children: [{ x: before!, y: 0, node: station }] };\n }\n case \"sequence\": return layoutSequence(d, width, depth, o, direction);\n case \"stack\": {\n const innerWidth = Math.max(0, width - 6 * o.radius);\n const top = layoutRec(d.top, innerWidth, depth + 1, o, direction);\n const polarity = d.polarity ?? \"positive\";\n const bottom = layoutRec(d.bottom, innerWidth, depth + 1, o, polarity === \"negative\" ? (direction === \"ltr\" ? \"rtl\" : \"ltr\") : direction);\n const bottomY = top.height + o.rowGap, height = bottomY + bottom.height;\n const entryY = o.align === \"top\" ? top.entryY : o.align === \"bottom\" ? bottomY + bottom.entryY :\n o.align === \"center\" ? height / 2 : (d.top.type === \"sequence\" && !d.top.items.length ? bottomY + bottom.entryY : top.entryY);\n return { ...d, kind: \"stack\", width, height, entryY, exitY: entryY, direction, polarity,\n top: { x: 3 * o.radius, y: 0, node: top }, bottom: { x: 3 * o.radius, y: bottomY, node: bottom } };\n }\n }\n}\n\nexport function layout(diagram: Diagram, options: LayoutOptions = {}): { node: LayoutNode; options: ResolvedOptions } {\n const o = resolveOptions(options), bounds = contentWidths(diagram, o);\n const width = Math.max(o.width <= 1 ? bounds.max * o.width : o.width, bounds.min);\n return { node: layoutRec(diagram, width, 0, o, o.direction), options: o };\n}\n"]}
|
package/dist/model.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type Direction = "ltr" | "rtl";
|
|
2
|
+
export type Polarity = "positive" | "negative";
|
|
3
|
+
export type Align = "top" | "center" | "bottom" | "baseline";
|
|
4
|
+
export type Justify = "start" | "end" | "center" | "space-between" | "space-around" | "space-evenly";
|
|
5
|
+
export interface Metadata {
|
|
6
|
+
id?: string;
|
|
7
|
+
className?: string;
|
|
8
|
+
label?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface Terminal extends Metadata {
|
|
11
|
+
type: "terminal";
|
|
12
|
+
text: string;
|
|
13
|
+
}
|
|
14
|
+
export interface Nonterminal extends Metadata {
|
|
15
|
+
type: "nonterminal";
|
|
16
|
+
text: string;
|
|
17
|
+
}
|
|
18
|
+
export interface Sequence extends Metadata {
|
|
19
|
+
type: "sequence";
|
|
20
|
+
items: Diagram[];
|
|
21
|
+
}
|
|
22
|
+
export interface Stack extends Metadata {
|
|
23
|
+
type: "stack";
|
|
24
|
+
top: Diagram;
|
|
25
|
+
bottom: Diagram;
|
|
26
|
+
polarity?: Polarity;
|
|
27
|
+
}
|
|
28
|
+
export type Diagram = Terminal | Nonterminal | Sequence | Stack;
|
|
29
|
+
export declare const terminal: (text: string, meta?: Metadata) => Terminal;
|
|
30
|
+
export declare const nonterminal: (text: string, meta?: Metadata) => Nonterminal;
|
|
31
|
+
export declare const sequence: (...items: Diagram[]) => Sequence;
|
|
32
|
+
export declare const choice: (top: Diagram, bottom: Diagram, meta?: Metadata) => Stack;
|
|
33
|
+
export declare const loop: (forward: Diagram, backward: Diagram, meta?: Metadata) => Stack;
|
|
34
|
+
export declare const optional: (item: Diagram, meta?: Metadata) => Stack;
|
|
35
|
+
export declare const zeroOrMore: (item: Diagram, separator?: Diagram, meta?: Metadata) => Stack;
|
|
36
|
+
export declare const oneOrMore: (item: Diagram, separator?: Diagram, meta?: Metadata) => Stack;
|
|
37
|
+
export declare function alternatives(first: Diagram, ...rest: Diagram[]): Diagram;
|
|
38
|
+
//# sourceMappingURL=model.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK,CAAC;AACtC,MAAM,MAAM,QAAQ,GAAG,UAAU,GAAG,UAAU,CAAC;AAC/C,MAAM,MAAM,KAAK,GAAG,KAAK,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,CAAC;AAC7D,MAAM,MAAM,OAAO,GAAG,OAAO,GAAG,KAAK,GAAG,QAAQ,GAAG,eAAe,GAAG,cAAc,GAAG,cAAc,CAAC;AAErG,MAAM,WAAW,QAAQ;IACvB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,QAAS,SAAQ,QAAQ;IAAG,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AAC7E,MAAM,WAAW,WAAY,SAAQ,QAAQ;IAAG,IAAI,EAAE,aAAa,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AACnF,MAAM,WAAW,QAAS,SAAQ,QAAQ;IAAG,IAAI,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,OAAO,EAAE,CAAA;CAAE;AACjF,MAAM,WAAW,KAAM,SAAQ,QAAQ;IACrC,IAAI,EAAE,OAAO,CAAC;IACd,GAAG,EAAE,OAAO,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB;AACD,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,WAAW,GAAG,QAAQ,GAAG,KAAK,CAAC;AAEhE,eAAO,MAAM,QAAQ,GAAI,MAAM,MAAM,EAAE,OAAM,QAAa,KAAG,QAAiD,CAAC;AAC/G,eAAO,MAAM,WAAW,GAAI,MAAM,MAAM,EAAE,OAAM,QAAa,KAAG,WAAuD,CAAC;AACxH,eAAO,MAAM,QAAQ,GAAI,GAAG,OAAO,OAAO,EAAE,KAAG,QAAyC,CAAC;AACzF,eAAO,MAAM,MAAM,GAAI,KAAK,OAAO,EAAE,QAAQ,OAAO,EAAE,OAAM,QAAa,KAAG,KACX,CAAC;AAClE,eAAO,MAAM,IAAI,GAAI,SAAS,OAAO,EAAE,UAAU,OAAO,EAAE,OAAM,QAAa,KAAG,KACI,CAAC;AACrF,eAAO,MAAM,QAAQ,GAAI,MAAM,OAAO,EAAE,OAAM,QAAa,KAAG,KAC9B,CAAC;AACjC,eAAO,MAAM,UAAU,GAAI,MAAM,OAAO,EAAE,YAAW,OAAoB,EAAE,OAAM,QAAa,KAAG,KAC1D,CAAC;AACxC,eAAO,MAAM,SAAS,GAAI,MAAM,OAAO,EAAE,YAAW,OAAoB,EAAE,OAAM,QAAa,KAAG,KACnE,CAAC;AAE9B,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAExE"}
|
package/dist/model.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const terminal = (text, meta = {}) => ({ type: "terminal", text, ...meta });
|
|
2
|
+
export const nonterminal = (text, meta = {}) => ({ type: "nonterminal", text, ...meta });
|
|
3
|
+
export const sequence = (...items) => ({ type: "sequence", items });
|
|
4
|
+
export const choice = (top, bottom, meta = {}) => ({ type: "stack", top, bottom, polarity: "positive", ...meta });
|
|
5
|
+
export const loop = (forward, backward, meta = {}) => ({ type: "stack", top: forward, bottom: backward, polarity: "negative", ...meta });
|
|
6
|
+
export const optional = (item, meta = {}) => choice(sequence(), item, meta);
|
|
7
|
+
export const zeroOrMore = (item, separator = sequence(), meta = {}) => optional(loop(item, separator), meta);
|
|
8
|
+
export const oneOrMore = (item, separator = sequence(), meta = {}) => loop(item, separator, meta);
|
|
9
|
+
export function alternatives(first, ...rest) {
|
|
10
|
+
return rest.reduce((top, bottom) => choice(top, bottom), first);
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=model.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model.js","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAsBA,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,IAAY,EAAE,OAAiB,EAAE,EAAY,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC;AAC/G,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,IAAY,EAAE,OAAiB,EAAE,EAAe,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC;AACxH,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,GAAG,KAAgB,EAAY,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;AACzF,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,GAAY,EAAE,MAAe,EAAE,OAAiB,EAAE,EAAS,EAAE,CAClF,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC;AAClE,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,OAAgB,EAAE,QAAiB,EAAE,OAAiB,EAAE,EAAS,EAAE,CACtF,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC;AACrF,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,IAAa,EAAE,OAAiB,EAAE,EAAS,EAAE,CACpE,MAAM,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AACjC,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,IAAa,EAAE,YAAqB,QAAQ,EAAE,EAAE,OAAiB,EAAE,EAAS,EAAE,CACvG,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,EAAE,IAAI,CAAC,CAAC;AACxC,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,IAAa,EAAE,YAAqB,QAAQ,EAAE,EAAE,OAAiB,EAAE,EAAS,EAAE,CACtG,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;AAE9B,MAAM,UAAU,YAAY,CAAC,KAAc,EAAE,GAAG,IAAe;IAC7D,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,EAAE,KAAK,CAAC,CAAC;AAClE,CAAC","sourcesContent":["export type Direction = \"ltr\" | \"rtl\";\nexport type Polarity = \"positive\" | \"negative\";\nexport type Align = \"top\" | \"center\" | \"bottom\" | \"baseline\";\nexport type Justify = \"start\" | \"end\" | \"center\" | \"space-between\" | \"space-around\" | \"space-evenly\";\n\nexport interface Metadata {\n id?: string;\n className?: string;\n label?: string;\n}\n\nexport interface Terminal extends Metadata { type: \"terminal\"; text: string }\nexport interface Nonterminal extends Metadata { type: \"nonterminal\"; text: string }\nexport interface Sequence extends Metadata { type: \"sequence\"; items: Diagram[] }\nexport interface Stack extends Metadata {\n type: \"stack\";\n top: Diagram;\n bottom: Diagram;\n polarity?: Polarity;\n}\nexport type Diagram = Terminal | Nonterminal | Sequence | Stack;\n\nexport const terminal = (text: string, meta: Metadata = {}): Terminal => ({ type: \"terminal\", text, ...meta });\nexport const nonterminal = (text: string, meta: Metadata = {}): Nonterminal => ({ type: \"nonterminal\", text, ...meta });\nexport const sequence = (...items: Diagram[]): Sequence => ({ type: \"sequence\", items });\nexport const choice = (top: Diagram, bottom: Diagram, meta: Metadata = {}): Stack =>\n ({ type: \"stack\", top, bottom, polarity: \"positive\", ...meta });\nexport const loop = (forward: Diagram, backward: Diagram, meta: Metadata = {}): Stack =>\n ({ type: \"stack\", top: forward, bottom: backward, polarity: \"negative\", ...meta });\nexport const optional = (item: Diagram, meta: Metadata = {}): Stack =>\n choice(sequence(), item, meta);\nexport const zeroOrMore = (item: Diagram, separator: Diagram = sequence(), meta: Metadata = {}): Stack =>\n optional(loop(item, separator), meta);\nexport const oneOrMore = (item: Diagram, separator: Diagram = sequence(), meta: Metadata = {}): Stack =>\n loop(item, separator, meta);\n\nexport function alternatives(first: Diagram, ...rest: Diagram[]): Diagram {\n return rest.reduce((top, bottom) => choice(top, bottom), first);\n}\n"]}
|
package/dist/ohm.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type Diagram } from "./model.js";
|
|
2
|
+
import type { LayoutOptions } from "./layout.js";
|
|
3
|
+
export interface RailroadRule {
|
|
4
|
+
name: string;
|
|
5
|
+
diagram: Diagram;
|
|
6
|
+
}
|
|
7
|
+
export interface RailroadGrammar {
|
|
8
|
+
name: string;
|
|
9
|
+
rules: RailroadRule[];
|
|
10
|
+
}
|
|
11
|
+
export interface ExpandRailroadOptions {
|
|
12
|
+
/** Safety limit for unusually deep non-recursive rule chains. */
|
|
13
|
+
maxDepth?: number;
|
|
14
|
+
/** Keep rules with two or more references as nonterminal stations. */
|
|
15
|
+
preserveSharedRules?: boolean;
|
|
16
|
+
/** Maximum approximate number of diagram nodes after expansion. */
|
|
17
|
+
maxNodes?: number;
|
|
18
|
+
}
|
|
19
|
+
/** Parse Ohm grammar source and convert every declared rule to a diagram. */
|
|
20
|
+
export declare function railroadGrammars(grammarSource: string): RailroadGrammar[];
|
|
21
|
+
/** Convert the first grammar declaration in an Ohm source string. */
|
|
22
|
+
export declare function railroadGrammar(grammarSource: string): RailroadGrammar;
|
|
23
|
+
/** Render each rule in an Ohm grammar as an independently displayable SVG. */
|
|
24
|
+
export declare function renderOhmGrammar(grammarSource: string, options?: LayoutOptions): Map<string, string>;
|
|
25
|
+
/**
|
|
26
|
+
* Inline rule applications until only terminals, built-ins, or recursive
|
|
27
|
+
* cycle-closing references remain. A rule already active on the current path
|
|
28
|
+
* is retained as a nonterminal, making recursive grammars finite.
|
|
29
|
+
*/
|
|
30
|
+
export declare function expandRailroadRule(grammar: RailroadGrammar, ruleName: string, options?: ExpandRailroadOptions): Diagram;
|
|
31
|
+
/** Render one recursively expanded rule, normally the grammar's start rule. */
|
|
32
|
+
export declare function renderOhmRuleFull(grammarSource: string, ruleName?: string, layoutOptions?: LayoutOptions, expandOptions?: ExpandRailroadOptions): string;
|
|
33
|
+
//# sourceMappingURL=ohm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ohm.d.ts","sourceRoot":"","sources":["../src/ohm.ts"],"names":[],"mappings":"AACA,OAAO,EAQL,KAAK,OAAO,EACb,MAAM,YAAY,CAAC;AAEpB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,YAAY,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,qBAAqB;IACpC,iEAAiE;IACjE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,mEAAmE;IACnE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAgFD,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAAC,aAAa,EAAE,MAAM,GAAG,eAAe,EAAE,CAIzE;AAED,qEAAqE;AACrE,wBAAgB,eAAe,CAAC,aAAa,EAAE,MAAM,GAAG,eAAe,CAItE;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,aAAa,EAAE,MAAM,EAAE,OAAO,GAAE,aAAkB,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAKxG;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,eAAe,EACxB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAqCT;AAED,+EAA+E;AAC/E,wBAAgB,iBAAiB,CAC/B,aAAa,EAAE,MAAM,EACrB,QAAQ,CAAC,EAAE,MAAM,EACjB,aAAa,GAAE,aAAkB,EACjC,aAAa,GAAE,qBAA0B,GACxC,MAAM,CAMR"}
|
package/dist/ohm.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import * as ohm from "ohm-js";
|
|
2
|
+
import { alternatives, nonterminal, oneOrMore, optional, sequence, terminal, zeroOrMore, } from "./model.js";
|
|
3
|
+
import { renderSvg } from "./svg.js";
|
|
4
|
+
const grammarSemantics = ohm.ohmGrammar.createSemantics();
|
|
5
|
+
/**
|
|
6
|
+
* Semantics over Ohm's own grammar grammar. It translates parsing expressions
|
|
7
|
+
* to the railroad diagram language while retaining each named rule.
|
|
8
|
+
*/
|
|
9
|
+
grammarSemantics.addOperation("railroad", {
|
|
10
|
+
Grammars(grammarIter) {
|
|
11
|
+
return grammarIter.children.map(child => child.railroad());
|
|
12
|
+
},
|
|
13
|
+
Grammar(name, _superGrammar, _open, rules, _close) {
|
|
14
|
+
return {
|
|
15
|
+
name: name.sourceString,
|
|
16
|
+
rules: rules.children.map(rule => rule.railroad()),
|
|
17
|
+
};
|
|
18
|
+
},
|
|
19
|
+
Rule_define(name, _formals, _description, _equals, body) {
|
|
20
|
+
return { name: name.sourceString, diagram: body.railroad() };
|
|
21
|
+
},
|
|
22
|
+
Rule_override(name, _formals, _operator, body) {
|
|
23
|
+
return { name: name.sourceString, diagram: body.railroad() };
|
|
24
|
+
},
|
|
25
|
+
Rule_extend(name, _formals, _operator, body) {
|
|
26
|
+
return { name: name.sourceString, diagram: body.railroad() };
|
|
27
|
+
},
|
|
28
|
+
RuleBody(_leadingBar, terms) {
|
|
29
|
+
const choices = terms.railroad();
|
|
30
|
+
return choices.length === 1 ? choices[0] : alternatives(choices[0], ...choices.slice(1));
|
|
31
|
+
},
|
|
32
|
+
OverrideRuleBody(_leadingBar, terms) {
|
|
33
|
+
const choices = terms.railroad();
|
|
34
|
+
return choices.length === 1 ? choices[0] : alternatives(choices[0], ...choices.slice(1));
|
|
35
|
+
},
|
|
36
|
+
TopLevelTerm_inline(expr, _caseName) { return expr.railroad(); },
|
|
37
|
+
TopLevelTerm(expr) { return expr.railroad(); },
|
|
38
|
+
OverrideTopLevelTerm_superSplice(_) { return nonterminal("super"); },
|
|
39
|
+
OverrideTopLevelTerm(expr) { return expr.railroad(); },
|
|
40
|
+
Alt(seqs) {
|
|
41
|
+
const choices = seqs.railroad();
|
|
42
|
+
return choices.length === 1 ? choices[0] : alternatives(choices[0], ...choices.slice(1));
|
|
43
|
+
},
|
|
44
|
+
Seq(exprs) {
|
|
45
|
+
const items = exprs.children.map(expr => expr.railroad());
|
|
46
|
+
return items.length === 0 ? sequence() : items.length === 1 ? items[0] : sequence(...items);
|
|
47
|
+
},
|
|
48
|
+
Iter_star(expr, _) { return zeroOrMore(expr.railroad()); },
|
|
49
|
+
Iter_plus(expr, _) { return oneOrMore(expr.railroad()); },
|
|
50
|
+
Iter_opt(expr, _) { return optional(expr.railroad()); },
|
|
51
|
+
Iter(expr) { return expr.railroad(); },
|
|
52
|
+
Pred_not(_, expr) { return sequence(terminal("not"), expr.railroad()); },
|
|
53
|
+
Pred_lookahead(_, expr) { return sequence(terminal("lookahead"), expr.railroad()); },
|
|
54
|
+
Pred(expr) { return expr.railroad(); },
|
|
55
|
+
Lex_lex(_, expr) { return expr.railroad(); },
|
|
56
|
+
Lex(expr) { return expr.railroad(); },
|
|
57
|
+
Base_application(rule, _params) { return nonterminal(rule.sourceString); },
|
|
58
|
+
Base_range(from, _, to) { return terminal(`${decodeOhmTerminal(from.sourceString)}–${decodeOhmTerminal(to.sourceString)}`); },
|
|
59
|
+
Base_terminal(expr) {
|
|
60
|
+
const value = decodeOhmTerminal(expr.sourceString);
|
|
61
|
+
return value === "" ? sequence() : terminal(value);
|
|
62
|
+
},
|
|
63
|
+
Base_paren(_open, expr, _close) { return expr.railroad(); },
|
|
64
|
+
NonemptyListOf(first, _separators, rest) {
|
|
65
|
+
return [first.railroad(), ...rest.children.map(child => child.railroad())];
|
|
66
|
+
},
|
|
67
|
+
EmptyListOf() { return []; },
|
|
68
|
+
_iter(...children) { return children.map(child => child.railroad()); },
|
|
69
|
+
});
|
|
70
|
+
function decodeOhmTerminal(source) {
|
|
71
|
+
// Ohm string terminals use the same common escapes as JSON. Code-point
|
|
72
|
+
// escapes are normalized separately because JSON does not accept \u{...}.
|
|
73
|
+
const normalized = source.replace(/\\u\{([0-9a-fA-F]+)\}/g, (_, hex) => String.fromCodePoint(Number.parseInt(hex, 16)));
|
|
74
|
+
return JSON.parse(normalized);
|
|
75
|
+
}
|
|
76
|
+
/** Parse Ohm grammar source and convert every declared rule to a diagram. */
|
|
77
|
+
export function railroadGrammars(grammarSource) {
|
|
78
|
+
const match = ohm.ohmGrammar.match(grammarSource, "Grammars");
|
|
79
|
+
if (match.failed())
|
|
80
|
+
throw new SyntaxError(match.message ?? "Invalid Ohm grammar");
|
|
81
|
+
return grammarSemantics(match).railroad();
|
|
82
|
+
}
|
|
83
|
+
/** Convert the first grammar declaration in an Ohm source string. */
|
|
84
|
+
export function railroadGrammar(grammarSource) {
|
|
85
|
+
const grammar = railroadGrammars(grammarSource)[0];
|
|
86
|
+
if (!grammar)
|
|
87
|
+
throw new SyntaxError("Expected at least one Ohm grammar declaration");
|
|
88
|
+
return grammar;
|
|
89
|
+
}
|
|
90
|
+
/** Render each rule in an Ohm grammar as an independently displayable SVG. */
|
|
91
|
+
export function renderOhmGrammar(grammarSource, options = {}) {
|
|
92
|
+
return new Map(railroadGrammar(grammarSource).rules.map(rule => [
|
|
93
|
+
rule.name,
|
|
94
|
+
renderSvg({ ...rule.diagram, label: rule.name }, options),
|
|
95
|
+
]));
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Inline rule applications until only terminals, built-ins, or recursive
|
|
99
|
+
* cycle-closing references remain. A rule already active on the current path
|
|
100
|
+
* is retained as a nonterminal, making recursive grammars finite.
|
|
101
|
+
*/
|
|
102
|
+
export function expandRailroadRule(grammar, ruleName, options = {}) {
|
|
103
|
+
const rules = new Map(grammar.rules.map(rule => [rule.name, rule.diagram]));
|
|
104
|
+
if (!rules.has(ruleName))
|
|
105
|
+
throw new Error(`Unknown rule ${JSON.stringify(ruleName)} in grammar ${JSON.stringify(grammar.name)}`);
|
|
106
|
+
const maxDepth = options.maxDepth ?? 64;
|
|
107
|
+
if (!Number.isInteger(maxDepth) || maxDepth < 1)
|
|
108
|
+
throw new RangeError("maxDepth must be a positive integer");
|
|
109
|
+
const maxNodes = options.maxNodes ?? Number.POSITIVE_INFINITY;
|
|
110
|
+
if (!(maxNodes > 0))
|
|
111
|
+
throw new RangeError("maxNodes must be positive");
|
|
112
|
+
const referenceCounts = new Map();
|
|
113
|
+
const countReferences = (diagram) => {
|
|
114
|
+
if (diagram.type === "nonterminal")
|
|
115
|
+
referenceCounts.set(diagram.text, (referenceCounts.get(diagram.text) ?? 0) + 1);
|
|
116
|
+
else if (diagram.type === "sequence")
|
|
117
|
+
diagram.items.forEach(countReferences);
|
|
118
|
+
else if (diagram.type === "stack") {
|
|
119
|
+
countReferences(diagram.top);
|
|
120
|
+
countReferences(diagram.bottom);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
grammar.rules.forEach(rule => countReferences(rule.diagram));
|
|
124
|
+
let usedNodes = 0;
|
|
125
|
+
const expand = (diagram, active, depth) => {
|
|
126
|
+
if (diagram.type === "nonterminal") {
|
|
127
|
+
const target = rules.get(diagram.text);
|
|
128
|
+
const shared = options.preserveSharedRules && (referenceCounts.get(diagram.text) ?? 0) > 1;
|
|
129
|
+
if (!target || shared || active.has(diagram.text) || depth >= maxDepth) {
|
|
130
|
+
usedNodes++;
|
|
131
|
+
return diagram;
|
|
132
|
+
}
|
|
133
|
+
const before = usedNodes;
|
|
134
|
+
const expanded = expand(target, new Set([...active, diagram.text]), depth + 1);
|
|
135
|
+
if (usedNodes <= maxNodes)
|
|
136
|
+
return expanded;
|
|
137
|
+
usedNodes = before + 1;
|
|
138
|
+
return diagram;
|
|
139
|
+
}
|
|
140
|
+
usedNodes++;
|
|
141
|
+
if (diagram.type === "sequence")
|
|
142
|
+
return { ...diagram, items: diagram.items.map(item => expand(item, active, depth)) };
|
|
143
|
+
if (diagram.type === "stack")
|
|
144
|
+
return { ...diagram, top: expand(diagram.top, active, depth), bottom: expand(diagram.bottom, active, depth) };
|
|
145
|
+
return diagram;
|
|
146
|
+
};
|
|
147
|
+
return expand(rules.get(ruleName), new Set([ruleName]), 0);
|
|
148
|
+
}
|
|
149
|
+
/** Render one recursively expanded rule, normally the grammar's start rule. */
|
|
150
|
+
export function renderOhmRuleFull(grammarSource, ruleName, layoutOptions = {}, expandOptions = {}) {
|
|
151
|
+
const grammar = railroadGrammar(grammarSource);
|
|
152
|
+
const selected = ruleName ?? grammar.rules[0]?.name;
|
|
153
|
+
if (!selected)
|
|
154
|
+
throw new Error(`Grammar ${JSON.stringify(grammar.name)} has no rules`);
|
|
155
|
+
const diagram = expandRailroadRule(grammar, selected, expandOptions);
|
|
156
|
+
return renderSvg({ ...diagram, label: selected }, layoutOptions);
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=ohm.js.map
|
package/dist/ohm.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ohm.js","sourceRoot":"","sources":["../src/ohm.ts"],"names":[],"mappings":"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","sourcesContent":["import * as ohm from \"ohm-js\";\nimport {\n alternatives,\n nonterminal,\n oneOrMore,\n optional,\n sequence,\n terminal,\n zeroOrMore,\n type Diagram,\n} from \"./model.js\";\nimport { renderSvg } from \"./svg.js\";\nimport type { LayoutOptions } from \"./layout.js\";\n\nexport interface RailroadRule {\n name: string;\n diagram: Diagram;\n}\n\nexport interface RailroadGrammar {\n name: string;\n rules: RailroadRule[];\n}\n\nexport interface ExpandRailroadOptions {\n /** Safety limit for unusually deep non-recursive rule chains. */\n maxDepth?: number;\n /** Keep rules with two or more references as nonterminal stations. */\n preserveSharedRules?: boolean;\n /** Maximum approximate number of diagram nodes after expansion. */\n maxNodes?: number;\n}\n\ntype SemanticValue = Diagram | Diagram[] | RailroadRule | RailroadGrammar | RailroadGrammar[] | string;\n\nconst grammarSemantics = ohm.ohmGrammar.createSemantics();\n\n/**\n * Semantics over Ohm's own grammar grammar. It translates parsing expressions\n * to the railroad diagram language while retaining each named rule.\n */\ngrammarSemantics.addOperation<SemanticValue>(\"railroad\", {\n Grammars(grammarIter) {\n return grammarIter.children.map(child => child.railroad()) as RailroadGrammar[];\n },\n Grammar(name, _superGrammar, _open, rules, _close) {\n return {\n name: name.sourceString,\n rules: rules.children.map(rule => rule.railroad()) as RailroadRule[],\n } satisfies RailroadGrammar;\n },\n Rule_define(name, _formals, _description, _equals, body) {\n return { name: name.sourceString, diagram: body.railroad() as Diagram } satisfies RailroadRule;\n },\n Rule_override(name, _formals, _operator, body) {\n return { name: name.sourceString, diagram: body.railroad() as Diagram } satisfies RailroadRule;\n },\n Rule_extend(name, _formals, _operator, body) {\n return { name: name.sourceString, diagram: body.railroad() as Diagram } satisfies RailroadRule;\n },\n RuleBody(_leadingBar, terms) {\n const choices = terms.railroad() as Diagram[];\n return choices.length === 1 ? choices[0]! : alternatives(choices[0]!, ...choices.slice(1));\n },\n OverrideRuleBody(_leadingBar, terms) {\n const choices = terms.railroad() as Diagram[];\n return choices.length === 1 ? choices[0]! : alternatives(choices[0]!, ...choices.slice(1));\n },\n TopLevelTerm_inline(expr, _caseName) { return expr.railroad(); },\n TopLevelTerm(expr) { return expr.railroad(); },\n OverrideTopLevelTerm_superSplice(_) { return nonterminal(\"super\"); },\n OverrideTopLevelTerm(expr) { return expr.railroad(); },\n Alt(seqs) {\n const choices = seqs.railroad() as Diagram[];\n return choices.length === 1 ? choices[0]! : alternatives(choices[0]!, ...choices.slice(1));\n },\n Seq(exprs) {\n const items = exprs.children.map(expr => expr.railroad() as Diagram);\n return items.length === 0 ? sequence() : items.length === 1 ? items[0]! : sequence(...items);\n },\n Iter_star(expr, _) { return zeroOrMore(expr.railroad() as Diagram); },\n Iter_plus(expr, _) { return oneOrMore(expr.railroad() as Diagram); },\n Iter_opt(expr, _) { return optional(expr.railroad() as Diagram); },\n Iter(expr) { return expr.railroad(); },\n Pred_not(_, expr) { return sequence(terminal(\"not\"), expr.railroad() as Diagram); },\n Pred_lookahead(_, expr) { return sequence(terminal(\"lookahead\"), expr.railroad() as Diagram); },\n Pred(expr) { return expr.railroad(); },\n Lex_lex(_, expr) { return expr.railroad(); },\n Lex(expr) { return expr.railroad(); },\n Base_application(rule, _params) { return nonterminal(rule.sourceString); },\n Base_range(from, _, to) { return terminal(`${decodeOhmTerminal(from.sourceString)}–${decodeOhmTerminal(to.sourceString)}`); },\n Base_terminal(expr) {\n const value = decodeOhmTerminal(expr.sourceString);\n return value === \"\" ? sequence() : terminal(value);\n },\n Base_paren(_open, expr, _close) { return expr.railroad(); },\n NonemptyListOf(first, _separators, rest) {\n return [first.railroad(), ...rest.children.map(child => child.railroad())] as Diagram[];\n },\n EmptyListOf() { return [] as Diagram[]; },\n _iter(...children) { return children.map(child => child.railroad()) as Diagram[]; },\n});\n\nfunction decodeOhmTerminal(source: string): string {\n // Ohm string terminals use the same common escapes as JSON. Code-point\n // escapes are normalized separately because JSON does not accept \\u{...}.\n const normalized = source.replace(/\\\\u\\{([0-9a-fA-F]+)\\}/g, (_, hex: string) =>\n String.fromCodePoint(Number.parseInt(hex, 16)));\n return JSON.parse(normalized) as string;\n}\n\n/** Parse Ohm grammar source and convert every declared rule to a diagram. */\nexport function railroadGrammars(grammarSource: string): RailroadGrammar[] {\n const match = ohm.ohmGrammar.match(grammarSource, \"Grammars\");\n if (match.failed()) throw new SyntaxError(match.message ?? \"Invalid Ohm grammar\");\n return grammarSemantics(match).railroad() as RailroadGrammar[];\n}\n\n/** Convert the first grammar declaration in an Ohm source string. */\nexport function railroadGrammar(grammarSource: string): RailroadGrammar {\n const grammar = railroadGrammars(grammarSource)[0];\n if (!grammar) throw new SyntaxError(\"Expected at least one Ohm grammar declaration\");\n return grammar;\n}\n\n/** Render each rule in an Ohm grammar as an independently displayable SVG. */\nexport function renderOhmGrammar(grammarSource: string, options: LayoutOptions = {}): Map<string, string> {\n return new Map(railroadGrammar(grammarSource).rules.map(rule => [\n rule.name,\n renderSvg({ ...rule.diagram, label: rule.name }, options),\n ]));\n}\n\n/**\n * Inline rule applications until only terminals, built-ins, or recursive\n * cycle-closing references remain. A rule already active on the current path\n * is retained as a nonterminal, making recursive grammars finite.\n */\nexport function expandRailroadRule(\n grammar: RailroadGrammar,\n ruleName: string,\n options: ExpandRailroadOptions = {},\n): Diagram {\n const rules = new Map(grammar.rules.map(rule => [rule.name, rule.diagram]));\n if (!rules.has(ruleName)) throw new Error(`Unknown rule ${JSON.stringify(ruleName)} in grammar ${JSON.stringify(grammar.name)}`);\n const maxDepth = options.maxDepth ?? 64;\n if (!Number.isInteger(maxDepth) || maxDepth < 1) throw new RangeError(\"maxDepth must be a positive integer\");\n const maxNodes = options.maxNodes ?? Number.POSITIVE_INFINITY;\n if (!(maxNodes > 0)) throw new RangeError(\"maxNodes must be positive\");\n const referenceCounts = new Map<string, number>();\n const countReferences = (diagram: Diagram): void => {\n if (diagram.type === \"nonterminal\") referenceCounts.set(diagram.text, (referenceCounts.get(diagram.text) ?? 0) + 1);\n else if (diagram.type === \"sequence\") diagram.items.forEach(countReferences);\n else if (diagram.type === \"stack\") { countReferences(diagram.top); countReferences(diagram.bottom); }\n };\n grammar.rules.forEach(rule => countReferences(rule.diagram));\n let usedNodes = 0;\n\n const expand = (diagram: Diagram, active: ReadonlySet<string>, depth: number): Diagram => {\n if (diagram.type === \"nonterminal\") {\n const target = rules.get(diagram.text);\n const shared = options.preserveSharedRules && (referenceCounts.get(diagram.text) ?? 0) > 1;\n if (!target || shared || active.has(diagram.text) || depth >= maxDepth) {\n usedNodes++;\n return diagram;\n }\n const before = usedNodes;\n const expanded = expand(target, new Set([...active, diagram.text]), depth + 1);\n if (usedNodes <= maxNodes) return expanded;\n usedNodes = before + 1;\n return diagram;\n }\n usedNodes++;\n if (diagram.type === \"sequence\") return { ...diagram, items: diagram.items.map(item => expand(item, active, depth)) };\n if (diagram.type === \"stack\") return { ...diagram, top: expand(diagram.top, active, depth), bottom: expand(diagram.bottom, active, depth) };\n return diagram;\n };\n\n return expand(rules.get(ruleName)!, new Set([ruleName]), 0);\n}\n\n/** Render one recursively expanded rule, normally the grammar's start rule. */\nexport function renderOhmRuleFull(\n grammarSource: string,\n ruleName?: string,\n layoutOptions: LayoutOptions = {},\n expandOptions: ExpandRailroadOptions = {},\n): string {\n const grammar = railroadGrammar(grammarSource);\n const selected = ruleName ?? grammar.rules[0]?.name;\n if (!selected) throw new Error(`Grammar ${JSON.stringify(grammar.name)} has no rules`);\n const diagram = expandRailroadRule(grammar, selected, expandOptions);\n return renderSvg({ ...diagram, label: selected }, layoutOptions);\n}\n"]}
|
package/dist/svg.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"svg.d.ts","sourceRoot":"","sources":["../src/svg.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2B,KAAK,aAAa,EAAwB,MAAM,aAAa,CAAC;AAChG,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AA2D1C,wBAAgB,SAAS,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,GAAE,aAAkB,GAAG,MAAM,CAS/E;AAED,eAAO,MAAM,KAAK,kBAAY,CAAC"}
|
package/dist/svg.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { layout } from "./layout.js";
|
|
2
|
+
const esc = (s) => s.replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
|
3
|
+
const n = (x) => Number(x.toFixed(3));
|
|
4
|
+
const attrs = (node, base) => {
|
|
5
|
+
const classes = ["rrd", base, node.terminal ? "rrd-terminal" : "", node.className].filter(Boolean).join(" ");
|
|
6
|
+
return `class="${esc(classes)}"${node.id ? ` id="${esc(node.id)}"` : ""}`;
|
|
7
|
+
};
|
|
8
|
+
function renderNode(node, o, layer) {
|
|
9
|
+
const r = o.radius, rail = (d) => `<path class="rrd-rail" d="${d}"/>`;
|
|
10
|
+
let body = "";
|
|
11
|
+
if (node.kind === "empty")
|
|
12
|
+
body = layer === "rails" ? rail(`M0 ${n(node.entryY)}H${n(node.width)}`) : "";
|
|
13
|
+
else if (node.kind === "station") {
|
|
14
|
+
const boxX = r, boxW = node.width - 2 * r, round = node.terminal ? r : 2;
|
|
15
|
+
body = layer === "rails"
|
|
16
|
+
? rail(`M0 ${n(node.entryY)}H${r}M${n(node.width - r)} ${n(node.exitY)}H${n(node.width)}`)
|
|
17
|
+
: `<rect x="${r}" y="0" width="${n(boxW)}" height="${n(node.height)}" rx="${round}"/>` +
|
|
18
|
+
`<text x="${n(node.width / 2)}" y="${n(node.height / 2)}">${esc(node.text ?? "")}</text>`;
|
|
19
|
+
}
|
|
20
|
+
else if (node.kind === "row") {
|
|
21
|
+
const cs = node.children ?? [];
|
|
22
|
+
const leftTipY = (p) => p.y + (node.direction === "ltr" ? p.node.entryY : p.node.exitY);
|
|
23
|
+
const rightTipY = (p) => p.y + (node.direction === "ltr" ? p.node.exitY : p.node.entryY);
|
|
24
|
+
body = layer === "rails" && cs.length ? rail(`M0 ${n(leftTipY(cs[0]))}H${n(cs[0].x)}`) : "";
|
|
25
|
+
body += cs.map((p, i) => {
|
|
26
|
+
const before = i ? cs[i - 1] : undefined;
|
|
27
|
+
const connector = layer === "rails" && before ? rail(`M${n(before.x + before.node.width)} ${n(rightTipY(before))}H${n(p.x)}`) : "";
|
|
28
|
+
return connector + `<g transform="translate(${n(p.x)} ${n(p.y)})">${renderNode(p.node, o, layer)}</g>`;
|
|
29
|
+
}).join("");
|
|
30
|
+
if (layer === "rails" && cs.length) {
|
|
31
|
+
const last = cs.at(-1);
|
|
32
|
+
body += rail(`M${n(last.x + last.node.width)} ${n(rightTipY(last))}H${n(node.width)}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else if (node.kind === "wrapped") {
|
|
36
|
+
const rows = node.rows ?? [], left = 2 * r, right = node.width - 2 * r;
|
|
37
|
+
body = rows.map(p => `<g transform="translate(${n(p.x)} ${n(p.y)})">${renderNode(p.node, o, layer)}</g>`).join("");
|
|
38
|
+
for (let i = 0; i < rows.length - 1; i++) {
|
|
39
|
+
const a = rows[i], b = rows[i + 1], ay = a.y + a.node.exitY, by = b.y + b.node.entryY;
|
|
40
|
+
if (o.continuationMarker && layer === "content")
|
|
41
|
+
body += `<text class="rrd-marker" x="${right}" y="${n(ay)}">${esc(o.continuationMarker)}</text><text class="rrd-marker" x="${left}" y="${n(by)}">${esc(o.continuationMarker)}</text>`;
|
|
42
|
+
else if (!o.continuationMarker && layer === "rails")
|
|
43
|
+
body += rail(`M${right} ${n(ay)}h${r}q${r} 0 ${r} ${r}v${n(by - ay - 2 * r)}q0 ${r} -${r} ${r}H${left}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else if (node.kind === "stack") {
|
|
47
|
+
const top = node.top, bot = node.bottom, x0 = top.x, x1 = x0 + top.node.width, ey = node.entryY;
|
|
48
|
+
const branch = (p) => {
|
|
49
|
+
const sy = p.y + p.node.entryY, ey2 = p.y + p.node.exitY;
|
|
50
|
+
return rail(`M0 ${n(ey)}H${r}Q${x0} ${n(ey)} ${x0} ${n(sy)}M${x1} ${n(ey2)}Q${n(node.width - r)} ${n(ey2)} ${n(node.width - r)} ${n(ey)}H${n(node.width)}`);
|
|
51
|
+
};
|
|
52
|
+
// Paint connectors first, so station fills mask collapsed/nested rails.
|
|
53
|
+
body = (layer === "rails" ? branch(top) + branch(bot) : "") +
|
|
54
|
+
`<g transform="translate(${x0} ${n(top.y)})">${renderNode(top.node, o, layer)}</g>` +
|
|
55
|
+
`<g transform="translate(${x0} ${n(bot.y)})">${renderNode(bot.node, o, layer)}</g>`;
|
|
56
|
+
}
|
|
57
|
+
const label = layer === "content" && node.label ? `<text class="rrd-label" x="0" y="-6">${esc(node.label)}</text>` : "";
|
|
58
|
+
const groupAttrs = layer === "content" ? attrs(node, `rrd-${node.kind}`) : `class="rrd-rail-layer"`;
|
|
59
|
+
return `<g ${groupAttrs}>${label}${body}</g>`;
|
|
60
|
+
}
|
|
61
|
+
export function renderSvg(diagram, options = {}) {
|
|
62
|
+
const result = layout(diagram, options), { node, options: o } = result, margin = Math.max(8, o.radius), labelPad = node.label ? o.fontSize + 8 : 0;
|
|
63
|
+
const width = node.width + 2 * margin, height = node.height + 2 * margin + labelPad;
|
|
64
|
+
const accessibleLabel = options.accessibleLabel ?? node.label ?? o.accessibleLabel;
|
|
65
|
+
const description = options.accessibleDescription ?? o.accessibleDescription;
|
|
66
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" class="rrd-diagram" viewBox="0 0 ${n(width)} ${n(height)}" width="${n(width)}" height="${n(height)}" role="img" aria-label="${esc(accessibleLabel)}">` +
|
|
67
|
+
`<title>${esc(accessibleLabel)}</title>${description ? `<desc>${esc(description)}</desc>` : ""}` +
|
|
68
|
+
`<style>.rrd-diagram{--rrd-stroke:#222;--rrd-fill:#fff;--rrd-terminal:#f7f7f7;color:var(--rrd-stroke);font-family:${esc(o.fontFamily)};font-size:${n(o.fontSize)}px}.rrd-rail{fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}.rrd-station rect{fill:var(--rrd-fill);stroke:currentColor;stroke-width:2}.rrd-terminal rect{fill:var(--rrd-terminal)}.rrd-station text{text-anchor:middle;dominant-baseline:central}.rrd-marker{text-anchor:middle;dominant-baseline:central}.rrd-label{font-size:.8em;dominant-baseline:auto}</style>` +
|
|
69
|
+
`<g transform="translate(${margin} ${margin + labelPad})"><g class="rrd-rails">${renderNode(node, o, "rails")}</g><g class="rrd-content">${renderNode(node, o, "content")}</g></g></svg>`;
|
|
70
|
+
}
|
|
71
|
+
export const toSVG = renderSvg;
|
|
72
|
+
//# sourceMappingURL=svg.js.map
|
package/dist/svg.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"svg.js","sourceRoot":"","sources":["../src/svg.ts"],"names":[],"mappings":"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","sourcesContent":["import { layout, type LayoutNode, type LayoutOptions, type ResolvedOptions } from \"./layout.js\";\nimport type { Diagram } from \"./model.js\";\n\nconst esc = (s: string) => s.replace(/[&<>\"']/g, c => ({\"&\":\"&\",\"<\":\"<\",\">\":\">\",'\"':\""\",\"'\":\"'\"}[c]!));\nconst n = (x: number) => Number(x.toFixed(3));\nconst attrs = (node: LayoutNode, base: string) => {\n const classes = [\"rrd\", base, node.terminal ? \"rrd-terminal\" : \"\", node.className].filter(Boolean).join(\" \");\n return `class=\"${esc(classes)}\"${node.id ? ` id=\"${esc(node.id)}\"` : \"\"}`;\n};\n\ntype RenderLayer = \"rails\" | \"content\";\n\nfunction renderNode(node: LayoutNode, o: ResolvedOptions, layer: RenderLayer): string {\n const r = o.radius, rail = (d: string) => `<path class=\"rrd-rail\" d=\"${d}\"/>`;\n let body = \"\";\n if (node.kind === \"empty\") body = layer === \"rails\" ? rail(`M0 ${n(node.entryY)}H${n(node.width)}`) : \"\";\n else if (node.kind === \"station\") {\n const boxX = r, boxW = node.width - 2 * r, round = node.terminal ? r : 2;\n body = layer === \"rails\"\n ? rail(`M0 ${n(node.entryY)}H${r}M${n(node.width-r)} ${n(node.exitY)}H${n(node.width)}`)\n : `<rect x=\"${r}\" y=\"0\" width=\"${n(boxW)}\" height=\"${n(node.height)}\" rx=\"${round}\"/>` +\n `<text x=\"${n(node.width/2)}\" y=\"${n(node.height/2)}\">${esc(node.text ?? \"\")}</text>`;\n } else if (node.kind === \"row\") {\n const cs = node.children ?? [];\n const leftTipY = (p: typeof cs[number]) => p.y + (node.direction === \"ltr\" ? p.node.entryY : p.node.exitY);\n const rightTipY = (p: typeof cs[number]) => p.y + (node.direction === \"ltr\" ? p.node.exitY : p.node.entryY);\n body = layer === \"rails\" && cs.length ? rail(`M0 ${n(leftTipY(cs[0]!))}H${n(cs[0]!.x)}`) : \"\";\n body += cs.map((p, i) => {\n const before = i ? cs[i - 1]! : undefined;\n const connector = layer === \"rails\" && before ? rail(`M${n(before.x+before.node.width)} ${n(rightTipY(before))}H${n(p.x)}`) : \"\";\n return connector + `<g transform=\"translate(${n(p.x)} ${n(p.y)})\">${renderNode(p.node,o,layer)}</g>`;\n }).join(\"\");\n if (layer === \"rails\" && cs.length) {\n const last = cs.at(-1)!;\n body += rail(`M${n(last.x+last.node.width)} ${n(rightTipY(last))}H${n(node.width)}`);\n }\n } else if (node.kind === \"wrapped\") {\n const rows = node.rows ?? [], left = 2*r, right = node.width - 2*r;\n body = rows.map(p => `<g transform=\"translate(${n(p.x)} ${n(p.y)})\">${renderNode(p.node,o,layer)}</g>`).join(\"\");\n for (let i=0;i<rows.length-1;i++) {\n const a=rows[i]!, b=rows[i+1]!, ay=a.y+a.node.exitY, by=b.y+b.node.entryY;\n if (o.continuationMarker && layer === \"content\") body += `<text class=\"rrd-marker\" x=\"${right}\" y=\"${n(ay)}\">${esc(o.continuationMarker)}</text><text class=\"rrd-marker\" x=\"${left}\" y=\"${n(by)}\">${esc(o.continuationMarker)}</text>`;\n else if (!o.continuationMarker && layer === \"rails\") body += rail(`M${right} ${n(ay)}h${r}q${r} 0 ${r} ${r}v${n(by-ay-2*r)}q0 ${r} -${r} ${r}H${left}`);\n }\n } else if (node.kind === \"stack\") {\n const top=node.top!, bot=node.bottom!, x0=top.x, x1=x0+top.node.width, ey=node.entryY;\n const branch = (p: typeof top) => {\n const sy=p.y+p.node.entryY, ey2=p.y+p.node.exitY;\n return rail(`M0 ${n(ey)}H${r}Q${x0} ${n(ey)} ${x0} ${n(sy)}M${x1} ${n(ey2)}Q${n(node.width-r)} ${n(ey2)} ${n(node.width-r)} ${n(ey)}H${n(node.width)}`);\n };\n // Paint connectors first, so station fills mask collapsed/nested rails.\n body = (layer === \"rails\" ? branch(top) + branch(bot) : \"\") +\n `<g transform=\"translate(${x0} ${n(top.y)})\">${renderNode(top.node,o,layer)}</g>`+\n `<g transform=\"translate(${x0} ${n(bot.y)})\">${renderNode(bot.node,o,layer)}</g>`;\n }\n const label = layer === \"content\" && node.label ? `<text class=\"rrd-label\" x=\"0\" y=\"-6\">${esc(node.label)}</text>` : \"\";\n const groupAttrs = layer === \"content\" ? attrs(node, `rrd-${node.kind}`) : `class=\"rrd-rail-layer\"`;\n return `<g ${groupAttrs}>${label}${body}</g>`;\n}\n\nexport function renderSvg(diagram: Diagram, options: LayoutOptions = {}): string {\n const result = layout(diagram, options), {node, options:o}=result, margin=Math.max(8,o.radius), labelPad=node.label?o.fontSize+8:0;\n const width=node.width+2*margin, height=node.height+2*margin+labelPad;\n const accessibleLabel = options.accessibleLabel ?? node.label ?? o.accessibleLabel;\n const description = options.accessibleDescription ?? o.accessibleDescription;\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"rrd-diagram\" viewBox=\"0 0 ${n(width)} ${n(height)}\" width=\"${n(width)}\" height=\"${n(height)}\" role=\"img\" aria-label=\"${esc(accessibleLabel)}\">`+\n `<title>${esc(accessibleLabel)}</title>${description ? `<desc>${esc(description)}</desc>` : \"\"}`+\n `<style>.rrd-diagram{--rrd-stroke:#222;--rrd-fill:#fff;--rrd-terminal:#f7f7f7;color:var(--rrd-stroke);font-family:${esc(o.fontFamily)};font-size:${n(o.fontSize)}px}.rrd-rail{fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}.rrd-station rect{fill:var(--rrd-fill);stroke:currentColor;stroke-width:2}.rrd-terminal rect{fill:var(--rrd-terminal)}.rrd-station text{text-anchor:middle;dominant-baseline:central}.rrd-marker{text-anchor:middle;dominant-baseline:central}.rrd-label{font-size:.8em;dominant-baseline:auto}</style>`+\n `<g transform=\"translate(${margin} ${margin+labelPad})\"><g class=\"rrd-rails\">${renderNode(node,o,\"rails\")}</g><g class=\"rrd-content\">${renderNode(node,o,\"content\")}</g></g></svg>`;\n}\n\nexport const toSVG = renderSvg;\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@marianoguerra/railroad-diagrams",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Width-aware SVG railroad diagrams based on librrd's three-pass layout algorithm",
|
|
5
|
+
"author": "Mariano Guerra",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/marianoguerra/railroad-diagrams.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://marianoguerra.github.io/railroad-diagrams/",
|
|
11
|
+
"bugs": "https://github.com/marianoguerra/railroad-diagrams/issues",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "dist/index.js",
|
|
14
|
+
"types": "dist/index.d.ts",
|
|
15
|
+
"bin": {"railroad-diagrams": "./bin/railroad-diagrams.js"},
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/index.js",
|
|
20
|
+
"default": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./ohm": {
|
|
23
|
+
"types": "./dist/ohm.d.ts",
|
|
24
|
+
"import": "./dist/ohm.js",
|
|
25
|
+
"default": "./dist/ohm.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"bin",
|
|
31
|
+
"scripts/grammar-stages.js",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE",
|
|
34
|
+
"CHANGELOG.md"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"clean": "tsc -b tsconfig.json --clean",
|
|
38
|
+
"build": "npm run clean && tsc -p tsconfig.json",
|
|
39
|
+
"test": "npm run build && node --test test/*.test.js",
|
|
40
|
+
"package:smoke": "node scripts/package-smoke.js",
|
|
41
|
+
"release:check": "npm test && npm run package:smoke",
|
|
42
|
+
"prepack": "npm test",
|
|
43
|
+
"example": "npm run build && node examples/json.js && node examples/wasmgroundup-noplang.js",
|
|
44
|
+
"wafer-diagrams": "npm run build && node examples/generate-wafer-stages.js",
|
|
45
|
+
"build:site": "npm run build && npm run example && node scripts/build-site.js"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"ohm-js": "^17.5.0",
|
|
49
|
+
"typescript": "^6.0.0"
|
|
50
|
+
},
|
|
51
|
+
"license": "MIT",
|
|
52
|
+
"keywords": ["railroad", "syntax", "diagram", "grammar", "svg", "typescript", "ohm"],
|
|
53
|
+
"engines": { "node": ">=18" },
|
|
54
|
+
"sideEffects": false,
|
|
55
|
+
"peerDependencies": { "ohm-js": "^17.5.0" },
|
|
56
|
+
"peerDependenciesMeta": { "ohm-js": { "optional": true } },
|
|
57
|
+
"publishConfig": { "access": "public", "provenance": true }
|
|
58
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {mkdir, rm, writeFile} from "node:fs/promises";
|
|
2
|
+
import {resolve} from "node:path";
|
|
3
|
+
import {expandRailroadRule, railroadGrammar, renderOhmGrammar} from "../dist/ohm.js";
|
|
4
|
+
import {renderSvg} from "../dist/index.js";
|
|
5
|
+
|
|
6
|
+
export const stageExpansionOptions = {preserveSharedRules: true, maxNodes: 1200};
|
|
7
|
+
|
|
8
|
+
/** Generate a browsable gallery from ordinary Ohm grammar sources. */
|
|
9
|
+
export async function generateGrammarStages(stages, outputRoot, {title = "Grammar stages", description, stylesheet, log = console.log} = {}) {
|
|
10
|
+
await rm(outputRoot, {recursive: true, force: true});
|
|
11
|
+
await mkdir(outputRoot, {recursive: true});
|
|
12
|
+
const indexEntries = [];
|
|
13
|
+
const manifest = [];
|
|
14
|
+
for (const stage of stages) {
|
|
15
|
+
const {name: stageName, source: sourcePath = `${stageName}.ohm`, grammarSource} = stage;
|
|
16
|
+
const grammar = railroadGrammar(grammarSource);
|
|
17
|
+
const stageDir = resolve(outputRoot, stageName);
|
|
18
|
+
await mkdir(stageDir, {recursive: true});
|
|
19
|
+
await writeFile(resolve(stageDir, "grammar.ohm"), `${grammarSource.trim()}\n`);
|
|
20
|
+
const startRule = grammar.rules[0]?.name;
|
|
21
|
+
if (!startRule) throw new Error(`Grammar ${JSON.stringify(grammar.name)} has no rules`);
|
|
22
|
+
const fullDiagram = `${startRule}.full.svg`;
|
|
23
|
+
await writeFile(resolve(stageDir, fullDiagram), renderSvg({...expandRailroadRule(grammar, startRule, stageExpansionOptions), label: startRule}, {width: 1200}));
|
|
24
|
+
const atlasRules = collectRuleAtlas(grammar, startRule);
|
|
25
|
+
await writeFile(resolve(stageDir, "overview.html"), renderAtlasPage(stageName, sourcePath, atlasRules));
|
|
26
|
+
const links = [];
|
|
27
|
+
for (const [ruleName, svg] of renderOhmGrammar(grammarSource, {width: 900})) {
|
|
28
|
+
await writeFile(resolve(stageDir, `${ruleName}.svg`), svg);
|
|
29
|
+
links.push(`<li><a href="${stageName}/${ruleName}.svg">${escapeHtml(ruleName)}</a></li>`);
|
|
30
|
+
}
|
|
31
|
+
indexEntries.push(`<section><h2>${escapeHtml(stageName)}</h2><p><code>${escapeHtml(sourcePath)}</code> · ${grammar.rules.length} rules · <a href="${stageName}/grammar.ohm">grammar</a> · <strong><a href="${stageName}/overview.html">rule atlas</a></strong> · <a href="${stageName}/${fullDiagram}">expanded ${escapeHtml(startRule)}</a></p><ul>${links.join("")}</ul></section>`);
|
|
32
|
+
manifest.push({stage: stageName, source: sourcePath, grammar: grammar.name, startRule, fullDiagram, overview: "overview.html", atlasRules: atlasRules.map(({name}) => name), expansion: stageExpansionOptions, rules: grammar.rules.map(rule => rule.name)});
|
|
33
|
+
log(`${stageName}: ${grammar.rules.length} diagrams from ${sourcePath}`);
|
|
34
|
+
}
|
|
35
|
+
await writeFile(resolve(outputRoot, "index.html"), renderIndex(title, description ?? `${stages.length} Ohm grammar stages.`, stylesheet, indexEntries));
|
|
36
|
+
await writeFile(resolve(outputRoot, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function collectRuleAtlas(grammar, startRule) {
|
|
40
|
+
const known = new Set(grammar.rules.map(rule => rule.name));
|
|
41
|
+
const queued = new Set([startRule]);
|
|
42
|
+
const queue = [startRule];
|
|
43
|
+
const atlas = [];
|
|
44
|
+
while (queue.length) {
|
|
45
|
+
const name = queue.shift();
|
|
46
|
+
const diagram = expandRailroadRule(grammar, name, stageExpansionOptions);
|
|
47
|
+
atlas.push({name, diagram});
|
|
48
|
+
for (const reference of referencedRules(diagram)) if (known.has(reference) && !queued.has(reference)) { queued.add(reference); queue.push(reference); }
|
|
49
|
+
}
|
|
50
|
+
return atlas;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function referencedRules(diagram, found = new Set()) {
|
|
54
|
+
if (diagram.type === "nonterminal") found.add(diagram.text);
|
|
55
|
+
else if (diagram.type === "sequence") diagram.items.forEach(item => referencedRules(item, found));
|
|
56
|
+
else if (diagram.type === "stack") { referencedRules(diagram.top, found); referencedRules(diagram.bottom, found); }
|
|
57
|
+
return found;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const escapeHtml = value => value.replace(/[&<>"']/g, character => ({"&":"&","<":"<",">":">",'"':""","'":"'"})[character]);
|
|
61
|
+
|
|
62
|
+
function renderIndex(title, description, stylesheet, entries) {
|
|
63
|
+
const style = stylesheet ? `<link rel="stylesheet" href="${escapeHtml(stylesheet)}">` : `<style>body{font:16px system-ui;max-width:1100px;margin:2rem auto;padding:0 1rem;color:#222}section{border-top:1px solid #ddd;padding:1rem 0}ul{columns:4;line-height:1.7}code{background:#f3f3f3;padding:.15rem .3rem}@media(max-width:700px){ul{columns:2}}</style>`;
|
|
64
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>${escapeHtml(title)}</title>${style}</head><body><main><h1>${escapeHtml(title)}</h1><p>${description}</p>${entries.join("\n")}</main></body></html>\n`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function renderAtlasPage(stageName, sourcePath, atlasRules) {
|
|
68
|
+
const navigation = atlasRules.map(({name}) => `<li><a href="#rule-${encodeURIComponent(name)}">${escapeHtml(name)}</a></li>`).join("");
|
|
69
|
+
const diagrams = atlasRules.map(({name, diagram}, index) => `<section id="rule-${encodeURIComponent(name)}"${index === 0 ? ' class="start-rule"' : ""}><h2>${escapeHtml(name)}</h2><div class="diagram">${renderSvg(diagram, {width: 1200})}</div></section>`).join("\n");
|
|
70
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>${escapeHtml(stageName)} rule atlas</title><style>*{box-sizing:border-box}body{margin:0;color:#17202a;font:15px system-ui;background:#f6f7f9}aside{position:fixed;inset:0 auto 0 0;width:230px;overflow:auto;padding:1rem;background:#fff;border-right:1px solid #ccd2d8}main{margin-left:230px;padding:1.25rem;max-width:1500px}h1{font-size:1.25rem}h2{font-family:ui-monospace,monospace}.diagram{overflow:auto;background:#fff;border:1px solid #dce1e5;border-radius:8px}.diagram svg{display:block;max-width:none}section{margin-bottom:2rem;scroll-margin-top:1rem}.start-rule{border-bottom:3px solid #788;padding-bottom:2rem}@media(max-width:800px){aside{position:static;width:auto}main{margin:0}}</style></head><body><aside><h1>${escapeHtml(stageName)}</h1><p><code>${escapeHtml(sourcePath)}</code></p><p><a href="../">All stages</a> · <a href="grammar.ohm">grammar</a></p><ol>${navigation}</ol></aside><main>${diagrams}</main></body></html>\n`;
|
|
71
|
+
}
|