@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.
- package/LICENSE.txt +201 -0
- package/README.md +288 -43
- package/dst/specbook-api.d.ts +23 -3
- package/dst/specbook-api.js +133 -59
- package/dst/specbook-cli.js +93 -26
- package/dst/specbook-cmd-describe.js +4 -4
- package/dst/specbook-cmd-export-schema.d.ts +8 -0
- package/dst/specbook-cmd-export-schema.js +441 -0
- package/dst/specbook-cmd-export.js +23 -6
- package/dst/specbook-cmd-init-sample.d.ts +4 -0
- package/dst/specbook-cmd-init-sample.js +322 -0
- package/dst/specbook-cmd-init.d.ts +1 -0
- package/dst/specbook-cmd-init.js +42 -10
- package/dst/specbook-cmd-lint.js +46 -48
- package/dst/specbook-cmd-query.d.ts +30 -0
- package/dst/specbook-cmd-query.js +165 -0
- package/dst/specbook-config.d.ts +9 -1
- package/dst/specbook-config.js +135 -10
- package/dst/specbook-coverage.js +2 -2
- package/dst/specbook-diagram.d.ts +7 -5
- package/dst/specbook-diagram.js +546 -143
- package/dst/specbook-export-ast.d.ts +1 -1
- package/dst/specbook-export-ast.js +11 -8
- package/dst/specbook-export-code.js +3 -3
- package/dst/specbook-export-common.d.ts +1 -1
- package/dst/specbook-export-common.js +3 -2
- package/dst/specbook-export-diagram.d.ts +1 -1
- package/dst/specbook-export-diagram.js +25 -7
- package/dst/specbook-export-html-fold.d.ts +16 -6
- package/dst/specbook-export-html-fold.js +65 -44
- package/dst/specbook-export-html-render.js +107 -26
- package/dst/specbook-export-html-search.js +18 -20
- package/dst/specbook-export-html-template.js +38 -9
- package/dst/specbook-export-html-widget.d.ts +2 -0
- package/dst/specbook-export-html-widget.js +219 -19
- package/dst/specbook-export-html.css +208 -38
- package/dst/specbook-export-html.js +96 -53
- package/dst/specbook-export-md.d.ts +2 -1
- package/dst/specbook-export-md.js +7 -8
- package/dst/specbook-export-pdf.js +1 -1
- package/dst/specbook-files.d.ts +2 -0
- package/dst/specbook-files.js +41 -0
- package/dst/specbook-format-meta.yaml +401 -0
- package/dst/specbook-format-pm.yaml +1240 -0
- package/dst/specbook-format-schema.d.ts +33 -3
- package/dst/specbook-format-schema.js +49 -3
- package/dst/specbook-format.md +427 -47
- package/dst/specbook-format.yaml +524 -228
- package/dst/specbook-ignore.js +2 -2
- package/dst/specbook-link.d.ts +4 -0
- package/dst/specbook-link.js +61 -6
- package/dst/specbook-mcp.js +103 -19
- package/dst/specbook-parse-semantic.d.ts +1 -0
- package/dst/specbook-parse-semantic.js +11 -4
- package/dst/specbook-parse-syntax.d.ts +1 -0
- package/dst/specbook-parse-syntax.js +1 -1
- package/dst/specbook-parse.d.ts +1 -1
- package/dst/specbook-parse.js +3 -3
- package/dst/specbook-project.d.ts +2 -0
- package/dst/specbook-project.js +15 -5
- package/dst/specbook-remote.d.ts +28 -0
- package/dst/specbook-remote.js +429 -0
- 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-
|
|
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
|
|
449
|
-
bar between the search tab and the
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
501
|
-
|
|
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.
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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:
|
|
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:
|
|
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
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
names
|
|
50
|
-
|
|
51
|
-
|
|
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,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
(result.config?.["
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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,
|
|
101
|
+
for (const [object, diagrams] of svgs) {
|
|
105
102
|
const level = levels.get(object) ?? 1;
|
|
106
|
-
|
|
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,
|
|
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:
|
|
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
|
-
|
|
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
|
|
80
|
-
embedded as
|
|
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
|
|
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,
|
|
120
|
-
if (
|
|
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 "" 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: "
|
|
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,
|