@rse/specbook 1.2.10 → 1.2.12

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.
@@ -11,7 +11,9 @@
11
11
  the table header and the headings of all enclosing objects, and
12
12
  every matched word is highlighted with a <mark> element (an SVG
13
13
  <tspan> element underlaid with a <rect> inside a diagram), while
14
- the table of contents (page and side panel) is filtered along */
14
+ the table of contents (page and side panel) is filtered along; the
15
+ explicit anchor id of an object (invisible in the document) is
16
+ searched like its text, a hit marking the anchor symbol instead */
15
17
  (function () {
16
18
  const tab = document.getElementById("search")
17
19
  const toggle = document.getElementById("search-toggle")
@@ -52,7 +54,9 @@
52
54
  one not nested inside another unit (like a paragraph or diagram
53
55
  in a table cell, which is covered by its table row), where a
54
56
  diagram is searched by its SVG text labels, joined by spaces,
55
- as its raw text content would run them together */
57
+ as its raw text content would run them together, and where the
58
+ explicit anchor id of an object heading or table row joins the
59
+ searched text */
56
60
  const index = () => {
57
61
  if (indexed)
58
62
  return
@@ -66,12 +70,13 @@
66
70
  return
67
71
  parent = parent.parentElement
68
72
  }
69
- const text = (el.classList.contains("diagram") ?
73
+ const id = (el.dataset.id ?? "").toLowerCase()
74
+ const text = ((el.classList.contains("diagram") ?
70
75
  Array.from(el.querySelectorAll("text")).map((t) => t.textContent ?? "").join(" ") :
71
- (el.textContent ?? "")).toLowerCase()
76
+ (el.textContent ?? "")) + " " + id).toLowerCase()
72
77
  const words = new Set(text.split(/[^\p{L}\p{N}]+/u).filter((w) => w !== ""))
73
78
  words.forEach((w) => { vocab.add(w) })
74
- units.push({ el, text, words, keep: false })
79
+ units.push({ el, id, text, words, keep: false })
75
80
  })
76
81
  })
77
82
 
@@ -319,12 +324,24 @@
319
324
  el.classList.add("search-hide")
320
325
  })
321
326
 
322
- /* highlight every matched word within the kept units */
327
+ /* highlight every matched word within the kept units, plus the
328
+ anchor symbol of a unit whose explicit anchor id matched, as
329
+ the id itself is nowhere visible */
323
330
  const words = Array.from(new Set(parts.flat()))
324
331
  const regex = buildRegex(words, Array.from(variants))
325
332
  units.forEach((unit) => {
326
- if (unit.el.classList.contains("search-keep"))
327
- highlight(unit.el, regex)
333
+ if (!unit.el.classList.contains("search-keep"))
334
+ return
335
+ highlight(unit.el, regex)
336
+ regex.lastIndex = 0
337
+ const symbol = unit.el.querySelector("span.anchor-symbol")
338
+ if (unit.id === "" || symbol === null || !regex.test(unit.id))
339
+ return
340
+ const mark = document.createElement("mark")
341
+ mark.classList.add("search-hit")
342
+ while (symbol.firstChild !== null)
343
+ mark.appendChild(symbol.firstChild)
344
+ symbol.appendChild(mark)
328
345
  })
329
346
 
330
347
  /* filter the table of contents (its page rows and its side
@@ -282,7 +282,9 @@ body.info-on div.info-switch svg.info-icon {
282
282
  icon leading each, the kinds bold in the path color and the names
283
283
  semi-bold in the description color, as in the document content,
284
284
  and a pointer separating the segments), extended for properties
285
- with the not-filled circle icon and the bold property name, and
285
+ with the not-filled circle icon and the bold property name, trailed
286
+ for instances by the anchor id of the object in the description
287
+ color (its "(#...)" wrapping normal, the id itself semi-bold), and
286
288
  the description led by a "Schema" or "Specification" origin label
287
289
  (its word in normal weight in a rounded box filled with the path
288
290
  color), where
@@ -329,6 +331,13 @@ div.info-popup span.info-name {
329
331
  div.info-popup.spec span.info-name {
330
332
  color: var(--theme-color-specbook-popup-spec-desc);
331
333
  }
334
+ div.info-popup.spec span.info-id {
335
+ color: var(--theme-color-specbook-popup-spec-desc);
336
+ font-weight: normal;
337
+ }
338
+ div.info-popup.spec span.info-id-name {
339
+ font-weight: 600;
340
+ }
332
341
  div.info-popup span.info-object::before {
333
342
  content: "\25CF";
334
343
  font-weight: normal;
@@ -109,7 +109,7 @@ const templates = {
109
109
  <h1>Table of Contents</h1>
110
110
  <table>
111
111
  {% for entry in Toc.entries %}
112
- <tr class="level-{{ entry.level }}"><td><a href="#{{ entry.id }}"><span class="object-kind"{% if entry.info %} data-info="{{ entry.info }}" data-info-path="{{ entry.infopath }}"{% endif %}>{{ entry.kind }}:</span> <span class="object-name"{% if entry.spec %} data-info-spec="{{ entry.spec }}" data-info-path="{{ entry.specpath }}"{% endif %}>{{ entry.name }}</span> <span class="link-symbol">&#x26AD;</span></a></td>{% if entry.page %}<td class="page"><a href="#{{ entry.id }}">{{ entry.page }}</a></td>{% endif %}</tr>
112
+ <tr class="level-{{ entry.level }}"><td><a href="#{{ entry.id }}"><span class="object-kind"{% if entry.info %} data-info="{{ entry.info }}" data-info-path="{{ entry.infopath }}"{% endif %}>{{ entry.kind }}:</span> <span class="object-name"{% if entry.spec %} data-info-spec="{{ entry.spec }}" data-info-id="{{ entry.specid }}" data-info-path="{{ entry.specpath }}"{% endif %}>{{ entry.name }}</span> <span class="link-symbol">&#x26AD;</span></a></td>{% if entry.page %}<td class="page"><a href="#{{ entry.id }}">{{ entry.page }}</a></td>{% endif %}</tr>
113
113
  {% endfor %}
114
114
  </table>
115
115
  </nav>
@@ -132,7 +132,7 @@ const templates = {
132
132
  "TocPanelEntries": textframe `
133
133
  <ul>
134
134
  {% for entry in Entries %}
135
- <li><a href="#{{ entry.id }}"><span class="entry"><span class="object-kind"{% if entry.info %} data-info="{{ entry.info }}" data-info-path="{{ entry.infopath }}"{% endif %}>{{ entry.kind }}:</span> <span class="object-name"{% if entry.spec %} data-info-spec="{{ entry.spec }}" data-info-path="{{ entry.specpath }}"{% endif %}>{{ entry.name }}</span></span></a>{{ entry.children }}</li>
135
+ <li><a href="#{{ entry.id }}"><span class="entry"><span class="object-kind"{% if entry.info %} data-info="{{ entry.info }}" data-info-path="{{ entry.infopath }}"{% endif %}>{{ entry.kind }}:</span> <span class="object-name"{% if entry.spec %} data-info-spec="{{ entry.spec }}" data-info-id="{{ entry.specid }}" data-info-path="{{ entry.specpath }}"{% endif %}>{{ entry.name }}</span></span></a>{{ entry.children }}</li>
136
136
  {% endfor %}
137
137
  </ul>
138
138
  `,
@@ -152,7 +152,7 @@ const templates = {
152
152
  /* <Object/> */
153
153
  "Object": textframe `
154
154
  <section>
155
- <h{{ Object.level }} id="{{ Object.id }}"><span class="object-kind"{% if Object.info %} data-info="{{ Object.info }}" data-info-path="{{ Object.infopath }}"{% endif %}>{{ Object.kind }}:</span> <span class="object-name"{% if Object.spec %} data-info-spec="{{ Object.spec }}" data-info-path="{{ Object.specpath }}"{% endif %}>{{ Object.name }}</span>{% if Object.primary %} <span class="primary-marker">&#x2318;</span>{% endif %}{% if Object.paren %} <span class="anchor-paren">({{ Object.paren }})</span>{% endif %} <a href="#{{ Object.id }}"><span class="anchor-symbol">&#x2693;&#xFE0E;</span></a></h{{ Object.level }}>
155
+ <h{{ Object.level }} id="{{ Object.id }}"{% if Object.anchor %} data-id="{{ Object.anchor }}"{% endif %}><span class="object-kind"{% if Object.info %} data-info="{{ Object.info }}" data-info-path="{{ Object.infopath }}"{% endif %}>{{ Object.kind }}:</span> <span class="object-name"{% if Object.spec %} data-info-spec="{{ Object.spec }}" data-info-id="{{ Object.specid }}" data-info-path="{{ Object.specpath }}"{% endif %}>{{ Object.name }}</span>{% if Object.primary %} <span class="primary-marker">&#x2318;</span>{% endif %}{% if Object.paren %} <span class="anchor-paren">({{ Object.paren }})</span>{% endif %} <a href="#{{ Object.id }}"><span class="anchor-symbol">&#x2693;&#xFE0E;</span></a></h{{ Object.level }}>
156
156
  {{ Object.diagram }}
157
157
  {{ Object.properties }}
158
158
  {{ Object.description }}
@@ -206,8 +206,8 @@ const templates = {
206
206
  </thead>
207
207
  <tbody>
208
208
  {% for row in Table.rows %}
209
- <tr id="{{ row.id }}"{% if row.even %} class="even"{% endif %}>
210
- <td><span{% if row.spec %} data-info-spec="{{ row.spec }}" data-info-path="{{ row.specpath }}"{% endif %}>{{ row.name }}</span>{% if row.primary %} <span class="primary-marker">&#x2318;</span>{% endif %}{% if row.paren %} <span class="anchor-paren">({{ row.paren }})</span>{% endif %} <a href="#{{ row.id }}"><span class="anchor-symbol">&#x2693;&#xFE0E;</span></a></td>
209
+ <tr id="{{ row.id }}"{% if row.anchor %} data-id="{{ row.anchor }}"{% endif %}{% if row.even %} class="even"{% endif %}>
210
+ <td><span{% if row.spec %} data-info-spec="{{ row.spec }}" data-info-id="{{ row.specid }}" data-info-path="{{ row.specpath }}"{% endif %}>{{ row.name }}</span>{% if row.primary %} <span class="primary-marker">&#x2318;</span>{% endif %}{% if row.paren %} <span class="anchor-paren">({{ row.paren }})</span>{% endif %} <a href="#{{ row.id }}"><span class="anchor-symbol">&#x2693;&#xFE0E;</span></a></td>
211
211
  {%- for value in row.values %}<td>{{ value }}</td>{% endfor %}
212
212
  {%- if Table.desc %}<td>{{ row.description }}</td>{% endif %}
213
213
  </tr>
@@ -226,8 +226,8 @@ const templates = {
226
226
  </thead>
227
227
  <tbody>
228
228
  {% for row in Table.rows %}
229
- <tr id="{{ row.id }}"{% if row.even %} class="even"{% endif %}>
230
- <td><span{% if row.spec %} data-info-spec="{{ row.spec }}" data-info-path="{{ row.specpath }}"{% endif %}>{{ row.name }}</span>{% if row.primary %} <span class="primary-marker">&#x2318;</span>{% endif %}{% if row.paren %} <span class="anchor-paren">({{ row.paren }})</span>{% endif %} <a href="#{{ row.id }}"><span class="anchor-symbol">&#x2693;&#xFE0E;</span></a></td>
229
+ <tr id="{{ row.id }}"{% if row.anchor %} data-id="{{ row.anchor }}"{% endif %}{% if row.even %} class="even"{% endif %}>
230
+ <td><span{% if row.spec %} data-info-spec="{{ row.spec }}" data-info-id="{{ row.specid }}" data-info-path="{{ row.specpath }}"{% endif %}>{{ row.name }}</span>{% if row.primary %} <span class="primary-marker">&#x2318;</span>{% endif %}{% if row.paren %} <span class="anchor-paren">({{ row.paren }})</span>{% endif %} <a href="#{{ row.id }}"><span class="anchor-symbol">&#x2693;&#xFE0E;</span></a></td>
231
231
  <td class="chunks">
232
232
  <table class="chunks">
233
233
  {% for chunk in row.chunks %}
@@ -866,7 +866,8 @@ const tocPanelScript = textframe `
866
866
  carrying a "data-info-spec" key pops up the corpus description of
867
867
  the object instance (a diagram node box resolves through its
868
868
  hyperlinked object anchor), titled with the "data-info-path" object
869
- path and fed from the injected INFO/SPEC maps of pre-rendered
869
+ path (an instance popup trailing the "data-info-id" anchor id of
870
+ the object) and fed from the injected INFO/SPEC maps of pre-rendered
870
871
  description HTML; the popup is capped at 40% viewport width and
871
872
  attached above or below, whichever side offers more space; the
872
873
  script runs at the end of the body, so a live preview body swap
@@ -964,6 +965,20 @@ const infoPopupScript = textframe `
964
965
  }
965
966
  title.appendChild(path)
966
967
  }
968
+
969
+ /* the anchor id of an object instance (explicit or derived
970
+ from its name, carried by the "data-info-id" key) trails
971
+ the last segment */
972
+ const anchor = source.getAttribute("data-info-id")
973
+ if (spec !== null && anchor !== null) {
974
+ const id = document.createElement("span")
975
+ id.className = "info-id"
976
+ const name = document.createElement("span")
977
+ name.className = "info-id-name"
978
+ name.textContent = anchor
979
+ id.append(" (#", name, ")")
980
+ title.appendChild(id)
981
+ }
967
982
  if (prop !== null) {
968
983
  title.appendChild(pointer())
969
984
  const name = document.createElement("span")
@@ -1154,11 +1169,12 @@ const infoAttr = (object) => {
1154
1169
  ` data-info="${escapeHtml(key)}" data-info-path="${escapeHtml(infoPathOf(object, false))}"` : "";
1155
1170
  };
1156
1171
  /* render the corpus description popup key (the fully-qualified anchor
1157
- path) and name-ending title path of an object as "data-info-spec"
1158
- attributes for the manually assembled hyperlink markup (the
1172
+ path), anchor id, and name-ending title path of an object as
1173
+ "data-info-spec" attributes for the manually assembled hyperlink markup (the
1159
1174
  instance popup of the compact and full hyperlinks) */
1160
1175
  const specAttr = (object) => infoParents !== null ?
1161
- ` data-info-spec="${escapeHtml(anchorOf(object))}" data-info-path="${escapeHtml(infoPathOf(object))}"` : "";
1176
+ ` data-info-spec="${escapeHtml(anchorOf(object))}" data-info-id="${escapeHtml(object.id)}"` +
1177
+ ` data-info-path="${escapeHtml(infoPathOf(object))}"` : "";
1162
1178
  /* collect the description popup map and the qualified title paths of
1163
1179
  the schema nodes (the artifact kind leading its name, the nested
1164
1180
  kinds appended), with the description Markdown of the objects and
@@ -1460,9 +1476,11 @@ const renderTable = (children, maxColumns) => {
1460
1476
  width: Math.round(200 / (keys.length + 3)),
1461
1477
  rows: children.map((child, i) => scoped(child, () => ({
1462
1478
  id: anchorOf(child),
1479
+ anchor: child.anchor,
1463
1480
  paren: child.paren,
1464
1481
  primary: child.primary,
1465
1482
  spec: infoParents !== null ? anchorOf(child) : undefined,
1483
+ specid: child.id,
1466
1484
  specpath: infoPathOf(child),
1467
1485
  name: inline(child.name),
1468
1486
  even: i % 2 === 1,
@@ -1500,9 +1518,11 @@ const renderTable = (children, maxColumns) => {
1500
1518
  chunks.unshift([{ key: "Diagram", desc: true, span: size, value: diagram }]);
1501
1519
  return {
1502
1520
  id: anchorOf(child),
1521
+ anchor: child.anchor,
1503
1522
  paren: child.paren,
1504
1523
  primary: child.primary,
1505
1524
  spec: infoParents !== null ? anchorOf(child) : undefined,
1525
+ specid: child.id,
1506
1526
  specpath: infoPathOf(child),
1507
1527
  name: inline(child.name),
1508
1528
  even: i % 2 === 1,
@@ -1520,8 +1540,10 @@ const renderObject = (object, level, concise) => {
1520
1540
  info: infoKeyOf(object),
1521
1541
  infopath: infoPathOf(object, false),
1522
1542
  spec: infoParents !== null ? anchorOf(object) : undefined,
1543
+ specid: object.id,
1523
1544
  specpath: infoPathOf(object),
1524
1545
  id: anchorOf(object),
1546
+ anchor: object.anchor,
1525
1547
  paren: object.paren,
1526
1548
  primary: object.primary,
1527
1549
  name: inline(object.name),
@@ -1590,7 +1612,7 @@ const tocEntries = (objects, pages) => {
1590
1612
  const id = anchorOf(object);
1591
1613
  entries.push({ id, kind: object.kind,
1592
1614
  info: infoKeyOf(object), infopath: infoPathOf(object, false),
1593
- spec: infoParents !== null ? id : undefined, specpath: infoPathOf(object),
1615
+ spec: infoParents !== null ? id : undefined, specid: object.id, specpath: infoPathOf(object),
1594
1616
  name: inline(object.name), level: Math.min(level, 6), page: pages?.get(id) });
1595
1617
  collect(headingChildren(object), level + 1);
1596
1618
  }
@@ -1610,6 +1632,7 @@ const renderTocPanel = (objects, title, doc) => {
1610
1632
  info: infoKeyOf(object),
1611
1633
  infopath: infoPathOf(object, false),
1612
1634
  spec: infoParents !== null ? anchorOf(object) : undefined,
1635
+ specid: object.id,
1613
1636
  specpath: infoPathOf(object),
1614
1637
  name: inline(object.name),
1615
1638
  children: safe(entries(headingChildren(object)))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rse/specbook",
3
- "version": "1.2.10",
3
+ "version": "1.2.12",
4
4
  "publishConfig": { "access": "public" },
5
5
  "description": "Markdown-based Specification Format (API, CLI and MCP service)",
6
6
  "license": "Apache-2.0",
@@ -17,9 +17,9 @@
17
17
  },
18
18
  "files": [ "dst" ],
19
19
  "dependencies": {
20
- "marked": "18.0.12",
20
+ "marked": "18.0.13",
21
21
  "marked-smartypants": "1.1.12",
22
- "yaml": "2.9.0",
22
+ "yaml": "2.9.1",
23
23
  "json5": "2.2.3",
24
24
  "pdf-lib": "1.17.1",
25
25
  "subset-font": "2.7.0",
@@ -40,10 +40,10 @@
40
40
  "glob": "13.0.6",
41
41
  "picomatch": "4.0.7",
42
42
  "es-toolkit": "1.52.0",
43
- "fastify": "5.12.3",
43
+ "fastify": "5.12.4",
44
44
  "@fastify/websocket": "11.3.0",
45
45
  "@modelcontextprotocol/sdk": "1.30.0",
46
- "zod": "4.6.1"
46
+ "zod": "4.6.5"
47
47
  },
48
48
  "devDependencies": {
49
49
  "chokidar-cli": "3.0.0",
@@ -55,7 +55,7 @@
55
55
  "globals": "17.12.0",
56
56
  "markdownlint": "0.41.1",
57
57
  "markdownlint-cli2": "0.23.2",
58
- "@types/node": "26.5.0",
58
+ "@types/node": "26.5.1",
59
59
  "@types/picomatch": "4.0.3",
60
60
  "@types/subset-font": "2.3.0",
61
61
  "@types/nunjucks": "3.2.6",