@rse/specbook 1.2.8 → 1.2.9
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 +1 -0
- package/dst/specbook-export-html-search.js +17 -0
- package/dst/specbook-export-html.css +82 -0
- package/dst/specbook-export-html.js +106 -0
- package/dst/specbook-format.md +1 -0
- package/dst/specbook-format.yaml +28 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -86,6 +86,7 @@ CLI with commands `specbook <xxx>`, and an MCP service with tools
|
|
|
86
86
|
reference coverage tables (e.g. the share of the use cases, scenarios,
|
|
87
87
|
requirements, and rules the test cases verify), a
|
|
88
88
|
scroll progress meter (which also scrolls back to the top on click),
|
|
89
|
+
diagram maximization (into the viewport or the fullscreen),
|
|
89
90
|
description popups (which show the schema and corpus description of
|
|
90
91
|
an object kind, property, or object instance on hover), and a
|
|
91
92
|
light/dark theme toggle. It is intended for the day-to-day online
|
|
@@ -74,6 +74,23 @@
|
|
|
74
74
|
units.push({ el, text, words, keep: false })
|
|
75
75
|
})
|
|
76
76
|
})
|
|
77
|
+
|
|
78
|
+
/* let a hyperlink whose target the filtering hid leave the
|
|
79
|
+
search mode (like the Escape key) before it jumps: the click
|
|
80
|
+
is intercepted, the document revealed again, and the click
|
|
81
|
+
re-issued, so the browser performs the regular jump */
|
|
82
|
+
document.querySelectorAll("a[href^='#']").forEach((link) => {
|
|
83
|
+
link.addEventListener("click", (event) => {
|
|
84
|
+
const target = document.getElementById(
|
|
85
|
+
decodeURIComponent((link.getAttribute("href") ?? "").slice(1)))
|
|
86
|
+
if (target === null || target.closest(".search-hide") === null)
|
|
87
|
+
return
|
|
88
|
+
event.preventDefault()
|
|
89
|
+
reset()
|
|
90
|
+
slide(false)
|
|
91
|
+
link.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }))
|
|
92
|
+
})
|
|
93
|
+
})
|
|
77
94
|
}
|
|
78
95
|
|
|
79
96
|
/* the Levenshtein edit distance of two words */
|
|
@@ -574,6 +574,87 @@ span.fold-label svg {
|
|
|
574
574
|
display: none;
|
|
575
575
|
}
|
|
576
576
|
}
|
|
577
|
+
/* the maximization of the diagrams (screen-only): every diagram
|
|
578
|
+
carries two controls at its top right corner (drawn like the fold
|
|
579
|
+
chevron of an over-long cell, in rounded boxes on the page
|
|
580
|
+
background), faded in while the diagram is hovered, which maximize
|
|
581
|
+
the diagram temporarily in the shared overlay covering the whole
|
|
582
|
+
viewport (or, taken into the browser fullscreen, the whole screen)
|
|
583
|
+
on the page background, where the cloned diagram scales to fit the
|
|
584
|
+
padded area (aspect ratio kept, centered) and a close mark sits at
|
|
585
|
+
the top right corner, while the document beneath stops scrolling */
|
|
586
|
+
span.maximize-controls {
|
|
587
|
+
position: absolute;
|
|
588
|
+
top: 0.35rem;
|
|
589
|
+
right: 0.35rem;
|
|
590
|
+
display: flex;
|
|
591
|
+
gap: 0.25rem;
|
|
592
|
+
line-height: 0;
|
|
593
|
+
opacity: 0;
|
|
594
|
+
transition: opacity 0.2s ease;
|
|
595
|
+
}
|
|
596
|
+
div.diagram:hover span.maximize-controls {
|
|
597
|
+
opacity: 1;
|
|
598
|
+
}
|
|
599
|
+
span.maximize-controls > span,
|
|
600
|
+
span.maximize-close {
|
|
601
|
+
display: inline-block;
|
|
602
|
+
padding: 0.2rem;
|
|
603
|
+
line-height: 0;
|
|
604
|
+
color: var(--theme-color-specbook-symbol);
|
|
605
|
+
background-color: var(--theme-color-specbook-background);
|
|
606
|
+
border: 1px solid var(--theme-color-specbook-border);
|
|
607
|
+
border-radius: 0.25rem;
|
|
608
|
+
cursor: pointer;
|
|
609
|
+
-webkit-user-select: none;
|
|
610
|
+
user-select: none;
|
|
611
|
+
}
|
|
612
|
+
span.maximize-controls > span:hover,
|
|
613
|
+
span.maximize-close:hover {
|
|
614
|
+
color: var(--theme-color-specbook-accent);
|
|
615
|
+
border-color: var(--theme-color-specbook-accent);
|
|
616
|
+
}
|
|
617
|
+
span.maximize-controls svg,
|
|
618
|
+
span.maximize-close svg {
|
|
619
|
+
width: 1rem;
|
|
620
|
+
height: 1rem;
|
|
621
|
+
}
|
|
622
|
+
div.maximize {
|
|
623
|
+
display: none;
|
|
624
|
+
position: fixed;
|
|
625
|
+
inset: 0;
|
|
626
|
+
z-index: 40;
|
|
627
|
+
box-sizing: border-box;
|
|
628
|
+
padding: 2rem;
|
|
629
|
+
background-color: var(--theme-color-specbook-background);
|
|
630
|
+
}
|
|
631
|
+
div.maximize.open {
|
|
632
|
+
display: block;
|
|
633
|
+
}
|
|
634
|
+
div.maximize span.maximize-close {
|
|
635
|
+
position: absolute;
|
|
636
|
+
top: 0.6rem;
|
|
637
|
+
right: 0.6rem;
|
|
638
|
+
}
|
|
639
|
+
div.maximize div.maximize-content {
|
|
640
|
+
width: 100%;
|
|
641
|
+
height: 100%;
|
|
642
|
+
}
|
|
643
|
+
div.maximize div.maximize-content svg {
|
|
644
|
+
display: block;
|
|
645
|
+
width: 100%;
|
|
646
|
+
height: 100%;
|
|
647
|
+
max-width: none;
|
|
648
|
+
}
|
|
649
|
+
html.maximized {
|
|
650
|
+
overflow: hidden;
|
|
651
|
+
}
|
|
652
|
+
@media print {
|
|
653
|
+
span.maximize-controls,
|
|
654
|
+
div.maximize {
|
|
655
|
+
display: none;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
577
658
|
/* the connection status tab of the live preview (screen-only): the
|
|
578
659
|
bottommost tab riding right of the brand bar below the table of
|
|
579
660
|
contents tab (sized like the search tab, fixed independent of the
|
|
@@ -1204,6 +1285,7 @@ span.theme-dark-only {
|
|
|
1204
1285
|
}
|
|
1205
1286
|
/* the diagrams, rationales, keywords, and code spans */
|
|
1206
1287
|
div.diagram {
|
|
1288
|
+
position: relative;
|
|
1207
1289
|
margin: 1rem 0;
|
|
1208
1290
|
}
|
|
1209
1291
|
div.diagram svg {
|
|
@@ -60,6 +60,7 @@ const templates = {
|
|
|
60
60
|
{% if Document.search %}<script>{{ Document.search }}</script>{% endif %}
|
|
61
61
|
<script>{{ Document.progress }}</script>
|
|
62
62
|
<script>{{ Document.fold }}</script>
|
|
63
|
+
<script>{{ Document.maximize }}</script>
|
|
63
64
|
{% if Document.tocpanel %}<script>{{ Document.tocscript }}</script>{% endif %}
|
|
64
65
|
{% if Document.info %}<script>{{ Document.info }}</script>{% endif %}
|
|
65
66
|
{% if Document.realtime %}<script class="realtime">{{ Document.realtime }}</script>{% endif %}
|
|
@@ -654,6 +655,110 @@ const foldScript = textframe `
|
|
|
654
655
|
|
|
655
656
|
})()
|
|
656
657
|
`;
|
|
658
|
+
/* the client-side script of the diagram maximization: it attaches two
|
|
659
|
+
controls to the top right corner of every diagram, revealed by the
|
|
660
|
+
stylesheet while the diagram is hovered, which maximize the diagram
|
|
661
|
+
temporarily in a single shared overlay covering the viewport (the
|
|
662
|
+
SVG cloned into it and fitted by the stylesheet), the second one
|
|
663
|
+
additionally taking the overlay into the browser fullscreen (a
|
|
664
|
+
refused request leaves the viewport overlay), where the close mark
|
|
665
|
+
at the top right corner, "Escape", a click beside the diagram, and
|
|
666
|
+
a jump through a node hyperlink close the overlay again (a browser
|
|
667
|
+
leaving the fullscreen on its own closes it, too). The script runs
|
|
668
|
+
at the end of the body, as the diagrams it decorates have to exist
|
|
669
|
+
already, and a live preview body swap replaces the overlay and the
|
|
670
|
+
body-bound listeners along with the body, while the document-bound
|
|
671
|
+
fullscreen listener retires itself */
|
|
672
|
+
const maximizeScript = textframe `
|
|
673
|
+
(function () {
|
|
674
|
+
/* an icon control drawn like the tab icons */
|
|
675
|
+
const svgNS = "http://www.w3.org/2000/svg"
|
|
676
|
+
const mark = (name, title, paths) => {
|
|
677
|
+
const span = document.createElement("span")
|
|
678
|
+
span.className = name
|
|
679
|
+
span.title = title
|
|
680
|
+
const svg = document.createElementNS(svgNS, "svg")
|
|
681
|
+
svg.setAttribute("viewBox", "0 0 24 24")
|
|
682
|
+
svg.setAttribute("fill", "none")
|
|
683
|
+
svg.setAttribute("stroke", "currentColor")
|
|
684
|
+
svg.setAttribute("stroke-width", "2.5")
|
|
685
|
+
svg.setAttribute("stroke-linecap", "round")
|
|
686
|
+
svg.setAttribute("stroke-linejoin", "round")
|
|
687
|
+
for (const d of paths) {
|
|
688
|
+
const path = document.createElementNS(svgNS, "path")
|
|
689
|
+
path.setAttribute("d", d)
|
|
690
|
+
svg.appendChild(path)
|
|
691
|
+
}
|
|
692
|
+
span.appendChild(svg)
|
|
693
|
+
return span
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/* the shared overlay: the close mark plus the content holding
|
|
697
|
+
the clone of the maximized diagram */
|
|
698
|
+
const overlay = document.createElement("div")
|
|
699
|
+
overlay.className = "maximize"
|
|
700
|
+
const close = mark("maximize-close", "close the maximized diagram",
|
|
701
|
+
[ "M18 6 6 18", "M6 6l12 12" ])
|
|
702
|
+
const content = document.createElement("div")
|
|
703
|
+
content.className = "maximize-content"
|
|
704
|
+
overlay.append(close, content)
|
|
705
|
+
document.body.appendChild(overlay)
|
|
706
|
+
|
|
707
|
+
/* open the overlay onto a diagram, optionally in the browser
|
|
708
|
+
fullscreen, and close it again (leaving the fullscreen, too),
|
|
709
|
+
with the document scrolling locked while it is open */
|
|
710
|
+
const open = (diagram, fullscreen) => {
|
|
711
|
+
const svg = diagram.querySelector("svg").cloneNode(true)
|
|
712
|
+
svg.removeAttribute("style")
|
|
713
|
+
content.replaceChildren(svg)
|
|
714
|
+
overlay.classList.add("open")
|
|
715
|
+
document.documentElement.classList.add("maximized")
|
|
716
|
+
if (fullscreen)
|
|
717
|
+
overlay.requestFullscreen().catch(() => {})
|
|
718
|
+
}
|
|
719
|
+
const shut = () => {
|
|
720
|
+
overlay.classList.remove("open")
|
|
721
|
+
document.documentElement.classList.remove("maximized")
|
|
722
|
+
content.replaceChildren()
|
|
723
|
+
if (document.fullscreenElement === overlay)
|
|
724
|
+
document.exitFullscreen().catch(() => {})
|
|
725
|
+
}
|
|
726
|
+
close.addEventListener("click", shut)
|
|
727
|
+
overlay.addEventListener("click", (event) => {
|
|
728
|
+
/* a node hyperlink navigates on its own, the overlay just
|
|
729
|
+
gets out of its way, while a click beside the diagram
|
|
730
|
+
closes the overlay alone */
|
|
731
|
+
if (event.target.closest("a") !== null || event.target.closest("svg") === null)
|
|
732
|
+
shut()
|
|
733
|
+
})
|
|
734
|
+
document.body.addEventListener("keydown", (event) => {
|
|
735
|
+
if (event.key === "Escape" && overlay.classList.contains("open"))
|
|
736
|
+
shut()
|
|
737
|
+
})
|
|
738
|
+
const left = () => {
|
|
739
|
+
if (!overlay.isConnected)
|
|
740
|
+
document.removeEventListener("fullscreenchange", left)
|
|
741
|
+
else if (document.fullscreenElement === null && overlay.classList.contains("open"))
|
|
742
|
+
shut()
|
|
743
|
+
}
|
|
744
|
+
document.addEventListener("fullscreenchange", left)
|
|
745
|
+
|
|
746
|
+
/* attach the two controls to every diagram */
|
|
747
|
+
document.querySelectorAll("article div.diagram, nav.doc div.diagram").forEach((diagram) => {
|
|
748
|
+
const controls = document.createElement("span")
|
|
749
|
+
controls.className = "maximize-controls"
|
|
750
|
+
const view = mark("maximize-view", "maximize this diagram in the viewport",
|
|
751
|
+
[ "M15 3h6v6", "M21 3l-7 7", "M9 21H3v-6", "M3 21l7-7" ])
|
|
752
|
+
const full = mark("maximize-full", "maximize this diagram in fullscreen",
|
|
753
|
+
[ "M3 7V5a2 2 0 0 1 2-2h2", "M17 3h2a2 2 0 0 1 2 2v2",
|
|
754
|
+
"M21 17v2a2 2 0 0 1-2 2h-2", "M7 21H5a2 2 0 0 1-2-2v-2" ])
|
|
755
|
+
view.addEventListener("click", () => { open(diagram, false) })
|
|
756
|
+
full.addEventListener("click", () => { open(diagram, true) })
|
|
757
|
+
controls.append(view, full)
|
|
758
|
+
diagram.appendChild(controls)
|
|
759
|
+
})
|
|
760
|
+
})()
|
|
761
|
+
`;
|
|
657
762
|
/* the client-side script of the table of contents side panel: it
|
|
658
763
|
slides the panel out of and back into the viewport edge (closing it
|
|
659
764
|
also on a jump, on "Escape", and on a click outside), remembers the
|
|
@@ -1661,6 +1766,7 @@ export const renderHtml = async (specification, config, tocPages, css, realtime
|
|
|
1661
1766
|
search: safe(searchScript()),
|
|
1662
1767
|
progress: safe(scrollProgressScript),
|
|
1663
1768
|
fold: safe(foldScript),
|
|
1769
|
+
maximize: safe(maximizeScript),
|
|
1664
1770
|
info: info !== null ? safe(infoScript(info, spec ?? {})) : "",
|
|
1665
1771
|
realtime: realtime ? safe(realtimeScript) : "",
|
|
1666
1772
|
toc: entries.length > 0 ? safe(render("Toc", { Toc: { entries } })) : "",
|
package/dst/specbook-format.md
CHANGED
package/dst/specbook-format.yaml
CHANGED
|
@@ -1239,7 +1239,27 @@
|
|
|
1239
1239
|
type: graph
|
|
1240
1240
|
nodes: "[[DM.ENTITY:*]]"
|
|
1241
1241
|
edges: "[[RELATION:*]]"
|
|
1242
|
+
nest:
|
|
1243
|
+
properties:
|
|
1244
|
+
- GROUP
|
|
1245
|
+
config:
|
|
1246
|
+
graph-columns-max: 2
|
|
1242
1247
|
children:
|
|
1248
|
+
- kind: GROUP
|
|
1249
|
+
name: "[A-Z][a-zA-Z0-9]*"
|
|
1250
|
+
desc: >
|
|
1251
|
+
A named group of strongly related entities, carrying a Pascal-cased, 1-3 word long,
|
|
1252
|
+
per-artifact unique name, distinct from the names of its entities (e.g. `Statistics`
|
|
1253
|
+
for the snapshot entities, `Messages` for a message and its texts), and described by
|
|
1254
|
+
a 1-sentence description ("what") plus a 1-sentence rationale ("why") of the
|
|
1255
|
+
cohesion. A group is either an aggregate (a root entity with the part entities it
|
|
1256
|
+
owns) or a subject area (entities sharing one concern), affects the overview diagram
|
|
1257
|
+
only, where its entities are drawn next to each other inside a box, and carries no
|
|
1258
|
+
semantics of its own: the relations between its entities stay explicit. The groups
|
|
1259
|
+
are listed before the entities, and every entity belongs to at most one group.
|
|
1260
|
+
optional: true
|
|
1261
|
+
referenced:
|
|
1262
|
+
- "[[DM.ENTITY:*]]"
|
|
1243
1263
|
- kind: ENTITY
|
|
1244
1264
|
name: "[A-Z][a-zA-Z0-9]*"
|
|
1245
1265
|
desc: >
|
|
@@ -1281,6 +1301,13 @@
|
|
|
1281
1301
|
is anchored in the ubiquitous language and every term shows its data.
|
|
1282
1302
|
value: list([[TERM:*]])
|
|
1283
1303
|
optional: true
|
|
1304
|
+
- name: GROUP
|
|
1305
|
+
desc: >
|
|
1306
|
+
Reference to the DATA-MODEL group the entity belongs to, placing it next to
|
|
1307
|
+
the other entities of the group in the overview diagram; omitted for an
|
|
1308
|
+
entity standing on its own.
|
|
1309
|
+
value: "[[DM.GROUP:*]]"
|
|
1310
|
+
optional: true
|
|
1284
1311
|
children:
|
|
1285
1312
|
- kind: ATTRIBUTE
|
|
1286
1313
|
name: "[a-z][a-zA-Z0-9]*"
|
|
@@ -2872,7 +2899,7 @@
|
|
|
2872
2899
|
- PRODUCT
|
|
2873
2900
|
config:
|
|
2874
2901
|
size-node-width-max: 440
|
|
2875
|
-
grid-columns-max:
|
|
2902
|
+
grid-columns-max: 2
|
|
2876
2903
|
children:
|
|
2877
2904
|
- kind: TIER
|
|
2878
2905
|
desc: |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rse/specbook",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.9",
|
|
4
4
|
"publishConfig": { "access": "public" },
|
|
5
5
|
"description": "Markdown-based Specification Format (API, CLI and MCP service)",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -17,16 +17,16 @@
|
|
|
17
17
|
},
|
|
18
18
|
"files": [ "dst" ],
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"marked": "18.0.
|
|
20
|
+
"marked": "18.0.12",
|
|
21
21
|
"marked-smartypants": "1.1.12",
|
|
22
22
|
"yaml": "2.9.0",
|
|
23
23
|
"json5": "2.2.3",
|
|
24
24
|
"pdf-lib": "1.17.1",
|
|
25
25
|
"subset-font": "2.7.0",
|
|
26
26
|
"@rse/mrcs": "0.9.2",
|
|
27
|
-
"@rse/gradia": "1.2.
|
|
27
|
+
"@rse/gradia": "1.2.3",
|
|
28
28
|
"@toon-format/toon": "4.1.1",
|
|
29
|
-
"valibot": "1.
|
|
29
|
+
"valibot": "1.5.0",
|
|
30
30
|
"nunjucks": "3.2.4",
|
|
31
31
|
"@rse/nunjucks-addons": "2.2.0",
|
|
32
32
|
"textframe": "1.2.1",
|
|
@@ -42,13 +42,13 @@
|
|
|
42
42
|
"fastify": "5.12.3",
|
|
43
43
|
"@fastify/websocket": "11.3.0",
|
|
44
44
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
45
|
-
"zod": "4.
|
|
45
|
+
"zod": "4.6.1"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"chokidar-cli": "3.0.0",
|
|
49
49
|
"eslint": "10.9.0",
|
|
50
50
|
"@eslint/js": "10.0.1",
|
|
51
|
-
"typescript-eslint": "8.
|
|
51
|
+
"typescript-eslint": "8.70.0",
|
|
52
52
|
"eslint-plugin-promise": "7.3.0",
|
|
53
53
|
"neostandard": "0.14.0-next.1",
|
|
54
54
|
"globals": "17.12.0",
|