@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.
@@ -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 childs of an unresolvable entry */
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 childs = resolve(entry.childs);
52
- return dest !== undefined ? [{ title: entry.title, dest, childs }] : childs;
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 childs = item.childs.length > 0 ? materialize(item.childs, refs[i]) : undefined;
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: childs?.first,
69
- Last: childs?.last,
68
+ First: children?.first,
69
+ Last: children?.last,
70
70
  /* a negative count keeps the sub-tree initially collapsed */
71
- Count: childs !== undefined ? -item.childs.length : undefined,
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
- /* resolve the browser printing the PDF: the downloaded Playwright
137
- Chromium is a plain file check, while a system-installed Google
138
- Chrome is only detectable by launching it */
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
- "system-installed Google Chrome", "notice");
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
- "Playwright Chromium nor a system-installed Google Chrome was found -- run " +
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
- childs?: SchemaObject[];
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 childs */
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
- childs: v.optional(v.array(v.lazy(() => SchemaObject)))
100
+ children: v.optional(v.array(v.lazy(() => SchemaObject)))
91
101
  });
92
102
  export const Schema = v.pipe(v.array(SchemaObject), v.minLength(1));
@@ -16,7 +16,7 @@ export type SpecObject = {
16
16
  primary?: boolean;
17
17
  description?: SpecDescription;
18
18
  properties: SpecProperty[];
19
- childs: SpecObject[];
19
+ children: SpecObject[];
20
20
  };
21
21
  export type SpecDescription = {
22
22
  description: string;
@@ -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 childs */
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
- childs: v.array(v.lazy(() => SpecObject))
29
+ children: v.array(v.lazy(() => SpecObject))
30
30
  });
31
31
  const SpecArtifact = v.object({
32
32
  created: v.date(),
@@ -78,7 +78,7 @@ type SchemaObject = {
78
78
  diagram?: SchemaDiagram
79
79
  format?: SchemaFormat
80
80
  props?: SchemaProperty[]
81
- childs?: SchemaObject[]
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.childs?: 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
- lifted to their nearest node and counted as the arity (default: `false`),
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. With `deep: true` the references of all
478
- descendants of a node object count as its own, every referenced object
479
- is lifted to its nearest ancestor-or-self within the node set, and the
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), and `withUnusedProps`
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
- childs: SpecObject[]
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.childs: 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