@rse/specbook 1.5.2 → 1.6.1

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.
Files changed (63) hide show
  1. package/LICENSE.txt +201 -0
  2. package/README.md +288 -43
  3. package/dst/specbook-api.d.ts +23 -3
  4. package/dst/specbook-api.js +133 -59
  5. package/dst/specbook-cli.js +93 -26
  6. package/dst/specbook-cmd-describe.js +4 -4
  7. package/dst/specbook-cmd-export-schema.d.ts +8 -0
  8. package/dst/specbook-cmd-export-schema.js +441 -0
  9. package/dst/specbook-cmd-export.js +23 -6
  10. package/dst/specbook-cmd-init-sample.d.ts +4 -0
  11. package/dst/specbook-cmd-init-sample.js +322 -0
  12. package/dst/specbook-cmd-init.d.ts +1 -0
  13. package/dst/specbook-cmd-init.js +42 -10
  14. package/dst/specbook-cmd-lint.js +46 -48
  15. package/dst/specbook-cmd-query.d.ts +30 -0
  16. package/dst/specbook-cmd-query.js +165 -0
  17. package/dst/specbook-config.d.ts +9 -1
  18. package/dst/specbook-config.js +135 -10
  19. package/dst/specbook-coverage.js +2 -2
  20. package/dst/specbook-diagram.d.ts +7 -5
  21. package/dst/specbook-diagram.js +546 -143
  22. package/dst/specbook-export-ast.d.ts +1 -1
  23. package/dst/specbook-export-ast.js +11 -8
  24. package/dst/specbook-export-code.js +3 -3
  25. package/dst/specbook-export-common.d.ts +1 -1
  26. package/dst/specbook-export-common.js +3 -2
  27. package/dst/specbook-export-diagram.d.ts +1 -1
  28. package/dst/specbook-export-diagram.js +25 -7
  29. package/dst/specbook-export-html-fold.d.ts +16 -6
  30. package/dst/specbook-export-html-fold.js +65 -44
  31. package/dst/specbook-export-html-render.js +107 -26
  32. package/dst/specbook-export-html-search.js +18 -20
  33. package/dst/specbook-export-html-template.js +38 -9
  34. package/dst/specbook-export-html-widget.d.ts +2 -0
  35. package/dst/specbook-export-html-widget.js +219 -19
  36. package/dst/specbook-export-html.css +208 -38
  37. package/dst/specbook-export-html.js +96 -53
  38. package/dst/specbook-export-md.d.ts +2 -1
  39. package/dst/specbook-export-md.js +7 -8
  40. package/dst/specbook-export-pdf.js +1 -1
  41. package/dst/specbook-files.d.ts +2 -0
  42. package/dst/specbook-files.js +41 -0
  43. package/dst/specbook-format-meta.yaml +401 -0
  44. package/dst/specbook-format-pm.yaml +1240 -0
  45. package/dst/specbook-format-schema.d.ts +33 -3
  46. package/dst/specbook-format-schema.js +49 -3
  47. package/dst/specbook-format.md +427 -47
  48. package/dst/specbook-format.yaml +524 -228
  49. package/dst/specbook-ignore.js +2 -2
  50. package/dst/specbook-link.d.ts +4 -0
  51. package/dst/specbook-link.js +61 -6
  52. package/dst/specbook-mcp.js +103 -19
  53. package/dst/specbook-parse-semantic.d.ts +1 -0
  54. package/dst/specbook-parse-semantic.js +11 -4
  55. package/dst/specbook-parse-syntax.d.ts +1 -0
  56. package/dst/specbook-parse-syntax.js +1 -1
  57. package/dst/specbook-parse.d.ts +1 -1
  58. package/dst/specbook-parse.js +3 -3
  59. package/dst/specbook-project.d.ts +2 -0
  60. package/dst/specbook-project.js +15 -5
  61. package/dst/specbook-remote.d.ts +28 -0
  62. package/dst/specbook-remote.js +429 -0
  63. package/package.json +32 -20
@@ -132,6 +132,16 @@ html.theme-light {
132
132
  --gradia-color-container-name: var(--theme-color-base-12);
133
133
  --gradia-color-container-box: var(--theme-color-base-31);
134
134
  --gradia-color-container-border: var(--theme-color-base-24);
135
+ --gradia-color-composite-border: var(--theme-color-base-24);
136
+ --gradia-color-node-lifeline: var(--theme-color-accent-26);
137
+ --gradia-color-node-place-name: var(--theme-color-base-12);
138
+ --gradia-color-node-place-box: var(--theme-color-base-31);
139
+ --gradia-color-node-place-border: var(--theme-color-base-24);
140
+ --gradia-color-node-pseudo-name: var(--theme-color-base-32);
141
+ --gradia-color-node-pseudo-box: var(--theme-color-accent-12);
142
+ --gradia-color-node-pseudo-border: var(--theme-color-accent-8);
143
+ --gradia-color-mark-violation: var(--theme-color-signal-12);
144
+ --gradia-color-mark-critical: var(--theme-color-accent-14);
135
145
  --gradia-color-edge-line: var(--theme-color-base-22);
136
146
  --gradia-color-edge-name: var(--theme-color-base-5);
137
147
  --gradia-color-edge-arity: var(--theme-color-base-5);
@@ -187,8 +197,18 @@ html.theme-light {
187
197
  --gradia-color-group-box: var(--theme-color-accent-1);
188
198
  --gradia-color-group-border: var(--theme-color-accent-7);
189
199
  --gradia-color-container-name: var(--theme-color-base-24);
190
- --gradia-color-container-box: var(--theme-color-base-4);
200
+ --gradia-color-container-box: var(--theme-color-base-10);
191
201
  --gradia-color-container-border: var(--theme-color-base-16);
202
+ --gradia-color-composite-border: var(--theme-color-base-16);
203
+ --gradia-color-node-lifeline: var(--theme-color-accent-7);
204
+ --gradia-color-node-place-name: var(--theme-color-base-24);
205
+ --gradia-color-node-place-box: var(--theme-color-base-10);
206
+ --gradia-color-node-place-border: var(--theme-color-base-16);
207
+ --gradia-color-node-pseudo-name: var(--theme-color-base-1);
208
+ --gradia-color-node-pseudo-box: var(--theme-color-accent-20);
209
+ --gradia-color-node-pseudo-border: var(--theme-color-accent-24);
210
+ --gradia-color-mark-violation: var(--theme-color-signal-22);
211
+ --gradia-color-mark-critical: var(--theme-color-accent-22);
192
212
  --gradia-color-edge-line: var(--theme-color-base-20);
193
213
  --gradia-color-edge-name: var(--theme-color-base-29);
194
214
  --gradia-color-edge-arity: var(--theme-color-base-29);
@@ -445,17 +465,22 @@ div.info-popup.spec a.link-compact::before {
445
465
  display: none;
446
466
  }
447
467
  }
448
- /* the folding tab (screen-only): the tab riding right of the brand
449
- bar between the search tab and the table of contents tab (as wide
450
- as those, fixed independent of the content scrolling), showing a
451
- fold icon (drawn like the search icon), which slides the
452
- controls out to the right (and back in), where the tab follows the
453
- bar when the table of contents panel shifts it aside; the controls
454
- fold and unfold all diagrams of a type, all diagrams from a nesting
455
- level on, and all over-long cell texts of the content at once and
456
- carry, like the fold icon itself, the mark colors in a rounded box
457
- while anything of their kind is folded */
458
- div.fold-switch {
468
+ /* the artifact filter tab (screen-only): the tab riding right of the
469
+ brand bar between the search tab and the folding tab (as wide as
470
+ those, fixed independent of the content scrolling), showing a
471
+ layers icon (drawn like the search icon), which slides a grid out
472
+ to the right (and back in), hanging from the top of the tab beside
473
+ the tabs below, where the tab follows the bar when the table of
474
+ contents panel shifts it aside (a small gap keeping the grid off
475
+ the tabs below, bridged beside the tab itself, as the panel clips
476
+ everything outside of it); the grid holds one row per artifact
477
+ kind -- the kind cell first, toggling the whole row, then one cell
478
+ per top-level artifact of the kind, labeled by its id -- whose
479
+ cells are sticky checkboxes, bordered lighter than the panel in
480
+ both themes to read as buttons, carrying the mark colors while enabled
481
+ (a kind cell outlined while only some of its row is), like the
482
+ filter icon itself while any artifact is */
483
+ div.filter-switch {
459
484
  position: fixed;
460
485
  top: 10.66rem;
461
486
  left: calc(0.6rem - 1px);
@@ -464,22 +489,51 @@ div.fold-switch {
464
489
  color: var(--theme-color-specbook-background);
465
490
  background-color: var(--theme-color-specbook-accent);
466
491
  transition: left 0.3s ease;
467
- display: flex;
468
- align-items: center;
469
492
  }
470
- body:has(nav.toc-panel.open) div.fold-switch {
493
+ body:has(nav.toc-panel.open) div.filter-switch {
471
494
  left: calc(31rem - 1px);
472
495
  box-shadow: 0.2rem 0 0.6rem rgba(0,0,0,0.3);
473
496
  }
497
+ div.filter-switch,
498
+ div.fold-switch {
499
+ display: flex;
500
+ align-items: center;
501
+ }
502
+ div.filter-switch.open,
503
+ div.filter-switch.open div.filter-toggle,
504
+ div.fold-switch.open,
505
+ div.fold-switch.open div.fold-toggle {
506
+ border-radius: 0;
507
+ }
508
+ div.filter-switch::after,
509
+ div.fold-switch::after {
510
+ content: "";
511
+ position: absolute;
512
+ top: 0;
513
+ bottom: 0;
514
+ left: 100%;
515
+ width: 0.4rem;
516
+ background-color: var(--theme-color-specbook-accent);
517
+ visibility: hidden;
518
+ transition: visibility 0s 0.3s;
519
+ }
520
+ div.filter-switch.open::after,
521
+ div.fold-switch.open::after {
522
+ visibility: visible;
523
+ transition-delay: 0s;
524
+ }
525
+ div.filter-switch div.filter-toggle,
474
526
  div.fold-switch div.fold-toggle {
475
527
  padding: 0.65rem 0.1rem;
476
528
  line-height: 0;
477
529
  border-radius: 0 0.3rem 0.3rem 0;
478
530
  cursor: pointer;
479
531
  }
532
+ div.filter-switch div.filter-toggle:hover,
480
533
  div.fold-switch div.fold-toggle:hover {
481
534
  background-color: var(--theme-color-specbook-accent-soft);
482
535
  }
536
+ div.filter-switch svg.filter-icon,
483
537
  div.fold-switch svg.fold-icon {
484
538
  width: 1.1rem;
485
539
  height: 1.1rem;
@@ -487,24 +541,113 @@ div.fold-switch svg.fold-icon {
487
541
  border-radius: 0.3rem;
488
542
  margin-right: 0.15rem;
489
543
  }
490
- div.fold-switch.folded svg.fold-icon {
544
+ body.filtering div.filter-switch svg.filter-icon {
491
545
  color: var(--theme-color-specbook-mark-text);
492
546
  background-color: var(--theme-color-specbook-mark-back);
493
547
  }
494
- div.fold-switch div.fold-controls {
495
- display: flex;
496
- align-items: center;
497
- gap: 0.3rem;
548
+ div.filter-switch div.filter-panel,
549
+ div.fold-switch div.fold-panel {
550
+ position: absolute;
551
+ top: 0;
552
+ left: calc(100% + 0.4rem);
553
+ width: max-content;
498
554
  overflow: hidden;
499
555
  max-width: 0;
500
- padding: 0;
501
- transition: max-width 0.3s ease, padding 0.3s ease;
556
+ border-radius: 0 0.3rem 0.3rem 0.3rem;
557
+ background-color: var(--theme-color-specbook-accent);
558
+ transition: max-width 0.3s ease;
559
+ }
560
+ body:has(nav.toc-panel.open) div.filter-switch div.filter-panel,
561
+ body:has(nav.toc-panel.open) div.fold-switch div.fold-panel {
562
+ box-shadow: 0.2rem 0 0.6rem rgba(0,0,0,0.3);
563
+ }
564
+ div.filter-switch.open div.filter-panel {
565
+ max-width: 48rem;
566
+ }
567
+ div.filter-switch div.filter-grid {
568
+ display: grid;
569
+ gap: 0.3rem;
570
+ padding: 0.7rem;
571
+ line-height: 1;
572
+ }
573
+ div.filter-switch div.filter-grid > div {
574
+ padding: 0.3rem 0.4rem;
575
+ border: 1px solid rgba(255,255,255,0.45);
576
+ border-radius: 0.3rem;
577
+ font-size: 0.8rem;
578
+ text-align: center;
579
+ white-space: nowrap;
580
+ cursor: pointer;
581
+ -webkit-user-select: none;
582
+ user-select: none;
502
583
  }
503
- div.fold-switch.open div.fold-controls {
584
+ div.filter-switch div.filter-grid > div:hover {
585
+ background-color: var(--theme-color-specbook-accent-soft);
586
+ }
587
+ div.filter-switch div.filter-grid > div.on {
588
+ color: var(--theme-color-specbook-mark-text);
589
+ background-color: var(--theme-color-specbook-mark-back);
590
+ }
591
+ div.filter-switch div.filter-kind {
592
+ grid-column: 1;
593
+ font-weight: bold;
594
+ text-align: left;
595
+ }
596
+ div.filter-switch div.filter-kind.some {
597
+ border-color: var(--theme-color-specbook-mark-back);
598
+ }
599
+ @media print {
600
+ div.filter-switch {
601
+ display: none;
602
+ }
603
+ }
604
+ /* the folding tab (screen-only): the tab riding right of the brand
605
+ bar between the artifact filter tab and the table of contents tab
606
+ (as wide as those, fixed independent of the content scrolling),
607
+ showing a fold icon (drawn like the search icon), which slides a
608
+ panel out to the right (and back in), hanging from the top of the
609
+ tab beside the tabs below exactly like the artifact filter grid
610
+ (the same gap, bridged beside the tab itself); the panel holds the
611
+ controls in two rows -- the Gradia diagram types first (the row
612
+ centered on the tab), then the diagrams as code, the images, the
613
+ listings, the nesting levels, and the cell texts -- which fold and
614
+ unfold all diagrams of a type, all diagrams from a nesting level
615
+ on, and all over-long cell texts of the content at once and carry,
616
+ like the fold icon itself, the mark colors in a rounded box while
617
+ anything of their kind is folded */
618
+ div.fold-switch {
619
+ position: fixed;
620
+ top: 13.89rem;
621
+ left: calc(0.6rem - 1px);
622
+ z-index: 30;
623
+ border-radius: 0 0.3rem 0.3rem 0;
624
+ color: var(--theme-color-specbook-background);
625
+ background-color: var(--theme-color-specbook-accent);
626
+ transition: left 0.3s ease;
627
+ }
628
+ body:has(nav.toc-panel.open) div.fold-switch {
629
+ left: calc(31rem - 1px);
630
+ box-shadow: 0.2rem 0 0.6rem rgba(0,0,0,0.3);
631
+ }
632
+ div.fold-switch.folded svg.fold-icon {
633
+ color: var(--theme-color-specbook-mark-text);
634
+ background-color: var(--theme-color-specbook-mark-back);
635
+ }
636
+ div.fold-switch.open div.fold-panel {
504
637
  max-width: 24rem;
505
- padding: 0 0.5rem 0 0.2rem;
506
638
  }
507
- div.fold-switch div.fold-controls > div {
639
+ div.fold-switch div.fold-rows {
640
+ display: flex;
641
+ flex-direction: column;
642
+ gap: 0.3rem;
643
+ padding: 0.65rem 0.5rem;
644
+ }
645
+ div.fold-switch div.fold-row {
646
+ display: flex;
647
+ align-items: center;
648
+ gap: 0.3rem;
649
+ }
650
+ div.fold-switch div.fold-row > div {
508
651
  padding: 0.25rem;
509
652
  line-height: 0;
510
653
  border-radius: 0.3rem;
@@ -512,14 +655,14 @@ div.fold-switch div.fold-controls > div {
512
655
  -webkit-user-select: none;
513
656
  user-select: none;
514
657
  }
515
- div.fold-switch div.fold-controls > div:hover {
658
+ div.fold-switch div.fold-row > div:hover {
516
659
  background-color: var(--theme-color-specbook-accent-soft);
517
660
  }
518
- div.fold-switch div.fold-controls > div.folded {
661
+ div.fold-switch div.fold-row > div.folded {
519
662
  color: var(--theme-color-specbook-mark-text);
520
663
  background-color: var(--theme-color-specbook-mark-back);
521
664
  }
522
- div.fold-switch div.fold-controls svg {
665
+ div.fold-switch div.fold-row svg {
523
666
  width: 1.1rem;
524
667
  height: 1.1rem;
525
668
  }
@@ -723,7 +866,7 @@ html.maximized {
723
866
  for 2s (toggling every 250ms) after a content reload */
724
867
  div.realtime-status {
725
868
  position: fixed;
726
- top: 17.12rem;
869
+ top: 20.35rem;
727
870
  left: calc(0.6rem - 2px);
728
871
  z-index: 30;
729
872
  border-radius: 0 0.3rem 0.3rem 0;
@@ -853,7 +996,7 @@ nav.toc-panel.open div.toc-tab {
853
996
  }
854
997
  nav.toc-panel div.toc-tab {
855
998
  position: absolute;
856
- top: 13.89rem;
999
+ top: 17.12rem;
857
1000
  left: calc(100% - 1px);
858
1001
  padding: 0.9rem 0.35rem;
859
1002
  line-height: 0;
@@ -1191,6 +1334,11 @@ table.objects th {
1191
1334
  background-color: var(--theme-color-specbook-tint);
1192
1335
  box-sizing: border-box;
1193
1336
  }
1337
+ /* the kind label of a reference stays in one piece (not broken at
1338
+ a hyphen), as the column widths account for it anyway */
1339
+ table.objects td .object-kind {
1340
+ white-space: nowrap;
1341
+ }
1194
1342
  /* the name column of every compact table at a fixed width, so that the
1195
1343
  columns of the consecutive tables align */
1196
1344
  table.objects th.object-kind {
@@ -1466,6 +1614,13 @@ div.description ul {
1466
1614
  div.description li {
1467
1615
  margin: 0.1rem 0;
1468
1616
  }
1617
+ /* the Markdown bold of the descriptions renders semi-bold (unlike the
1618
+ classed <strong> markup of the hyperlinks), so it stands out from the
1619
+ prose without the weight of the headings and object kinds */
1620
+ p.description strong:not([class]),
1621
+ div.description strong:not([class]) {
1622
+ font-weight: 600;
1623
+ }
1469
1624
  div.embedding {
1470
1625
  margin: 0.5rem 0;
1471
1626
  }
@@ -1550,6 +1705,12 @@ code {
1550
1705
  font-size: 0.89em;
1551
1706
  letter-spacing: -0.03rem;
1552
1707
  }
1708
+ /* an inline code span breaks inside an overlong word (like a long
1709
+ value expression without any space) instead of overflowing its
1710
+ table cell or text column */
1711
+ :not(pre) > code {
1712
+ overflow-wrap: anywhere;
1713
+ }
1553
1714
  /* the code listings: block-level lines on a transparent background with a
1554
1715
  grey gutter for the line numbers, drawn from a counter (in the
1555
1716
  symbol color and excluded from a selection, an empty line kept open by
@@ -1689,14 +1850,14 @@ div.titlepage div.description p + p:not(.rationale) {
1689
1850
  margin-top: 0.4rem;
1690
1851
  text-indent: 3rem;
1691
1852
  }
1692
- /* the fuzzy search tab (screen-only): the middle tab riding right of
1693
- the brand bar between the info tab and the folding
1694
- tab (as wide as those, fixed independent of the content
1695
- scrolling), showing the search icon, which slides the input field
1696
- and its clear symbol out to the right (and back in), where the tab
1697
- follows the bar when the table of contents panel shifts it aside
1698
- and the icon carries the mark colors in a rounded box while a
1699
- search filter is active, signalling the filtered content */
1853
+ /* the fuzzy search tab (screen-only): the tab riding right of the
1854
+ brand bar between the info tab and the artifact filter tab (as
1855
+ wide as those, fixed independent of the content scrolling),
1856
+ showing the search icon, which slides the input field and its clear
1857
+ symbol out to the right (and back in), where the tab follows the bar
1858
+ when the table of contents panel shifts it aside and the icon
1859
+ carries the mark colors in a rounded box while a search filter is
1860
+ active, signalling the filtered content */
1700
1861
  div.search {
1701
1862
  position: fixed;
1702
1863
  top: 7.43rem;
@@ -1805,6 +1966,15 @@ tspan.search-hit {
1805
1966
  rect.search-hit {
1806
1967
  fill: var(--theme-color-specbook-mark-back);
1807
1968
  }
1969
+ /* the artifact filter hiding (the hidden objects, the emptied tables
1970
+ and articles, and the table of contents entries, where a fold
1971
+ container inside a table cell outranks the bare class), plus the
1972
+ diagram of contents, which spans all artifacts */
1973
+ .filter-hide,
1974
+ div.fold.filter-hide,
1975
+ body.filtering nav.doc {
1976
+ display: none;
1977
+ }
1808
1978
  /* the table of contents (with page numbers for print only): all
1809
1979
  section headings, numbered like the document (three levels deep,
1810
1980
  the deeper ones unnumbered) and indented per nesting level, where
@@ -4,7 +4,7 @@
4
4
  ** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
5
5
  */
6
6
  import { Gradia } from "@rse/gradia";
7
- import { buildLinkIndex, anchorPaths, plainText } from "./specbook-link.js";
7
+ import { buildLinkIndex, anchorPaths, plainText, referenceTargets } from "./specbook-link.js";
8
8
  import { stylesheet, searchScript, isTitleObject, titleObject, documentTitle, documentLang, documentThemeStyle } from "./specbook-export-common.js";
9
9
  import { collectSchemas } from "./specbook-parse-semantic.js";
10
10
  import { renderDiagrams } from "./specbook-diagram.js";
@@ -13,7 +13,7 @@ import { renderEmbeddedDiagrams, objectLevels } from "./specbook-export-diagram.
13
13
  import { prepareListings } from "./specbook-export-code.js";
14
14
  import { specCoverage } from "./specbook-coverage.js";
15
15
  import { safe, render } from "./specbook-export-html-template.js";
16
- import { themeScript, placeholderStylesheet, realtimeScript, scrollProgressScript, maximizeScript, tocPanelScript, infoScript } from "./specbook-export-html-widget.js";
16
+ import { themeScript, placeholderStylesheet, realtimeScript, scrollProgressScript, maximizeScript, tocPanelScript, infoScript, filterScript } from "./specbook-export-html-widget.js";
17
17
  import { foldScript, omittedControls } from "./specbook-export-html-fold.js";
18
18
  import { withDocument, makeLinker, collectInfo, collectSpec, collectMembers, conciseGroup, flowChildren, groupChildren, formatDate, renderTitlePage, renderArtifact, tocEntries, renderTocPanel } from "./specbook-export-html-render.js";
19
19
  /* derive the hierarchy of the rendered object headings, skipping the
@@ -42,44 +42,40 @@ export const titlePageObject = (specification) => {
42
42
  const title = object?.properties.find((property) => property.key === "TITLE");
43
43
  return title !== undefined && title.value.trim() !== "" ? object : undefined;
44
44
  };
45
- /* pre-render the configured diagrams as embeddable SVGs, displayed
46
- at a reduced coordinate scale, as the Gradia geometry (node
47
- boxes, font sizes) is dimensioned for a stand-alone canvas and
48
- would dwarf the document text at 1:1: the scale renders the node
49
- names (30 units) at 15px, about the size of the document text, so a
50
- diagram narrower than its container is no longer blown up beyond
51
- the zoom level of the diagrams the container width caps anyway. The
52
- <style> elements are stripped off the diagrams and the union of
53
- their CSS rules is returned for the document stylesheet instead, as
54
- Gradia names a class after its declarations and hence hundreds of
55
- diagrams repeat the very same few rules (which are XML-escaped
56
- inside an SVG, but plain text inside the HTML <style> element,
57
- which no unescaped "</" of a configured value may close) */
45
+ /* pre-render the configured diagrams as embeddable SVGs at half their
46
+ coordinate scale (the node names of 30 units at about the 15px of the
47
+ document text), and strip their <style> elements, returning the union
48
+ of their CSS rules for the document stylesheet instead, as Gradia
49
+ names a class after its declarations (the rules XML-unescaped, but
50
+ with every "</" escaped, as they become plain text of the HTML <style>
51
+ element) */
58
52
  const scaledDiagrams = async (specification, config, verbose, omit) => {
59
53
  const scale = 0.5;
60
54
  const rendered = new Map();
61
55
  const rules = new Set();
62
56
  const entities = { amp: "&", lt: "<", gt: ">", quot: "\"" };
63
- for (const [object, result] of await renderDiagrams(specification, config, verbose, omit)) {
64
- const svg = result.svg.replace(/<style>([\s\S]*?)<\/style>\s*/g, (_, css) => {
65
- for (const rule of css.split("\n"))
66
- if (rule.trim() !== "")
67
- rules.add(rule.trim()
68
- .replace(/&(amp|lt|gt|quot);/g, (_, name) => entities[name])
69
- .replace(/<\//g, "<\\/"));
70
- return "";
71
- });
72
- /* a "hub" diagram is capped to the width share it would
73
- occupy on its full three-column canvas (padded by the
74
- minimum widths of the absent columns and their channels),
75
- so all hub diagrams share the zoom level of the
76
- three-column ones, the leftover space splitting evenly, as
77
- the stylesheet centers every diagram */
78
- const absent = result.columns !== undefined ? 3 - result.columns : 0;
79
- const pad = absent * ((result.config?.["size-node-width-min"] ?? Gradia.config["size-node-width-min"]) +
80
- (result.config?.["hub-channel-width-min"] ?? Gradia.config["hub-channel-width-min"]));
81
- rendered.set(object, svg.replace(/(<svg[^>]*) width="([0-9.]+)" height="([0-9.]+)"/, (_, head, w, h) => `${head} width="${Number(w) * scale}" height="${Number(h) * scale}"` +
82
- (absent > 0 ? ` style="max-width: ${(Number(w) / (Number(w) + pad) * 100).toFixed(2)}%"` : "")));
57
+ for (const [object, results] of await renderDiagrams(specification, config, verbose, omit)) {
58
+ rendered.set(object, results.map((result) => {
59
+ const svg = result.svg.replace(/<style>([\s\S]*?)<\/style>\s*/g, (_, css) => {
60
+ for (const rule of css.split("\n"))
61
+ if (rule.trim() !== "")
62
+ rules.add(rule.trim()
63
+ .replace(/&(amp|lt|gt|quot);/g, (_, name) => entities[name])
64
+ .replace(/<\//g, "<\\/"));
65
+ return "";
66
+ });
67
+ /* a "hub" diagram is capped to the width share it would
68
+ occupy on its full three-column canvas (padded by the
69
+ minimum widths of the absent columns and their channels),
70
+ so all hub diagrams share the zoom level of the
71
+ three-column ones, the leftover space splitting evenly, as
72
+ the stylesheet centers every diagram */
73
+ const absent = result.columns !== undefined ? 3 - result.columns : 0;
74
+ const pad = absent * ((result.config?.["size-node-width-min"] ?? Gradia.config["size-node-width-min"]) +
75
+ (result.config?.["hub-channel-width-min"] ?? Gradia.config["hub-channel-width-min"]));
76
+ return { type: result.type, svg: svg.replace(/(<svg[^>]*) width="([0-9.]+)" height="([0-9.]+)"/, (_, head, w, h) => `${head} width="${Number(w) * scale}" height="${Number(h) * scale}"` +
77
+ (absent > 0 ? ` style="max-width: ${(Number(w) / (Number(w) + pad) * 100).toFixed(2)}%"` : "")) };
78
+ }));
83
79
  }
84
80
  return { svgs: rendered, css: Array.from(rules).join("\n") };
85
81
  };
@@ -96,18 +92,44 @@ export const renderPlaceholder = (message) => render("Placeholder", { Placeholde
96
92
  realtime: safe(realtimeScript),
97
93
  message
98
94
  } });
99
- /* wrap the pre-rendered diagrams into their blocks, carrying the
100
- diagram type and the object tree nesting level the folding
101
- distinguishes (the top-level objects of an artifact being level 1) */
102
- const wrapDiagrams = (svgs, schemas, levels) => {
95
+ /* wrap the pre-rendered diagrams of every object into their blocks
96
+ (one per diagram, in their configured order), carrying the diagram
97
+ type and the object tree nesting level the folding distinguishes
98
+ (the top-level objects of an artifact being level 1) */
99
+ const wrapDiagrams = (svgs, levels) => {
103
100
  const blocks = new Map();
104
- for (const [object, svg] of svgs) {
101
+ for (const [object, diagrams] of svgs) {
105
102
  const level = levels.get(object) ?? 1;
106
- const type = schemas?.get(object)?.diagram?.type ?? "graph";
107
- blocks.set(object, `<div class="diagram" data-type="${type}" data-level="${level}">${svg}</div>`);
103
+ blocks.set(object, diagrams.map(({ type, svg }) => `<div class="diagram" data-type="${type}" data-level="${level}">${svg}</div>`).join(""));
108
104
  }
109
105
  return blocks;
110
106
  };
107
+ /* derive a short label out of a name: the upper-cased initials of its
108
+ words, or the first two letters of a single-word name */
109
+ const idify = (name) => {
110
+ const words = name.split(/[^\p{L}\p{N}]+/u).filter((word) => word !== "");
111
+ return (words.length === 1 ? words[0].slice(0, 2) : words.map((word) => word[0]).join("")).toUpperCase();
112
+ };
113
+ /* the rows of the artifact filter grid: one per artifact kind (in the
114
+ order of first occurrence), carrying the top-level objects of the
115
+ flow artifacts of the kind as cells, each identified by its anchor
116
+ path, labeled by its explicit anchor or parenthesized id (else by
117
+ the initials of its name, as its slugified id would be as long as
118
+ the name), and titled like its heading */
119
+ const filterRows = (artifacts, paths) => {
120
+ const rows = new Map();
121
+ for (const object of artifacts.flatMap((artifact) => artifact.objects)) {
122
+ let row = rows.get(object.kind);
123
+ if (row === undefined) {
124
+ row = { kind: object.kind, cells: [] };
125
+ rows.set(object.kind, row);
126
+ }
127
+ row.cells.push({ id: paths.get(object) ?? object.id,
128
+ label: object.anchor ?? object.paren ?? idify(plainText(object.name)),
129
+ title: (object.kind !== "" ? `${object.kind}: ` : "") + plainText(object.name) });
130
+ }
131
+ return Array.from(rows.values());
132
+ };
111
133
  /* render the entire specification into a self-contained HTML document,
112
134
  with the build-time pre-assembled stylesheet embedded inline, the
113
135
  artifact timestamps aggregated into min(Created)/max(Modified),
@@ -135,21 +157,23 @@ export const renderHtml = async (specification, config, tocPages, css, options =
135
157
  /* collect the per-document rendering state, where the description
136
158
  popup keys are filled in below, once the state is established */
137
159
  const index = buildLinkIndex(specification);
160
+ const paths = anchorPaths(index);
138
161
  const schemas = config !== undefined ? collectSchemas(specification, config) : null;
139
162
  const levels = objectLevels(specification);
140
163
  const infoKeys = new Map();
141
164
  const infoObjects = new Map();
142
165
  /* wrap the diagrams into their blocks, carrying the diagram type
143
166
  and the object tree nesting level the folding distinguishes */
144
- const diagrams = rendered !== null ? wrapDiagrams(rendered.svgs, schemas, levels) : null;
145
- /* the document language selects the smart typography quote style */
167
+ const diagrams = rendered !== null ? wrapDiagrams(rendered.svgs, levels) : null;
168
+ /* the document language selects the smart typography quote style
169
+ and the rendered rationale keyword translation */
146
170
  const lang = documentLang(specification);
147
171
  /* establish the per-document rendering state (released again
148
172
  afterwards, also on a failure of the collecting or rendering) */
149
173
  return withDocument({
150
174
  lang,
151
175
  linker: makeLinker(index),
152
- anchors: anchorPaths(index),
176
+ anchors: paths,
153
177
  members: config !== undefined ? collectMembers(config, new Map()) : null,
154
178
  schemas,
155
179
  diagrams,
@@ -194,6 +218,23 @@ export const renderHtml = async (specification, config, tocPages, css, options =
194
218
  const doc = meta !== undefined ? diagrams?.get(meta) : undefined;
195
219
  const objects = artifacts.flatMap((artifact) => artifact.objects);
196
220
  const entries = tocEntries(objects, tocPages);
221
+ /* collect the reference table of the artifact filter: per
222
+ object its parent index, its anchor path segment (the path
223
+ less the one of its parent), and the indices of the objects
224
+ its own texts reference */
225
+ const positions = new Map(index.map((node) => [node.object, node.pos]));
226
+ const links = index.map((node) => {
227
+ const path = paths.get(node.object) ?? node.object.id;
228
+ const parent = node.parent !== undefined ? paths.get(node.parent.object) : undefined;
229
+ return [node.parent?.pos ?? -1, parent !== undefined ? path.slice(parent.length + 1) : path,
230
+ referenceTargets(index, node.object).map((target) => positions.get(target) ?? -1)
231
+ .filter((pos) => pos >= 0)];
232
+ });
233
+ const rows = filterRows(artifacts, paths);
234
+ /* the client-side scripts of the widgets and the side panel of
235
+ the table of contents serve the screen only, so print (the
236
+ PDF) neither carries nor runs them */
237
+ const screen = !print;
197
238
  return render("Document", { Document: {
198
239
  title: documentTitle(specification).title,
199
240
  lang,
@@ -202,17 +243,19 @@ export const renderHtml = async (specification, config, tocPages, css, options =
202
243
  themescript: safe(themeScript),
203
244
  titlepage: title !== undefined ?
204
245
  safe(renderTitlePage(title, formatDate(created), formatDate(modified))) : "",
205
- search: safe(searchScript()),
206
- progress: safe(scrollProgressScript),
246
+ search: screen ? safe(searchScript()) : "",
247
+ filtergrid: { rows, columns: 1 + Math.max(0, ...rows.map((row) => row.cells.length)) },
248
+ filter: screen ? safe(filterScript(links)) : "",
249
+ progress: screen ? safe(scrollProgressScript) : "",
207
250
  omitted,
208
- fold: safe(foldScript(omit.size > 0, omitted.text)),
209
- maximize: safe(maximizeScript),
210
- info: info !== null ? safe(infoScript(info, spec)) : "",
211
- realtime: options.realtime === true ? safe(realtimeScript) : "",
251
+ fold: screen ? safe(foldScript(omit.size > 0, omitted.text)) : "",
252
+ maximize: screen ? safe(maximizeScript) : "",
253
+ info: screen && info !== null ? safe(infoScript(info, spec)) : "",
254
+ realtime: screen && options.realtime === true ? safe(realtimeScript) : "",
212
255
  toc: entries.length > 0 ? safe(render("Toc", { Toc: { entries } })) : "",
213
- tocpanel: entries.length > 0 ?
256
+ tocpanel: screen && entries.length > 0 ?
214
257
  safe(renderTocPanel(objects, title !== undefined, doc !== undefined)) : "",
215
- tocscript: entries.length > 0 ? safe(tocPanelScript) : "",
258
+ tocscript: screen && entries.length > 0 ? safe(tocPanelScript) : "",
216
259
  doc: doc !== undefined ? safe(render("Doc", { Doc: { diagram: safe(doc) } })) : "",
217
260
  artifacts: safe(artifacts.map((artifact) => renderArtifact(artifact)).join(""))
218
261
  } });
@@ -1,7 +1,8 @@
1
- import type { Spec, SpecArtifact } from "./specbook-format-spec.js";
1
+ import type { Spec, SpecArtifact, SpecObject } from "./specbook-format-spec.js";
2
2
  import type { Schema } from "./specbook-format-schema.js";
3
3
  import type { Verbose } from "./specbook-verbose.js";
4
4
  export declare const formatTimestamp: (date: Date) => string;
5
+ export declare const renderObjectMd: (object: SpecObject, level: number, diagrams?: Map<SpecObject, string[]>) => string;
5
6
  export interface MarkdownRebase {
6
7
  origins: Map<SpecArtifact, string>;
7
8
  dir: string;
@@ -76,14 +76,13 @@ const renderConciseMd = (object, depth = 0) => {
76
76
  a description no concise item can carry, so all children (as siblings
77
77
  have to share the format) stay in the Complex Format down to heading
78
78
  level 6, the deepest one Markdown knows, below which the children are
79
- concise regardless -- and the optionally derived Gradia diagram spec
80
- embedded as a "gradia" fenced code block below the heading */
81
- const renderObjectMd = (object, level, diagrams) => {
79
+ concise regardless -- and the optionally derived Gradia diagram specs
80
+ embedded as "gradia" fenced code blocks below the heading */
81
+ export const renderObjectMd = (object, level, diagrams) => {
82
82
  const heading = `${"#".repeat(level)}${" ".repeat(Math.max(1, 4 - level))}` +
83
83
  `${object.kind}: ${object.name}${nameSuffixMd(object)}`;
84
84
  const parts = [heading];
85
- const spec = diagrams?.get(object);
86
- if (spec !== undefined)
85
+ for (const spec of diagrams?.get(object) ?? [])
87
86
  parts.push(`\`\`\`gradia\n${spec}\`\`\``);
88
87
  if (object.properties.length > 0)
89
88
  parts.push(renderKeyValuesMd(object.properties));
@@ -116,9 +115,9 @@ export const renderMarkdown = async (specification, config, rebase, verbose) =>
116
115
  the title object is reserved for the HTML/PDF export) */
117
116
  const diagrams = new Map();
118
117
  if (config !== undefined)
119
- for (const [object, result] of specDiagrams(specification, config))
120
- if (result.spec !== undefined && !isTitleObject(object))
121
- diagrams.set(object, result.spec);
118
+ for (const [object, results] of specDiagrams(specification, config))
119
+ if (!isTitleObject(object))
120
+ diagrams.set(object, results.flatMap((result) => result.spec !== undefined ? [result.spec] : []));
122
121
  /* embed the images of a text, optimized like the ones of the HTML
123
122
  export: an embeddable "![alt](file)" becomes the reference-style
124
123
  "![alt][img-N]", whose "[img-N]: data:..." definition ends the
@@ -220,7 +220,7 @@ export const htmlToPdf = async (renderHtmlPass, heading, outline, titlePage, ver
220
220
  const page = await browser.newPage();
221
221
  /* render an HTML document into a paginated PDF */
222
222
  const renderPdf = async (html, options = {}) => {
223
- await page.setContent(html, { waitUntil: "networkidle" });
223
+ await page.setContent(html, { waitUntil: "load", timeout: 0 });
224
224
  await page.evaluate(() => document.fonts.ready);
225
225
  return page.pdf({
226
226
  format: paper,
@@ -0,0 +1,2 @@
1
+ export declare const specFiles: (basedir: string) => string[];
2
+ export declare const isBelow: (file: string, dir: string) => boolean;