@rse/specbook 1.2.3 → 1.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +124 -204
- package/dst/specbook-cmd-export.js +4 -1
- package/dst/specbook-config.js +6 -6
- package/dst/specbook-diagram.js +154 -22
- package/dst/specbook-export-ast.js +1 -1
- package/dst/specbook-export-common.js +1 -1
- package/dst/specbook-export-html.css +178 -5
- package/dst/specbook-export-html.d.ts +1 -1
- package/dst/specbook-export-html.js +304 -42
- package/dst/specbook-export-md.js +9 -9
- package/dst/specbook-export-pdf.js +37 -13
- package/dst/specbook-format-schema.d.ts +11 -1
- package/dst/specbook-format-schema.js +12 -2
- package/dst/specbook-format-spec.d.ts +1 -1
- package/dst/specbook-format-spec.js +2 -2
- package/dst/specbook-format.md +80 -9
- package/dst/specbook-format.yaml +294 -197
- package/dst/specbook-link.js +2 -2
- package/dst/specbook-parse-semantic.js +22 -22
- package/dst/specbook-parse-syntax.js +5 -5
- package/dst/specbook-parse.js +1 -1
- package/package.json +6 -6
|
@@ -28,7 +28,7 @@ const anchorPages = async (pdf) => {
|
|
|
28
28
|
};
|
|
29
29
|
/* attach a hierarchical PDF outline (the "bookmarks" of the viewer
|
|
30
30
|
side-bar) to a document, resolving the entry anchors against the
|
|
31
|
-
named destinations and hoisting the
|
|
31
|
+
named destinations and hoisting the children of an unresolvable entry */
|
|
32
32
|
const addOutline = async (doc, entries) => {
|
|
33
33
|
const { PDFName, PDFDict, PDFArray, PDFRef, PDFHexString } = await import("pdf-lib");
|
|
34
34
|
const context = doc.context;
|
|
@@ -48,8 +48,8 @@ const addOutline = async (doc, entries) => {
|
|
|
48
48
|
}
|
|
49
49
|
const resolve = (entries) => entries.flatMap((entry) => {
|
|
50
50
|
const dest = targets.get(entry.anchor);
|
|
51
|
-
const
|
|
52
|
-
return dest !== undefined ? [{ title: entry.title, dest,
|
|
51
|
+
const children = resolve(entry.children);
|
|
52
|
+
return dest !== undefined ? [{ title: entry.title, dest, children }] : children;
|
|
53
53
|
});
|
|
54
54
|
const items = resolve(entries);
|
|
55
55
|
if (items.length === 0)
|
|
@@ -59,16 +59,16 @@ const addOutline = async (doc, entries) => {
|
|
|
59
59
|
const materialize = (items, parent) => {
|
|
60
60
|
const refs = items.map(() => context.nextRef());
|
|
61
61
|
items.forEach((item, i) => {
|
|
62
|
-
const
|
|
62
|
+
const children = item.children.length > 0 ? materialize(item.children, refs[i]) : undefined;
|
|
63
63
|
context.assign(refs[i], context.obj({
|
|
64
64
|
Title: PDFHexString.fromText(item.title),
|
|
65
65
|
Parent: parent,
|
|
66
66
|
Prev: i > 0 ? refs[i - 1] : undefined,
|
|
67
67
|
Next: i < refs.length - 1 ? refs[i + 1] : undefined,
|
|
68
|
-
First:
|
|
69
|
-
Last:
|
|
68
|
+
First: children?.first,
|
|
69
|
+
Last: children?.last,
|
|
70
70
|
/* a negative count keeps the sub-tree initially collapsed */
|
|
71
|
-
Count:
|
|
71
|
+
Count: children !== undefined ? -item.children.length : undefined,
|
|
72
72
|
Dest: item.dest
|
|
73
73
|
}));
|
|
74
74
|
});
|
|
@@ -133,11 +133,36 @@ const drawBrandBar = async (doc, accent) => {
|
|
|
133
133
|
Chrome has to actually launch it and as the export preflight and the
|
|
134
134
|
renderer both ask for the very same browser */
|
|
135
135
|
let browserOptions;
|
|
136
|
-
/*
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
/* the remedy of an unavailable Playwright Chromium, shared by the
|
|
137
|
+
fallback notice and the missing-browser error, as both have to point
|
|
138
|
+
at the very same one-time command */
|
|
139
|
+
const chromiumRemedy = `run "${literal("npx playwright install chromium")}" once to download the Playwright Chromium`;
|
|
140
|
+
/* resolve the browser printing the PDF: an explicitly configured one
|
|
141
|
+
wins, then the downloaded Playwright Chromium as a plain file check,
|
|
142
|
+
while a system-installed Google Chrome is only detectable by
|
|
143
|
+
launching it */
|
|
139
144
|
const resolveBrowser = async (verbose) => {
|
|
140
145
|
const { chromium } = await import("playwright");
|
|
146
|
+
/* an explicitly configured browser is taken as an executable path
|
|
147
|
+
once it carries a path separator and as a Playwright channel
|
|
148
|
+
name otherwise, and it never falls back onto another browser, as
|
|
149
|
+
an explicit choice has to be either honored or reported */
|
|
150
|
+
const configured = process.env.SPECBOOK_BROWSER ?? "";
|
|
151
|
+
if (configured !== "") {
|
|
152
|
+
const options = /[\\/]/.test(configured) ?
|
|
153
|
+
{ executablePath: configured } : { channel: configured };
|
|
154
|
+
try {
|
|
155
|
+
const browser = await chromium.launch(options);
|
|
156
|
+
await browser.close();
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
throw new Error(`the browser "${literal(configured)}" configured by ` +
|
|
160
|
+
`"${literal("SPECBOOK_BROWSER")}" failed to launch`, { cause: err });
|
|
161
|
+
}
|
|
162
|
+
verbose(`using the browser "${literal(configured)}" configured by ` +
|
|
163
|
+
`"${literal("SPECBOOK_BROWSER")}"`);
|
|
164
|
+
return options;
|
|
165
|
+
}
|
|
141
166
|
let executable = "";
|
|
142
167
|
try {
|
|
143
168
|
executable = chromium.executablePath();
|
|
@@ -151,7 +176,7 @@ const resolveBrowser = async (verbose) => {
|
|
|
151
176
|
const chrome = await chromium.launch({ channel: "chrome" });
|
|
152
177
|
await chrome.close();
|
|
153
178
|
verbose("Playwright Chromium unavailable -- falling back to the " +
|
|
154
|
-
|
|
179
|
+
`system-installed Google Chrome (${chromiumRemedy})`, "notice");
|
|
155
180
|
return { channel: "chrome" };
|
|
156
181
|
}
|
|
157
182
|
catch {
|
|
@@ -166,8 +191,7 @@ export const requireBrowser = async (verbose) => {
|
|
|
166
191
|
const options = await browserOptions;
|
|
167
192
|
if (options === undefined)
|
|
168
193
|
throw new Error("the PDF export requires a Chromium-class browser, but neither the " +
|
|
169
|
-
|
|
170
|
-
`"${literal("npx playwright install chromium")}" once to download the Playwright Chromium`);
|
|
194
|
+
`Playwright Chromium nor a system-installed Google Chrome was found -- ${chromiumRemedy}`);
|
|
171
195
|
return options;
|
|
172
196
|
};
|
|
173
197
|
/* the bound of the ToC page number fixpoint iteration, as the page
|
|
@@ -14,7 +14,7 @@ export type SchemaObject = {
|
|
|
14
14
|
diagram?: SchemaDiagram;
|
|
15
15
|
format?: SchemaFormat;
|
|
16
16
|
props?: SchemaProperty[];
|
|
17
|
-
|
|
17
|
+
children?: SchemaObject[];
|
|
18
18
|
};
|
|
19
19
|
export type SchemaAutomaton = {
|
|
20
20
|
nodes: string;
|
|
@@ -37,6 +37,13 @@ export type SchemaDiagramCenterEdges = {
|
|
|
37
37
|
both?: string;
|
|
38
38
|
labeled?: string;
|
|
39
39
|
};
|
|
40
|
+
export type SchemaDiagramNest = {
|
|
41
|
+
properties?: string[];
|
|
42
|
+
parent?: boolean;
|
|
43
|
+
kind?: boolean;
|
|
44
|
+
layout?: "graph" | "hub" | "grid";
|
|
45
|
+
crossing?: "nodes" | "target" | "both";
|
|
46
|
+
};
|
|
40
47
|
export type SchemaDiagram = {
|
|
41
48
|
type?: "graph" | "hub" | "grid";
|
|
42
49
|
nodes?: string;
|
|
@@ -53,12 +60,15 @@ export type SchemaDiagram = {
|
|
|
53
60
|
onlyConnected?: boolean;
|
|
54
61
|
collapse?: boolean;
|
|
55
62
|
qualified?: boolean;
|
|
63
|
+
ordered?: boolean;
|
|
64
|
+
nest?: SchemaDiagramNest;
|
|
56
65
|
properties?: string[];
|
|
57
66
|
config?: Partial<GradiaConfig>;
|
|
58
67
|
};
|
|
59
68
|
export type SchemaFormat = {
|
|
60
69
|
type?: "auto" | "complex" | "concise";
|
|
61
70
|
maxTableColumns?: number;
|
|
71
|
+
maxCellHeight?: number;
|
|
62
72
|
withUnusedProps?: boolean;
|
|
63
73
|
};
|
|
64
74
|
export type SchemaProperty = {
|
|
@@ -17,7 +17,7 @@ const GradiaConfigSchemas = Object.fromEntries(Object.entries(Gradia.config).map
|
|
|
17
17
|
const SchemaDiagramConfig = v.partial(v.strictObject(GradiaConfigSchemas, (issue) => issue.expected === "never" ? "unknown Gradia rendering option" :
|
|
18
18
|
"expected a map of Gradia rendering options"));
|
|
19
19
|
/* the Valibot schemas mirroring the types above, with the object
|
|
20
|
-
kind schema referencing itself lazily for the nested
|
|
20
|
+
kind schema referencing itself lazily for the nested children */
|
|
21
21
|
const SchemaProperty = v.strictObject({
|
|
22
22
|
name: v.string(),
|
|
23
23
|
desc: v.optional(v.string()),
|
|
@@ -50,6 +50,13 @@ const SchemaDiagramCenterEdges = v.strictObject({
|
|
|
50
50
|
both: v.optional(v.string()),
|
|
51
51
|
labeled: v.optional(v.string())
|
|
52
52
|
});
|
|
53
|
+
const SchemaDiagramNest = v.strictObject({
|
|
54
|
+
properties: v.optional(v.array(v.string())),
|
|
55
|
+
parent: v.optional(v.boolean()),
|
|
56
|
+
kind: v.optional(v.boolean()),
|
|
57
|
+
layout: v.optional(v.picklist(["graph", "hub", "grid"])),
|
|
58
|
+
crossing: v.optional(v.picklist(["nodes", "target", "both"]))
|
|
59
|
+
});
|
|
53
60
|
const SchemaDiagram = v.strictObject({
|
|
54
61
|
type: v.optional(v.picklist(["graph", "hub", "grid"])),
|
|
55
62
|
nodes: v.optional(v.string()),
|
|
@@ -66,12 +73,15 @@ const SchemaDiagram = v.strictObject({
|
|
|
66
73
|
onlyConnected: v.optional(v.boolean()),
|
|
67
74
|
collapse: v.optional(v.boolean()),
|
|
68
75
|
qualified: v.optional(v.boolean()),
|
|
76
|
+
ordered: v.optional(v.boolean()),
|
|
77
|
+
nest: v.optional(SchemaDiagramNest),
|
|
69
78
|
properties: v.optional(v.array(v.string())),
|
|
70
79
|
config: v.optional(SchemaDiagramConfig)
|
|
71
80
|
});
|
|
72
81
|
const SchemaFormat = v.strictObject({
|
|
73
82
|
type: v.optional(v.picklist(["auto", "complex", "concise"])),
|
|
74
83
|
maxTableColumns: v.optional(v.pipe(v.number(), v.integer(), v.minValue(2))),
|
|
84
|
+
maxCellHeight: v.optional(v.pipe(v.number(), v.integer(), v.minValue(0))),
|
|
75
85
|
withUnusedProps: v.optional(v.boolean())
|
|
76
86
|
});
|
|
77
87
|
const SchemaObject = v.strictObject({
|
|
@@ -87,6 +97,6 @@ const SchemaObject = v.strictObject({
|
|
|
87
97
|
diagram: v.optional(SchemaDiagram),
|
|
88
98
|
format: v.optional(SchemaFormat),
|
|
89
99
|
props: v.optional(v.array(SchemaProperty)),
|
|
90
|
-
|
|
100
|
+
children: v.optional(v.array(v.lazy(() => SchemaObject)))
|
|
91
101
|
});
|
|
92
102
|
export const Schema = v.pipe(v.array(SchemaObject), v.minLength(1));
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import * as v from "valibot";
|
|
7
7
|
/* ==== Schema ==== */
|
|
8
8
|
/* the Valibot schemas mirroring the types above, with the object
|
|
9
|
-
schema referencing itself lazily for the nested
|
|
9
|
+
schema referencing itself lazily for the nested children */
|
|
10
10
|
const SpecProperty = v.object({
|
|
11
11
|
key: v.string(),
|
|
12
12
|
value: v.string(),
|
|
@@ -26,7 +26,7 @@ const SpecObject = v.object({
|
|
|
26
26
|
primary: v.optional(v.boolean()),
|
|
27
27
|
description: v.optional(SpecDescription),
|
|
28
28
|
properties: v.array(SpecProperty),
|
|
29
|
-
|
|
29
|
+
children: v.array(v.lazy(() => SpecObject))
|
|
30
30
|
});
|
|
31
31
|
const SpecArtifact = v.object({
|
|
32
32
|
created: v.date(),
|
package/dst/specbook-format.md
CHANGED
|
@@ -78,7 +78,7 @@ type SchemaObject = {
|
|
|
78
78
|
diagram?: SchemaDiagram
|
|
79
79
|
format?: SchemaFormat
|
|
80
80
|
props?: SchemaProperty[]
|
|
81
|
-
|
|
81
|
+
children?: SchemaObject[]
|
|
82
82
|
}
|
|
83
83
|
type SchemaAutomaton = {
|
|
84
84
|
nodes: string
|
|
@@ -123,6 +123,7 @@ type SchemaDiagram = {
|
|
|
123
123
|
type SchemaFormat = {
|
|
124
124
|
type?: "auto" | "complex" | "concise"
|
|
125
125
|
maxTableColumns?: number
|
|
126
|
+
maxCellHeight?: number
|
|
126
127
|
withUnusedProps?: boolean
|
|
127
128
|
}
|
|
128
129
|
type SchemaProperty = {
|
|
@@ -251,7 +252,7 @@ type SchemaGradiaConfig = Partial<{
|
|
|
251
252
|
properties allowed on objects of this kind,
|
|
252
253
|
BECAUSE they carry the structured, machine-checkable content
|
|
253
254
|
|
|
254
|
-
- `SchemaObject.
|
|
255
|
+
- `SchemaObject.children?: SchemaObject[]`:
|
|
255
256
|
object kinds allowed one level below this object (RECURSION),
|
|
256
257
|
BECAUSE the nesting defines and bounds the document structure
|
|
257
258
|
|
|
@@ -350,7 +351,7 @@ type SchemaGradiaConfig = Partial<{
|
|
|
350
351
|
|
|
351
352
|
- `SchemaDiagram.deep?: boolean`:
|
|
352
353
|
whether the references of the descendants of a node count as its own,
|
|
353
|
-
|
|
354
|
+
counted as the arity (default: `false`),
|
|
354
355
|
BECAUSE the relations between coarse objects live in their fine parts
|
|
355
356
|
|
|
356
357
|
- `SchemaDiagram.onlyConnected?: boolean`:
|
|
@@ -365,6 +366,40 @@ type SchemaGradiaConfig = Partial<{
|
|
|
365
366
|
whether every node is labeled with its kind (default: `false`),
|
|
366
367
|
BECAUSE names alone are ambiguous across multiple object kinds
|
|
367
368
|
|
|
369
|
+
- `SchemaDiagram.ordered?: boolean`:
|
|
370
|
+
whether the nodes are placed in document order instead of by the
|
|
371
|
+
layered layout, the top-level ones stacked top-down and the members
|
|
372
|
+
of a container lined up left-to-right (default: `false`),
|
|
373
|
+
BECAUSE in a coarse-to-fine specification the references then all
|
|
374
|
+
point backward, i.e. upward
|
|
375
|
+
|
|
376
|
+
- `SchemaDiagram.nest?: SchemaDiagramNest`:
|
|
377
|
+
nesting of the nodes into container nodes,
|
|
378
|
+
BECAUSE a containment drawn as a box beats one drawn as an edge
|
|
379
|
+
|
|
380
|
+
- `SchemaDiagramNest.properties?: string[]`:
|
|
381
|
+
properties whose single referenced object is the container of a node,
|
|
382
|
+
BECAUSE a `PART-OF` or `TIER` reference is a containment, not a relation
|
|
383
|
+
|
|
384
|
+
- `SchemaDiagramNest.parent?: boolean`:
|
|
385
|
+
whether the parent object is the container of a node (default: `false`),
|
|
386
|
+
BECAUSE the nesting of the objects is a containment, too
|
|
387
|
+
|
|
388
|
+
- `SchemaDiagramNest.kind?: boolean`:
|
|
389
|
+
whether a synthetic container per object kind holds the nodes
|
|
390
|
+
(default: `false`),
|
|
391
|
+
BECAUSE a kind is a grouping no object models
|
|
392
|
+
|
|
393
|
+
- `SchemaDiagramNest.layout?: "graph" | "hub" | "grid"`:
|
|
394
|
+
shape the members of every container are laid out with
|
|
395
|
+
(default: the diagram type),
|
|
396
|
+
BECAUSE the inside of a container may tell a different story than the whole
|
|
397
|
+
|
|
398
|
+
- `SchemaDiagramNest.crossing?: "nodes" | "target" | "both"`:
|
|
399
|
+
ends of a boundary-crossing edge lifted onto the containers
|
|
400
|
+
(default: `nodes`, i.e. none),
|
|
401
|
+
BECAUSE a bundle of references into a container reads as one edge onto it
|
|
402
|
+
|
|
368
403
|
- `SchemaDiagram.properties?: string[]`:
|
|
369
404
|
property names attached to the nodes as key/value annotations,
|
|
370
405
|
BECAUSE the node name alone rarely makes the nodes comparable
|
|
@@ -385,6 +420,10 @@ type SchemaGradiaConfig = Partial<{
|
|
|
385
420
|
maximum column count of the table of the kind (default: `4`),
|
|
386
421
|
BECAUSE the fixed print/PDF page width bounds the table width
|
|
387
422
|
|
|
423
|
+
- `SchemaFormat.maxCellHeight?: number`:
|
|
424
|
+
percent a table cell may tower over its row before it folds (default: `40`),
|
|
425
|
+
BECAUSE a cell of rich prose otherwise dwarfs the whole table
|
|
426
|
+
|
|
388
427
|
- `SchemaFormat.withUnusedProps?: boolean`:
|
|
389
428
|
whether unused properties still render (default: `false`),
|
|
390
429
|
BECAUSE empty entries are noise, unless the gap is the message
|
|
@@ -474,9 +513,10 @@ type SchemaGradiaConfig = Partial<{
|
|
|
474
513
|
|
|
475
514
|
The edges are derived from the `[[xxx]]` references of the node objects
|
|
476
515
|
-- from their property values only, or, with `links: all`, from their
|
|
477
|
-
descriptions as well.
|
|
478
|
-
|
|
479
|
-
|
|
516
|
+
descriptions as well. Every referenced object is lifted to its nearest
|
|
517
|
+
ancestor-or-self within the node set, so a reference onto the scenario
|
|
518
|
+
of a use case yields an edge onto the use case. With `deep: true` the
|
|
519
|
+
references of all descendants of a node object count as its own and the
|
|
480
520
|
number of references behind an edge becomes its arity, so a diagram of
|
|
481
521
|
coarse objects (like the artifacts of a whole specification) shows the
|
|
482
522
|
relations living in their fine-grained parts. With `labeled: true` an
|
|
@@ -527,6 +567,34 @@ nodes as key/value annotations (with every `[[xxx]]` reference stripped
|
|
|
527
567
|
to its target object name), and `config` passes arbitrary Gradia
|
|
528
568
|
rendering options (e.g. `grid-columns-max: 5`) through to the diagram.
|
|
529
569
|
|
|
570
|
+
`ordered` replaces the layered layout of a `graph` by the document
|
|
571
|
+
order of the objects: the top-level nodes stack top-down in that order
|
|
572
|
+
(a synthetic container at the position of its first member), while the
|
|
573
|
+
members of a container line up left-to-right in it, wrapped by the
|
|
574
|
+
Gradia option `graph-columns-max`, so the references of a coarse-to-fine
|
|
575
|
+
specification all point backward, i.e. upward or leftward.
|
|
576
|
+
|
|
577
|
+
`nest` nests the nodes into *container* nodes, drawn as boxes around
|
|
578
|
+
their members with the edges crossing the box borders: the first of its
|
|
579
|
+
`properties` carrying exactly one resolvable reference names the
|
|
580
|
+
container of a node (a container outside the node set is added to it,
|
|
581
|
+
labeled and linked like a node, and nested on its own in turn, so a
|
|
582
|
+
`NODE` sits in its `TIER` and the tier in the tier it is `PART-OF`),
|
|
583
|
+
else with `parent: true` the parent object is the container when part
|
|
584
|
+
of the node set, else with `kind: true` a synthetic container per object
|
|
585
|
+
kind is. An edge between a container and one of its own members is
|
|
586
|
+
dropped, as the nesting already shows it, `layout` selects the shape
|
|
587
|
+
the members of every container are laid out with (default: the diagram
|
|
588
|
+
type), a `hub` carries no containers, as every container would need a
|
|
589
|
+
primary node of its own, and a node nested into itself or into a cycle
|
|
590
|
+
is reported. With `crossing: target` an edge crossing a container
|
|
591
|
+
boundary ends at the outermost container of its target not enclosing
|
|
592
|
+
its source, too, and with `crossing: both` it also starts at the
|
|
593
|
+
corresponding container of its source, where the edges coinciding
|
|
594
|
+
afterwards merge into one with their arities summed, so the edges
|
|
595
|
+
inside a container stay node-to-node while the bundles of references
|
|
596
|
+
into a container become single edges onto it.
|
|
597
|
+
|
|
530
598
|
### Format
|
|
531
599
|
|
|
532
600
|
An object kind can furthermore carry a `format` field, which controls
|
|
@@ -542,7 +610,10 @@ Inside a `concise` table, unconfigured child kinds implicitly stay
|
|
|
542
610
|
an explicitly configured `type` is always honored, even a `complex`
|
|
543
611
|
rendering pressed into a cell. `maxTableColumns` (default: `4`) bounds
|
|
544
612
|
the columns of the compact table of the kind (a wider group chunks
|
|
545
|
-
its properties into embedded per-object tables),
|
|
613
|
+
its properties into embedded per-object tables), `maxCellHeight`
|
|
614
|
+
(default: `40`) is the percentage a table cell of the kind may exceed
|
|
615
|
+
the height of every other non-empty cell of its row before the HTML
|
|
616
|
+
export folds its remaining text away, and `withUnusedProps`
|
|
546
617
|
(default: `false`) unconditionally renders the defined but unused
|
|
547
618
|
properties of that kind, as property table lines or table columns. In
|
|
548
619
|
the HTML/PDF export, a property absent from an object shows an "empty
|
|
@@ -579,7 +650,7 @@ type SpecObject = {
|
|
|
579
650
|
primary?: boolean
|
|
580
651
|
description?: SpecDescription
|
|
581
652
|
properties: SpecProperty[]
|
|
582
|
-
|
|
653
|
+
children: SpecObject[]
|
|
583
654
|
}
|
|
584
655
|
type SpecDescription = {
|
|
585
656
|
description: string
|
|
@@ -653,7 +724,7 @@ type SpecProperty = {
|
|
|
653
724
|
key/value properties of the object, in canonical order,
|
|
654
725
|
BECAUSE properties carry the structured, machine-checkable content
|
|
655
726
|
|
|
656
|
-
- `SpecObject.
|
|
727
|
+
- `SpecObject.children: SpecObject[]`:
|
|
657
728
|
child objects nested one level below this object (RECURSION),
|
|
658
729
|
BECAUSE a specification is a tree, not a flat list of statements
|
|
659
730
|
|