living-ai-documentation 1.0.0

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 (203) hide show
  1. package/LICENSE +661 -0
  2. package/README.fr.md +344 -0
  3. package/README.md +344 -0
  4. package/dist/bin/cli.d.ts +3 -0
  5. package/dist/bin/cli.d.ts.map +1 -0
  6. package/dist/bin/cli.js +262 -0
  7. package/dist/bin/cli.js.map +1 -0
  8. package/dist/src/frontend/accuracy-gauge.js +70 -0
  9. package/dist/src/frontend/admin.html +1532 -0
  10. package/dist/src/frontend/annotations.js +585 -0
  11. package/dist/src/frontend/boot.js +101 -0
  12. package/dist/src/frontend/config.js +29 -0
  13. package/dist/src/frontend/confirm-modal.js +82 -0
  14. package/dist/src/frontend/context.html +1252 -0
  15. package/dist/src/frontend/dark-mode.js +20 -0
  16. package/dist/src/frontend/diagram/alignment.js +161 -0
  17. package/dist/src/frontend/diagram/clipboard.js +187 -0
  18. package/dist/src/frontend/diagram/constants.js +109 -0
  19. package/dist/src/frontend/diagram/custom-shapes.js +104 -0
  20. package/dist/src/frontend/diagram/debug.js +43 -0
  21. package/dist/src/frontend/diagram/drawio-export.js +649 -0
  22. package/dist/src/frontend/diagram/edge-panel.js +293 -0
  23. package/dist/src/frontend/diagram/edge-rendering.js +12 -0
  24. package/dist/src/frontend/diagram/evidence.js +146 -0
  25. package/dist/src/frontend/diagram/grid.js +78 -0
  26. package/dist/src/frontend/diagram/groups.js +102 -0
  27. package/dist/src/frontend/diagram/history.js +157 -0
  28. package/dist/src/frontend/diagram/image-name-modal.js +48 -0
  29. package/dist/src/frontend/diagram/image-upload.js +36 -0
  30. package/dist/src/frontend/diagram/label-editor.js +115 -0
  31. package/dist/src/frontend/diagram/link-panel.js +144 -0
  32. package/dist/src/frontend/diagram/main.js +364 -0
  33. package/dist/src/frontend/diagram/network.js +2214 -0
  34. package/dist/src/frontend/diagram/node-panel.js +389 -0
  35. package/dist/src/frontend/diagram/node-rendering.js +964 -0
  36. package/dist/src/frontend/diagram/persistence.js +168 -0
  37. package/dist/src/frontend/diagram/ports.js +421 -0
  38. package/dist/src/frontend/diagram/selection-overlay.js +387 -0
  39. package/dist/src/frontend/diagram/state.js +43 -0
  40. package/dist/src/frontend/diagram/t.js +3 -0
  41. package/dist/src/frontend/diagram/toast.js +21 -0
  42. package/dist/src/frontend/diagram/unlock-hold.js +206 -0
  43. package/dist/src/frontend/diagram/zoom.js +20 -0
  44. package/dist/src/frontend/diagram-link-modal.js +137 -0
  45. package/dist/src/frontend/diagram.html +1494 -0
  46. package/dist/src/frontend/documents.js +479 -0
  47. package/dist/src/frontend/export.js +338 -0
  48. package/dist/src/frontend/file-attach.js +178 -0
  49. package/dist/src/frontend/files-modal.js +243 -0
  50. package/dist/src/frontend/i18n/en.json +624 -0
  51. package/dist/src/frontend/i18n/fr.json +624 -0
  52. package/dist/src/frontend/i18n.js +32 -0
  53. package/dist/src/frontend/image-paste.js +126 -0
  54. package/dist/src/frontend/index.html +2806 -0
  55. package/dist/src/frontend/local-search.js +476 -0
  56. package/dist/src/frontend/metadata.js +318 -0
  57. package/dist/src/frontend/misc.js +92 -0
  58. package/dist/src/frontend/new-doc-modal.js +285 -0
  59. package/dist/src/frontend/new-folder-modal.js +169 -0
  60. package/dist/src/frontend/search.js +194 -0
  61. package/dist/src/frontend/shape-editor.html +685 -0
  62. package/dist/src/frontend/sidebar-helpers.js +96 -0
  63. package/dist/src/frontend/sidebar-resize.js +98 -0
  64. package/dist/src/frontend/sidebar.js +351 -0
  65. package/dist/src/frontend/snippet-detect.js +25 -0
  66. package/dist/src/frontend/snippet-table.js +85 -0
  67. package/dist/src/frontend/snippet-tree.js +94 -0
  68. package/dist/src/frontend/snippets.js +1146 -0
  69. package/dist/src/frontend/state.js +46 -0
  70. package/dist/src/frontend/utils.js +21 -0
  71. package/dist/src/frontend/validate.js +107 -0
  72. package/dist/src/frontend/vendor/wordcloud2.js +1187 -0
  73. package/dist/src/frontend/wordcloud.js +693 -0
  74. package/dist/src/lib/config.d.ts +26 -0
  75. package/dist/src/lib/config.d.ts.map +1 -0
  76. package/dist/src/lib/config.js +195 -0
  77. package/dist/src/lib/config.js.map +1 -0
  78. package/dist/src/lib/hash.d.ts +2 -0
  79. package/dist/src/lib/hash.d.ts.map +1 -0
  80. package/dist/src/lib/hash.js +18 -0
  81. package/dist/src/lib/hash.js.map +1 -0
  82. package/dist/src/lib/metadata.d.ts +31 -0
  83. package/dist/src/lib/metadata.d.ts.map +1 -0
  84. package/dist/src/lib/metadata.js +128 -0
  85. package/dist/src/lib/metadata.js.map +1 -0
  86. package/dist/src/lib/parser.d.ts +11 -0
  87. package/dist/src/lib/parser.d.ts.map +1 -0
  88. package/dist/src/lib/parser.js +111 -0
  89. package/dist/src/lib/parser.js.map +1 -0
  90. package/dist/src/lib/status.d.ts +9 -0
  91. package/dist/src/lib/status.d.ts.map +1 -0
  92. package/dist/src/lib/status.js +72 -0
  93. package/dist/src/lib/status.js.map +1 -0
  94. package/dist/src/mcp/server.d.ts +3 -0
  95. package/dist/src/mcp/server.d.ts.map +1 -0
  96. package/dist/src/mcp/server.js +2046 -0
  97. package/dist/src/mcp/server.js.map +1 -0
  98. package/dist/src/mcp/tools/diagrams.d.ts +82 -0
  99. package/dist/src/mcp/tools/diagrams.d.ts.map +1 -0
  100. package/dist/src/mcp/tools/diagrams.js +594 -0
  101. package/dist/src/mcp/tools/diagrams.js.map +1 -0
  102. package/dist/src/mcp/tools/documents.d.ts +44 -0
  103. package/dist/src/mcp/tools/documents.d.ts.map +1 -0
  104. package/dist/src/mcp/tools/documents.js +186 -0
  105. package/dist/src/mcp/tools/documents.js.map +1 -0
  106. package/dist/src/mcp/tools/git.d.ts +10 -0
  107. package/dist/src/mcp/tools/git.d.ts.map +1 -0
  108. package/dist/src/mcp/tools/git.js +217 -0
  109. package/dist/src/mcp/tools/git.js.map +1 -0
  110. package/dist/src/mcp/tools/metadata.d.ts +57 -0
  111. package/dist/src/mcp/tools/metadata.d.ts.map +1 -0
  112. package/dist/src/mcp/tools/metadata.js +222 -0
  113. package/dist/src/mcp/tools/metadata.js.map +1 -0
  114. package/dist/src/mcp/tools/source.d.ts +29 -0
  115. package/dist/src/mcp/tools/source.d.ts.map +1 -0
  116. package/dist/src/mcp/tools/source.js +196 -0
  117. package/dist/src/mcp/tools/source.js.map +1 -0
  118. package/dist/src/routes/annotations.d.ts +3 -0
  119. package/dist/src/routes/annotations.d.ts.map +1 -0
  120. package/dist/src/routes/annotations.js +83 -0
  121. package/dist/src/routes/annotations.js.map +1 -0
  122. package/dist/src/routes/browse-source.d.ts +3 -0
  123. package/dist/src/routes/browse-source.d.ts.map +1 -0
  124. package/dist/src/routes/browse-source.js +79 -0
  125. package/dist/src/routes/browse-source.js.map +1 -0
  126. package/dist/src/routes/browse.d.ts +3 -0
  127. package/dist/src/routes/browse.d.ts.map +1 -0
  128. package/dist/src/routes/browse.js +91 -0
  129. package/dist/src/routes/browse.js.map +1 -0
  130. package/dist/src/routes/config.d.ts +3 -0
  131. package/dist/src/routes/config.d.ts.map +1 -0
  132. package/dist/src/routes/config.js +145 -0
  133. package/dist/src/routes/config.js.map +1 -0
  134. package/dist/src/routes/context.d.ts +3 -0
  135. package/dist/src/routes/context.d.ts.map +1 -0
  136. package/dist/src/routes/context.js +287 -0
  137. package/dist/src/routes/context.js.map +1 -0
  138. package/dist/src/routes/diagrams.d.ts +3 -0
  139. package/dist/src/routes/diagrams.d.ts.map +1 -0
  140. package/dist/src/routes/diagrams.js +69 -0
  141. package/dist/src/routes/diagrams.js.map +1 -0
  142. package/dist/src/routes/documents.d.ts +11 -0
  143. package/dist/src/routes/documents.d.ts.map +1 -0
  144. package/dist/src/routes/documents.js +450 -0
  145. package/dist/src/routes/documents.js.map +1 -0
  146. package/dist/src/routes/export.d.ts +3 -0
  147. package/dist/src/routes/export.d.ts.map +1 -0
  148. package/dist/src/routes/export.js +280 -0
  149. package/dist/src/routes/export.js.map +1 -0
  150. package/dist/src/routes/files.d.ts +3 -0
  151. package/dist/src/routes/files.d.ts.map +1 -0
  152. package/dist/src/routes/files.js +180 -0
  153. package/dist/src/routes/files.js.map +1 -0
  154. package/dist/src/routes/images.d.ts +3 -0
  155. package/dist/src/routes/images.d.ts.map +1 -0
  156. package/dist/src/routes/images.js +49 -0
  157. package/dist/src/routes/images.js.map +1 -0
  158. package/dist/src/routes/metadata.d.ts +3 -0
  159. package/dist/src/routes/metadata.d.ts.map +1 -0
  160. package/dist/src/routes/metadata.js +131 -0
  161. package/dist/src/routes/metadata.js.map +1 -0
  162. package/dist/src/routes/shape-libraries.d.ts +3 -0
  163. package/dist/src/routes/shape-libraries.d.ts.map +1 -0
  164. package/dist/src/routes/shape-libraries.js +118 -0
  165. package/dist/src/routes/shape-libraries.js.map +1 -0
  166. package/dist/src/routes/wordcloud.d.ts +3 -0
  167. package/dist/src/routes/wordcloud.d.ts.map +1 -0
  168. package/dist/src/routes/wordcloud.js +95 -0
  169. package/dist/src/routes/wordcloud.js.map +1 -0
  170. package/dist/src/server.d.ts +7 -0
  171. package/dist/src/server.d.ts.map +1 -0
  172. package/dist/src/server.js +93 -0
  173. package/dist/src/server.js.map +1 -0
  174. package/dist/starter-doc/.living-doc.json +52 -0
  175. package/dist/starter-doc/ADRS/2026_01_01_[ADR]_example_architecture_decision.md +59 -0
  176. package/dist/starter-doc/AI/2026_01_01_how_to.md +112 -0
  177. package/dist/starter-doc/AI/PROJECT-INSTRUCTIONS.md +172 -0
  178. package/dist/starter-doc/AI/PROJECT-STACK.md +77 -0
  179. package/dist/starter-doc/AI/PROJECT-USEFUL-COMMANDS.md +80 -0
  180. package/dist/starter-doc/AI/default/AGENTS.md +31 -0
  181. package/dist/starter-doc/AI/default/CLAUDE.md +31 -0
  182. package/dist/starter-doc/AI/default/MEMORY.md +24 -0
  183. package/dist/starter-doc/AI/rules/no-magic-numbers.md +18 -0
  184. package/dist/starter-doc/AI/rules/track-current-work.md +23 -0
  185. package/dist/starter-doc/WORKLOG/current-task.md +57 -0
  186. package/dist/starter-doc-fr/.living-doc.json +52 -0
  187. package/dist/starter-doc-fr/ADRS/2026_01_01_[ADR]_example_architecture_decision.md +59 -0
  188. package/dist/starter-doc-fr/AI/2026_01_01_how_to.md +100 -0
  189. package/dist/starter-doc-fr/AI/PROJECT-INSTRUCTIONS.md +172 -0
  190. package/dist/starter-doc-fr/AI/PROJECT-STACK.md +77 -0
  191. package/dist/starter-doc-fr/AI/PROJECT-USEFUL-COMMANDS.md +80 -0
  192. package/dist/starter-doc-fr/AI/default/AGENTS.md +31 -0
  193. package/dist/starter-doc-fr/AI/default/CLAUDE.md +31 -0
  194. package/dist/starter-doc-fr/AI/default/MEMORY.md +24 -0
  195. package/dist/starter-doc-fr/AI/rules/no-magic-numbers.md +18 -0
  196. package/dist/starter-doc-fr/AI/rules/track-current-work.md +23 -0
  197. package/dist/starter-doc-fr/WORKLOG/current-task.md +57 -0
  198. package/images/living_documentation.jpg +0 -0
  199. package/images/readme-extra-files.png +0 -0
  200. package/images/readme-filename-pattern.png +0 -0
  201. package/images/readme-intelligent-search-demo.jpg +0 -0
  202. package/images/readme-sidebar.png +0 -0
  203. package/package.json +72 -0
@@ -0,0 +1,476 @@
1
+ // ── Local search widget ─────────────────────────────────────────────────────
2
+ // Invoked from documents.js::_wireDocContent after hljs highlighting.
3
+ // Scans the rendered content for <div data-ld-local-search>. If at least one
4
+ // is found, detaches them from the doc body and injects a live-filter widget
5
+ // into the doc header (mount point #local-search-mount).
6
+ //
7
+ // Search features (zero external dependency):
8
+ // - diacritics stripped (NFD + combining removal) → "résumé" matches "resume"
9
+ // - separators collapsed ( -_ + whitespace → single space) → "date-only" matches "date only"
10
+ // - tokenized AND with OR fallback (no block matches all tokens → show any-token matches)
11
+ // - Damerau-Levenshtein distance 1-2 (length-adaptive) on unique-word index for typos and plurals
12
+ // - first-char + length pre-filters eliminate ~90% of candidates before Levenshtein
13
+ // - position map (normalizedIdx → originalIdx) so highlights wrap the exact source substring
14
+
15
+ // ── Text normalization ─────────────────────────────────────────────────────
16
+
17
+ // Normalize a string for case-insensitive, diacritic-insensitive, separator-tolerant matching.
18
+ // Also builds a map[normIdx] → originalIdx, so a match found in the normalized text
19
+ // can be translated back to an exact slice of the source.
20
+ function _ldNormalizeWithMap(src) {
21
+ let normalized = "";
22
+ const map = [];
23
+ let lastWasSpace = true;
24
+ for (let i = 0; i < src.length; i++) {
25
+ const decomposed = src[i].normalize("NFD");
26
+ for (let k = 0; k < decomposed.length; k++) {
27
+ const code = decomposed.charCodeAt(k);
28
+ if (code >= 0x0300 && code <= 0x036f) continue;
29
+ const isAlnum =
30
+ (code >= 48 && code <= 57) ||
31
+ (code >= 65 && code <= 90) ||
32
+ (code >= 97 && code <= 122);
33
+ if (isAlnum) {
34
+ const lower = code >= 65 && code <= 90 ? code + 32 : code;
35
+ normalized += String.fromCharCode(lower);
36
+ map.push(i);
37
+ lastWasSpace = false;
38
+ } else if (!lastWasSpace) {
39
+ normalized += " ";
40
+ map.push(i);
41
+ lastWasSpace = true;
42
+ }
43
+ }
44
+ }
45
+ map.push(src.length);
46
+ if (normalized.endsWith(" ")) {
47
+ normalized = normalized.slice(0, -1);
48
+ map.splice(map.length - 2, 1);
49
+ }
50
+ return { normalized, map };
51
+ }
52
+
53
+ function _ldNormalize(src) {
54
+ return _ldNormalizeWithMap(src).normalized;
55
+ }
56
+
57
+ // ── Damerau-Levenshtein with early exit ────────────────────────────────────
58
+ // Returns the distance if ≤ max, or `max + 1` as a sentinel "over max".
59
+ function _ldDamerauLevenshtein(a, b, max) {
60
+ if (a === b) return 0;
61
+ const alen = a.length;
62
+ const blen = b.length;
63
+ if (Math.abs(alen - blen) > max) return max + 1;
64
+ let prev2 = null;
65
+ let prev = new Array(blen + 1);
66
+ let curr = new Array(blen + 1);
67
+ for (let j = 0; j <= blen; j++) prev[j] = j;
68
+ for (let i = 1; i <= alen; i++) {
69
+ curr[0] = i;
70
+ let rowMin = i;
71
+ for (let j = 1; j <= blen; j++) {
72
+ const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
73
+ let v = Math.min(
74
+ prev[j] + 1,
75
+ curr[j - 1] + 1,
76
+ prev[j - 1] + cost,
77
+ );
78
+ if (
79
+ i > 1 &&
80
+ j > 1 &&
81
+ a.charCodeAt(i - 1) === b.charCodeAt(j - 2) &&
82
+ a.charCodeAt(i - 2) === b.charCodeAt(j - 1)
83
+ ) {
84
+ v = Math.min(v, prev2[j - 2] + 1);
85
+ }
86
+ curr[j] = v;
87
+ if (v < rowMin) rowMin = v;
88
+ }
89
+ if (rowMin > max) return max + 1;
90
+ prev2 = prev;
91
+ prev = curr;
92
+ curr = new Array(blen + 1);
93
+ }
94
+ return prev[blen];
95
+ }
96
+
97
+ // Adaptive distance: no typo tolerance for very short tokens.
98
+ function _ldMaxDistance(tokenLen) {
99
+ if (tokenLen >= 7) return 2;
100
+ if (tokenLen >= 4) return 1;
101
+ return 0;
102
+ }
103
+
104
+ // ── Widget ─────────────────────────────────────────────────────────────────
105
+
106
+ function _ldLocalTr(key, fallback) {
107
+ return (typeof window.t === "function" && window.t(key)) || fallback;
108
+ }
109
+
110
+ function _ldLocalEsc(s) {
111
+ return String(s)
112
+ .replace(/&/g, "&amp;")
113
+ .replace(/</g, "&lt;")
114
+ .replace(/>/g, "&gt;")
115
+ .replace(/"/g, "&quot;")
116
+ .replace(/'/g, "&#39;");
117
+ }
118
+
119
+ function _ldRegexEscape(s) {
120
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
121
+ }
122
+
123
+ function initLocalSearch(contentEl) {
124
+ const mount = document.getElementById("local-search-mount");
125
+ if (!mount) return;
126
+ mount.innerHTML = "";
127
+ mount.classList.add("hidden");
128
+
129
+ const placeholders = contentEl.querySelectorAll("[data-ld-local-search]");
130
+ if (!placeholders.length) return;
131
+ placeholders.forEach((el) => el.remove());
132
+
133
+ // Build the unique-word index once per doc. Used only for Levenshtein variants.
134
+ const wordIndex = (() => {
135
+ const set = new Set();
136
+ const normalized = _ldNormalize(contentEl.textContent);
137
+ normalized.split(" ").forEach((w) => {
138
+ if (w.length >= 3) set.add(w);
139
+ });
140
+ return Array.from(set);
141
+ })();
142
+
143
+ const placeholderTxt = _ldLocalTr(
144
+ "local_search.placeholder",
145
+ "Search in this document…",
146
+ );
147
+ const clearTitle = _ldLocalTr("local_search.clear", "Clear");
148
+
149
+ const wrapper = document.createElement("div");
150
+ wrapper.className =
151
+ "no-print mt-4 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 overflow-hidden";
152
+ wrapper.innerHTML = `
153
+ <div class="flex items-center gap-2 px-3 py-2 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
154
+ <span class="text-gray-400 select-none" aria-hidden="true">🔎</span>
155
+ <input
156
+ type="text"
157
+ id="ld-local-search-input"
158
+ autocomplete="off"
159
+ class="flex-1 bg-transparent border-0 text-sm text-gray-700 dark:text-gray-300 placeholder:text-gray-400 focus:outline-none focus:ring-0"
160
+ placeholder="${_ldLocalEsc(placeholderTxt)}"
161
+ />
162
+ <button
163
+ type="button"
164
+ id="ld-local-search-clear"
165
+ class="hidden text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 shrink-0 px-1"
166
+ title="${_ldLocalEsc(clearTitle)}"
167
+ aria-label="${_ldLocalEsc(clearTitle)}"
168
+ >✕</button>
169
+ </div>
170
+ <div id="ld-local-search-results" class="hidden">
171
+ <div id="ld-local-search-title" class="px-3 py-1.5 text-xs font-medium text-gray-500 dark:text-gray-400 border-b border-gray-100 dark:border-gray-800"></div>
172
+ <ol id="ld-local-search-list" class="max-h-40 overflow-y-auto divide-y divide-gray-100 dark:divide-gray-800 list-none m-0 p-0"></ol>
173
+ </div>
174
+ `;
175
+ mount.appendChild(wrapper);
176
+ mount.classList.remove("hidden");
177
+
178
+ const input = wrapper.querySelector("#ld-local-search-input");
179
+ const clearBtn = wrapper.querySelector("#ld-local-search-clear");
180
+ const results = wrapper.querySelector("#ld-local-search-results");
181
+ const titleEl = wrapper.querySelector("#ld-local-search-title");
182
+ const listEl = wrapper.querySelector("#ld-local-search-list");
183
+
184
+ let timer = null;
185
+
186
+ function clearHighlights() {
187
+ contentEl.querySelectorAll("mark.ld-local-mark").forEach((m) => {
188
+ const parent = m.parentNode;
189
+ while (m.firstChild) parent.insertBefore(m.firstChild, m);
190
+ parent.removeChild(m);
191
+ });
192
+ contentEl.normalize();
193
+ }
194
+
195
+ // Given a query token, return the set of terms to highlight for that token.
196
+ // - Always includes the token itself (substring match on normalized text).
197
+ // - Adds Levenshtein variants (distance 1-2 depending on token length) found in wordIndex.
198
+ function findVariants(token, normalizedContent) {
199
+ const variants = new Set();
200
+ if (normalizedContent.includes(token)) variants.add(token);
201
+ const maxDist = _ldMaxDistance(token.length);
202
+ if (maxDist === 0) return variants;
203
+ const firstChar = token[0];
204
+ for (const word of wordIndex) {
205
+ if (word === token) continue;
206
+ if (Math.abs(word.length - token.length) > maxDist) continue;
207
+ if (token.length >= 3 && word[0] !== firstChar) continue;
208
+ const d = _ldDamerauLevenshtein(token, word, maxDist);
209
+ if (d <= maxDist) variants.add(word);
210
+ }
211
+ return variants;
212
+ }
213
+
214
+ function runSearch(q) {
215
+ clearHighlights();
216
+ if (!q) {
217
+ results.classList.add("hidden");
218
+ clearBtn.classList.add("hidden");
219
+ listEl.innerHTML = "";
220
+ return;
221
+ }
222
+ clearBtn.classList.remove("hidden");
223
+
224
+ const normalizedQuery = _ldNormalize(q);
225
+ const tokens = normalizedQuery.split(" ").filter(Boolean);
226
+ if (!tokens.length) {
227
+ results.classList.add("hidden");
228
+ return;
229
+ }
230
+
231
+ // Pre-compute normalized content once (lightweight, reused for substring tests)
232
+ const normalizedContent = _ldNormalize(contentEl.textContent);
233
+
234
+ // For each token, gather the set of terms to highlight
235
+ const tokenVariants = tokens.map((t) => findVariants(t, normalizedContent));
236
+
237
+ // If no token has any variant, nothing to do
238
+ const hasAny = tokenVariants.some((s) => s.size > 0);
239
+ if (!hasAny) {
240
+ titleEl.textContent = _ldLocalTr(
241
+ "local_search.no_results",
242
+ "No matches found.",
243
+ );
244
+ listEl.innerHTML = "";
245
+ results.classList.remove("hidden");
246
+ return;
247
+ }
248
+
249
+ // Build the highlight regex: union of all variants. Longer first to ensure
250
+ // longest match wins. Word-boundary applied to Levenshtein variants only
251
+ // (so we don't highlight "date" inside "updated" by way of the typo path).
252
+ const allVariants = new Set();
253
+ tokenVariants.forEach((vs, i) => {
254
+ vs.forEach((v) => {
255
+ const isToken = v === tokens[i];
256
+ allVariants.add(isToken ? v : "\\b" + _ldRegexEscape(v) + "\\b");
257
+ });
258
+ });
259
+ // Deduplicate by original string (without \b wrappers) so we don't double-match
260
+ const parts = Array.from(allVariants).sort((a, b) => b.length - a.length);
261
+ const re = new RegExp("(" + parts.map((p) =>
262
+ p.startsWith("\\b") ? p : _ldRegexEscape(p),
263
+ ).join("|") + ")", "g");
264
+
265
+ // Walk text nodes; for each, normalize with a map, find regex matches on
266
+ // normalized, remap to original slice, wrap with a <mark>.
267
+ const walker = document.createTreeWalker(contentEl, NodeFilter.SHOW_TEXT, {
268
+ acceptNode(n) {
269
+ if (n.parentElement && n.parentElement.closest("mark")) {
270
+ return NodeFilter.FILTER_REJECT;
271
+ }
272
+ return NodeFilter.FILTER_ACCEPT;
273
+ },
274
+ });
275
+ const textNodes = [];
276
+ while (walker.nextNode()) textNodes.push(walker.currentNode);
277
+
278
+ let matchIdx = 0;
279
+ // block → Set<tokenIndex> of which query tokens matched inside that block
280
+ const blockCoverage = new Map();
281
+
282
+ textNodes.forEach((node) => {
283
+ const original = node.textContent;
284
+ const { normalized, map } = _ldNormalizeWithMap(original);
285
+ if (!normalized) return;
286
+
287
+ const localMatches = [];
288
+ let m;
289
+ re.lastIndex = 0;
290
+ while ((m = re.exec(normalized)) !== null) {
291
+ if (m[0].length === 0) {
292
+ re.lastIndex++;
293
+ continue;
294
+ }
295
+ localMatches.push({
296
+ start: m.index,
297
+ end: m.index + m[0].length,
298
+ term: m[0],
299
+ });
300
+ }
301
+ if (!localMatches.length) return;
302
+
303
+ // Remap and wrap
304
+ const block =
305
+ node.parentElement?.closest(
306
+ "p, li, td, h1, h2, h3, h4, h5, h6, pre, blockquote",
307
+ ) || node.parentElement;
308
+
309
+ let out = "";
310
+ let cursor = 0;
311
+ localMatches.forEach((mm) => {
312
+ const origStart = map[mm.start];
313
+ const origEnd = map[mm.end];
314
+ if (origStart < cursor) return; // overlap, skip
315
+ out += _ldLocalEsc(original.slice(cursor, origStart));
316
+ out += `<mark class="ld-local-mark" id="ld-local-match-${matchIdx++}">${_ldLocalEsc(original.slice(origStart, origEnd))}</mark>`;
317
+ cursor = origEnd;
318
+
319
+ if (block) {
320
+ if (!blockCoverage.has(block)) blockCoverage.set(block, new Set());
321
+ const cov = blockCoverage.get(block);
322
+ tokenVariants.forEach((vs, ti) => {
323
+ if (vs.has(mm.term)) cov.add(ti);
324
+ });
325
+ }
326
+ });
327
+ out += _ldLocalEsc(original.slice(cursor));
328
+
329
+ const span = document.createElement("span");
330
+ span.innerHTML = out;
331
+ node.parentNode.replaceChild(span, node);
332
+ });
333
+
334
+ // AND mode: keep blocks that contain all tokens. OR fallback if none.
335
+ const totalTokens = tokens.length;
336
+ const andBlocks = [];
337
+ for (const [block, cov] of blockCoverage.entries()) {
338
+ if (cov.size === totalTokens) andBlocks.push(block);
339
+ }
340
+
341
+ let marksToList;
342
+ if (andBlocks.length > 0) {
343
+ const ordered = Array.from(
344
+ contentEl.querySelectorAll("mark.ld-local-mark"),
345
+ );
346
+ const andBlockSet = new Set(andBlocks);
347
+ marksToList = ordered.filter((mk) => {
348
+ const parentBlock =
349
+ mk.closest("p, li, td, h1, h2, h3, h4, h5, h6, pre, blockquote") ||
350
+ mk.parentElement;
351
+ return andBlockSet.has(parentBlock);
352
+ });
353
+ } else {
354
+ marksToList = Array.from(
355
+ contentEl.querySelectorAll("mark.ld-local-mark"),
356
+ );
357
+ }
358
+
359
+ if (!marksToList.length) {
360
+ titleEl.textContent = _ldLocalTr(
361
+ "local_search.no_results",
362
+ "No matches found.",
363
+ );
364
+ listEl.innerHTML = "";
365
+ results.classList.remove("hidden");
366
+ return;
367
+ }
368
+
369
+ const label =
370
+ marksToList.length === 1
371
+ ? _ldLocalTr("local_search.count_singular", "{count} match").replace(
372
+ "{count}",
373
+ marksToList.length,
374
+ )
375
+ : _ldLocalTr("local_search.count_plural", "{count} matches").replace(
376
+ "{count}",
377
+ marksToList.length,
378
+ );
379
+ titleEl.textContent = label;
380
+
381
+ const qLower = normalizedQuery;
382
+ listEl.innerHTML = marksToList
383
+ .map((mk, i) => {
384
+ const block =
385
+ mk.closest(
386
+ "p, li, td, h1, h2, h3, h4, h5, h6, pre, blockquote",
387
+ ) || mk.parentElement;
388
+ const full = (block ? block.textContent : mk.textContent).trim();
389
+ const fullNorm = _ldNormalize(full);
390
+ // Find the first variant hit for snippet centering
391
+ let pos = -1;
392
+ for (const vs of tokenVariants) {
393
+ for (const v of vs) {
394
+ const p = fullNorm.indexOf(v);
395
+ if (p >= 0 && (pos < 0 || p < pos)) pos = p;
396
+ }
397
+ }
398
+ if (pos < 0) pos = fullNorm.indexOf(qLower.split(" ")[0]);
399
+ if (pos < 0) pos = 0;
400
+ const start = Math.max(0, pos - 40);
401
+ const end = Math.min(full.length, pos + 80);
402
+ const snippet =
403
+ (start > 0 ? "…" : "") +
404
+ full.slice(start, end) +
405
+ (end < full.length ? "…" : "");
406
+ return `<li>
407
+ <button type="button" data-target="${mk.id}"
408
+ class="ld-local-item w-full text-left px-3 py-1.5 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors text-xs">
409
+ <span class="text-gray-400 font-mono mr-2">${i + 1}.</span>${_ldLocalEsc(snippet)}
410
+ </button>
411
+ </li>`;
412
+ })
413
+ .join("");
414
+ results.classList.remove("hidden");
415
+
416
+ listEl.querySelectorAll(".ld-local-item").forEach((btn) => {
417
+ btn.addEventListener("click", () => {
418
+ scrollToLocalMatch(btn.dataset.target);
419
+ });
420
+ });
421
+ }
422
+
423
+ function scrollToLocalMatch(id) {
424
+ const mark = document.getElementById(id);
425
+ const container = document.getElementById("content-area");
426
+ if (!mark || !container) return;
427
+
428
+ let ancestor = mark.parentElement;
429
+ while (ancestor && ancestor !== container) {
430
+ if (ancestor.tagName === "DETAILS" && !ancestor.open) {
431
+ ancestor.open = true;
432
+ }
433
+ ancestor = ancestor.parentElement;
434
+ }
435
+
436
+ contentEl
437
+ .querySelectorAll("mark.ld-local-mark.ld-local-active")
438
+ .forEach((m) => m.classList.remove("ld-local-active"));
439
+ mark.classList.add("ld-local-active");
440
+
441
+ listEl.querySelectorAll(".ld-local-item").forEach((b) => {
442
+ b.classList.toggle("ld-local-item-active", b.dataset.target === id);
443
+ });
444
+
445
+ const stickyHeader = document.querySelector("#doc-view header");
446
+ const headerHeight = stickyHeader
447
+ ? stickyHeader.getBoundingClientRect().height
448
+ : 0;
449
+ const markTop = mark.getBoundingClientRect().top;
450
+ const containerTop = container.getBoundingClientRect().top;
451
+ const targetOffset =
452
+ headerHeight + (container.clientHeight - headerHeight) / 3;
453
+ container.scrollTop += markTop - containerTop - targetOffset;
454
+ }
455
+
456
+ input.addEventListener("input", () => {
457
+ clearTimeout(timer);
458
+ timer = setTimeout(() => runSearch(input.value.trim()), 250);
459
+ });
460
+
461
+ clearBtn.addEventListener("click", () => {
462
+ input.value = "";
463
+ runSearch("");
464
+ input.focus();
465
+ });
466
+
467
+ input.addEventListener("keydown", (e) => {
468
+ if (e.key === "Escape") {
469
+ input.value = "";
470
+ runSearch("");
471
+ input.blur();
472
+ }
473
+ });
474
+ }
475
+
476
+ window.initLocalSearch = initLocalSearch;