living-documentation 7.2.0 → 7.4.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.
@@ -87,5 +87,10 @@ document
87
87
  window.addEventListener("popstate", (e) => {
88
88
  const id =
89
89
  e.state?.docId || new URLSearchParams(location.search).get("doc");
90
- if (id) openDocument(id, true);
90
+ const anchor =
91
+ e.state?.anchor ||
92
+ (location.hash && location.hash.length > 1
93
+ ? location.hash.slice(1)
94
+ : null);
95
+ if (id) openDocument(id, true, false, anchor);
91
96
  });
@@ -43,17 +43,23 @@ async function refreshAnnotationCounts() {
43
43
  }
44
44
  }
45
45
 
46
- async function openDocument(id, skipHistory = false, fromLink = false) {
46
+ async function openDocument(id, skipHistory = false, fromLink = false, anchor = null) {
47
47
  // Track navigation history for breadcrumb trail
48
48
  // fromLink===true : forward navigation via in-doc link → push current to stack
49
+ // (unless target is already in the stack → rewind instead of loop)
49
50
  // fromLink==="restore" : back navigation via history breadcrumb → stack already trimmed, don't touch
50
51
  // fromLink===false : sidebar/direct navigation → reset stack
51
52
  if (fromLink === true && currentDocId && currentDocId !== id) {
52
- const prev = allDocs && allDocs.find((d) => d.id === currentDocId);
53
- navHistory.push({
54
- id: currentDocId,
55
- title: prev ? prev.title : currentDocId,
56
- });
53
+ const existingIdx = navHistory.findIndex((e) => e.id === id);
54
+ if (existingIdx !== -1) {
55
+ navHistory = navHistory.slice(0, existingIdx);
56
+ } else {
57
+ const prev = allDocs && allDocs.find((d) => d.id === currentDocId);
58
+ navHistory.push({
59
+ id: currentDocId,
60
+ title: prev ? prev.title : currentDocId,
61
+ });
62
+ }
57
63
  } else if (!fromLink) {
58
64
  navHistory = [];
59
65
  }
@@ -105,7 +111,8 @@ async function openDocument(id, skipHistory = false, fromLink = false) {
105
111
  if (!skipHistory) {
106
112
  const url = new URL(location.href);
107
113
  url.searchParams.set("doc", id);
108
- history.pushState({ docId: id }, "", url);
114
+ url.hash = anchor ? `#${anchor}` : "";
115
+ history.pushState({ docId: id, anchor: anchor || null }, "", url);
109
116
  }
110
117
 
111
118
  document.getElementById("welcome").classList.add("hidden");
@@ -158,14 +165,16 @@ async function openDocument(id, skipHistory = false, fromLink = false) {
158
165
  hljs.highlightElement(block);
159
166
  });
160
167
 
161
- // Intercept inter-doc links (?doc=X) to stay in SPA and track origin
168
+ // Intercept inter-doc links (?doc=X[#anchor]) to stay in SPA and track origin
162
169
  contentEl.querySelectorAll("a[href]").forEach((a) => {
163
170
  const href = a.getAttribute("href");
164
171
  const m = href && href.match(/[?&]doc=([^&#]+)/);
165
172
  if (!m) return;
173
+ const hashIdx = href.indexOf("#");
174
+ const anchor = hashIdx !== -1 ? href.slice(hashIdx + 1) : null;
166
175
  a.addEventListener("click", (e) => {
167
176
  e.preventDefault();
168
- openDocument(decodeURIComponent(m[1]), false, true);
177
+ openDocument(decodeURIComponent(m[1]), false, true, anchor);
169
178
  });
170
179
  });
171
180
 
@@ -199,10 +208,11 @@ async function openDocument(id, skipHistory = false, fromLink = false) {
199
208
 
200
209
  document.title = doc.title;
201
210
 
202
- // Scroll to anchor if present in URL
203
- const hash = window.location.hash;
204
- if (hash && hash.length > 1) {
205
- scrollToAnchor(hash.slice(1));
211
+ // Scroll to anchor if present (explicit param wins over URL hash)
212
+ const targetAnchor =
213
+ anchor || (window.location.hash ? window.location.hash.slice(1) : "");
214
+ if (targetAnchor) {
215
+ scrollToAnchor(targetAnchor);
206
216
  } else {
207
217
  document.getElementById("content-area").scrollTop = 0;
208
218
  }
@@ -331,9 +341,11 @@ async function saveDocument() {
331
341
  const href = a.getAttribute("href");
332
342
  const m = href && href.match(/[?&]doc=([^&#]+)/);
333
343
  if (!m) return;
344
+ const hashIdx = href.indexOf("#");
345
+ const anchor = hashIdx !== -1 ? href.slice(hashIdx + 1) : null;
334
346
  a.addEventListener("click", (e) => {
335
347
  e.preventDefault();
336
- openDocument(decodeURIComponent(m[1]), false, true);
348
+ openDocument(decodeURIComponent(m[1]), false, true, anchor);
337
349
  });
338
350
  });
339
351
 
@@ -71,8 +71,6 @@
71
71
  "modal.new_folder.location_label": "Location",
72
72
  "modal.new_folder.root": "/ (root)",
73
73
  "modal.new_folder.browse_btn": "Browse…",
74
- "modal.new_folder.select_btn": "Select this folder",
75
- "modal.new_folder.browse_select_btn": "Select",
76
74
  "modal.new_folder.enter_name": "(enter a name)",
77
75
  "modal.new_folder.will_be_created": "Will be created at",
78
76
  "modal.new_folder.create_btn": "Create",
@@ -93,7 +91,6 @@
93
91
  "modal.new_doc.create_folder_btn": "+ Create",
94
92
  "modal.new_doc.error_empty_title": "Please enter a title.",
95
93
  "modal.new_doc.no_subfolders": "No sub-folders",
96
- "modal.new_doc.use_folder_btn": "✓ Use this folder",
97
94
 
98
95
  "modal.diag_link.title": "◇ Link a diagram",
99
96
  "modal.diag_link.existing_radio": "Existing diagram",
@@ -152,6 +149,8 @@
152
149
  "snippet.link_anchor_label": "Anchor",
153
150
  "snippet.link_anchor_placeholder": "my-heading",
154
151
  "snippet.link_anchor_hint": "(without #, e.g. my-heading)",
152
+ "snippet.link_anchor_select_hint": "(pick a heading from the document)",
153
+ "snippet.link_anchor_no_headings": "No headings detected in this document",
155
154
  "snippet.link_target_doc_label": "Target document",
156
155
  "snippet.code_lang_label": "Language",
157
156
  "snippet.code_lang_hint": "(e.g. javascript, python, bash…)",
@@ -71,8 +71,6 @@
71
71
  "modal.new_folder.location_label": "Emplacement",
72
72
  "modal.new_folder.root": "/ (racine)",
73
73
  "modal.new_folder.browse_btn": "Parcourir…",
74
- "modal.new_folder.select_btn": "Sélectionner ce dossier",
75
- "modal.new_folder.browse_select_btn": "Sélectionner",
76
74
  "modal.new_folder.enter_name": "(saisir un nom)",
77
75
  "modal.new_folder.will_be_created": "Sera créé à",
78
76
  "modal.new_folder.create_btn": "Créer",
@@ -93,7 +91,6 @@
93
91
  "modal.new_doc.create_folder_btn": "+ Créer",
94
92
  "modal.new_doc.error_empty_title": "Veuillez saisir un titre.",
95
93
  "modal.new_doc.no_subfolders": "Aucun sous-dossier",
96
- "modal.new_doc.use_folder_btn": "✓ Utiliser ce dossier",
97
94
 
98
95
  "modal.diag_link.title": "◇ Lier un diagramme",
99
96
  "modal.diag_link.existing_radio": "Diagramme existant",
@@ -152,6 +149,8 @@
152
149
  "snippet.link_anchor_label": "Ancre",
153
150
  "snippet.link_anchor_placeholder": "mon-titre",
154
151
  "snippet.link_anchor_hint": "(sans #, ex : mon-titre)",
152
+ "snippet.link_anchor_select_hint": "(choisissez un titre du document)",
153
+ "snippet.link_anchor_no_headings": "Aucun titre détecté dans ce document",
155
154
  "snippet.link_target_doc_label": "Document cible",
156
155
  "snippet.code_lang_label": "Langage",
157
156
  "snippet.code_lang_hint": "(ex : javascript, python, bash…)",
@@ -736,21 +736,15 @@
736
736
  <button
737
737
  id="new-folder-browse-up"
738
738
  onclick="newFolderBrowseUp()"
739
- class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 disabled:opacity-30"
739
+ data-i18n="common.up"
740
+ class="text-xs text-blue-600 dark:text-blue-400 hover:underline disabled:opacity-30 disabled:pointer-events-none shrink-0"
740
741
  >
741
-
742
+ Up
742
743
  </button>
743
744
  <span
744
745
  id="new-folder-browse-path"
745
746
  class="text-xs text-gray-500 dark:text-gray-400 font-mono truncate flex-1"
746
747
  ></span>
747
- <button
748
- onclick="newFolderSelectCurrentLocation()"
749
- data-i18n="modal.new_folder.select_btn"
750
- class="text-xs text-blue-600 dark:text-blue-400 hover:underline shrink-0"
751
- >
752
- Select this folder
753
- </button>
754
748
  </div>
755
749
  <div
756
750
  id="new-folder-browse-list"
@@ -874,6 +868,7 @@
874
868
  <button
875
869
  id="new-doc-browse-up"
876
870
  onclick="newDocBrowseUp()"
871
+ data-i18n="common.up"
877
872
  class="text-xs text-blue-600 dark:text-blue-400 hover:underline disabled:opacity-30 disabled:pointer-events-none shrink-0"
878
873
  >
879
874
  &#8593; Up
@@ -1227,18 +1222,15 @@
1227
1222
  <label
1228
1223
  class="block text-xs font-medium text-gray-500 dark:text-gray-400"
1229
1224
  ><span data-i18n="snippet.link_anchor_label">Anchor</span>
1230
- <span data-i18n="snippet.link_anchor_hint" class="font-normal text-gray-400"
1231
- >(without #, e.g. my-heading)</span
1225
+ <span data-i18n="snippet.link_anchor_select_hint" class="font-normal text-gray-400"
1226
+ >(pick a heading from the document)</span
1232
1227
  ></label
1233
1228
  >
1234
- <input
1229
+ <select
1235
1230
  id="snip-anchor-id"
1236
- type="text"
1237
- data-i18n-placeholder="snippet.link_anchor_placeholder"
1238
- placeholder="my-heading"
1239
- oninput="snippetUpdatePreview()"
1231
+ onchange="snippetUpdatePreview()"
1240
1232
  class="w-full px-3 py-2 text-sm rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
1241
- />
1233
+ ></select>
1242
1234
  </div>
1243
1235
  </div>
1244
1236
 
@@ -1252,7 +1244,7 @@
1252
1244
  >
1253
1245
  <select
1254
1246
  id="snip-anchor-doc-select"
1255
- onchange="snippetUpdatePreview()"
1247
+ onchange="snippetAnchorDocChanged()"
1256
1248
  class="w-full px-3 py-2 text-sm rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
1257
1249
  ></select>
1258
1250
  </div>
@@ -1275,18 +1267,15 @@
1275
1267
  <label
1276
1268
  class="block text-xs font-medium text-gray-500 dark:text-gray-400"
1277
1269
  ><span data-i18n="snippet.link_anchor_label">Anchor</span>
1278
- <span data-i18n="snippet.link_anchor_hint" class="font-normal text-gray-400"
1279
- >(without #, e.g. my-heading)</span
1270
+ <span data-i18n="snippet.link_anchor_select_hint" class="font-normal text-gray-400"
1271
+ >(pick a heading from the document)</span
1280
1272
  ></label
1281
1273
  >
1282
- <input
1274
+ <select
1283
1275
  id="snip-anchor-doc-id"
1284
- type="text"
1285
- data-i18n-placeholder="snippet.link_anchor_placeholder"
1286
- placeholder="my-heading"
1287
- oninput="snippetUpdatePreview()"
1276
+ onchange="snippetUpdatePreview()"
1288
1277
  class="w-full px-3 py-2 text-sm rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
1289
- />
1278
+ ></select>
1290
1279
  </div>
1291
1280
  </div>
1292
1281
 
@@ -78,35 +78,28 @@ async function newDocLoadBrowse(dirPath) {
78
78
  ).then((r) => r.json());
79
79
  _newDocBrowseCurrent = data.current;
80
80
  _newDocBrowseParent = data.parent;
81
+ _newDocSelectedFolder = _newDocAbsToRel(data.current);
82
+
81
83
  document.getElementById("new-doc-browse-path").textContent =
82
84
  data.current;
83
85
  const atRoot = data.current === _newDocDocsFolder;
84
86
  document.getElementById("new-doc-browse-up").disabled = atRoot;
85
-
86
- const rows = data.dirs.map(
87
- (dir) =>
88
- `<div class="flex items-center hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
87
+ document.getElementById("new-doc-folder-display").textContent =
88
+ _newDocSelectedFolder ? "/" + _newDocSelectedFolder : "/ (root)";
89
+ newDocUpdatePreview();
90
+
91
+ list.innerHTML = data.dirs.length
92
+ ? data.dirs
93
+ .map(
94
+ (dir) => `
89
95
  <button data-path="${esc(dir.path)}" onclick="newDocLoadBrowse(this.dataset.path)"
90
- class="flex-1 flex items-center gap-2 px-3 py-2 text-sm text-left">
96
+ class="w-full flex items-center gap-2 px-3 py-2 text-sm text-left hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
91
97
  <span class="text-gray-400 shrink-0">&#128193;</span>
92
98
  <span class="text-gray-700 dark:text-gray-300 truncate">${esc(dir.name)}</span>
93
- </button>
94
- <button data-path="${esc(dir.path)}" onclick="newDocSelectFolder(this.dataset.path)"
95
- title="${window.t('modal.new_doc.use_folder_btn')}"
96
- class="shrink-0 text-blue-400 hover:text-blue-600 px-3 py-2 text-sm transition-colors">&#10003;</button>
97
- </div>`,
98
- );
99
-
100
- const selectBtn = `<button onclick="newDocSelectCurrentFolder()"
101
- class="w-full px-3 py-2 text-xs text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-950/30 text-left font-medium border-t border-gray-100 dark:border-gray-800">
102
- ${window.t('modal.new_doc.use_folder_btn')}
103
- </button>`;
104
-
105
- list.innerHTML =
106
- (rows.length
107
- ? rows.join("")
108
- : `<p class="px-3 py-3 text-xs text-gray-400 text-center">${window.t('modal.new_doc.no_subfolders')}</p>`) +
109
- selectBtn;
99
+ </button>`,
100
+ )
101
+ .join("")
102
+ : `<p class="px-3 py-3 text-xs text-gray-400 text-center">${window.t('modal.new_doc.no_subfolders')}</p>`;
110
103
  } catch {
111
104
  list.innerHTML =
112
105
  `<p class="px-3 py-4 text-xs text-red-400 text-center">${window.t('common.cannot_read_dir')}</p>`;
@@ -127,19 +120,6 @@ function _newDocAbsToRel(absPath) {
127
120
  return absPath;
128
121
  }
129
122
 
130
- function newDocSelectFolder(absPath) {
131
- _newDocSelectedFolder = _newDocAbsToRel(absPath);
132
- _newDocBrowseCurrent = absPath;
133
- document.getElementById("new-doc-folder-display").textContent =
134
- _newDocSelectedFolder ? "/" + _newDocSelectedFolder : "/ (root)";
135
- document.getElementById("new-doc-browser").classList.add("hidden");
136
- newDocUpdatePreview();
137
- }
138
-
139
- function newDocSelectCurrentFolder() {
140
- newDocSelectFolder(_newDocBrowseCurrent || _newDocDocsFolder);
141
- }
142
-
143
123
  function newDocCreateFolder() {
144
124
  const name = document
145
125
  .getElementById("new-doc-new-folder-name")
@@ -152,7 +132,6 @@ function newDocCreateFolder() {
152
132
  document.getElementById("new-doc-folder-display").textContent =
153
133
  "/" + newRelPath;
154
134
  document.getElementById("new-doc-new-folder-name").value = "";
155
- document.getElementById("new-doc-browser").classList.add("hidden");
156
135
  newDocUpdatePreview();
157
136
  }
158
137
 
@@ -69,24 +69,32 @@ async function newFolderLoadBrowse(dirPath) {
69
69
  ).then((r) => r.json());
70
70
  _newFolderBrowseCurrent = data.current;
71
71
  _newFolderBrowseParent = data.parent;
72
+ _newFolderSelectedPath = data.current;
73
+
72
74
  document.getElementById("new-folder-browse-path").textContent =
73
75
  data.current;
74
76
  const atRoot = data.current === _newFolderDocsFolder;
75
77
  document.getElementById("new-folder-browse-up").disabled = atRoot;
76
78
 
79
+ const rel = data.current.startsWith(_newFolderDocsFolder + "/")
80
+ ? data.current.slice(_newFolderDocsFolder.length)
81
+ : data.current === _newFolderDocsFolder
82
+ ? ""
83
+ : data.current;
84
+ document.getElementById("new-folder-location-display").textContent = rel
85
+ ? rel
86
+ : "/ (root)";
87
+ newFolderUpdatePreview();
88
+
77
89
  list.innerHTML = data.dirs.length
78
90
  ? data.dirs
79
91
  .map(
80
92
  (dir) => `
81
- <div class="flex items-center hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
82
- <button data-path="${esc(dir.path)}" onclick="newFolderLoadBrowse(this.dataset.path)"
83
- class="flex-1 flex items-center gap-2 px-3 py-2 text-sm text-left">
84
- <span class="text-gray-400 shrink-0">&#128193;</span>
85
- <span class="truncate text-gray-700 dark:text-gray-300">${esc(dir.name)}</span>
86
- </button>
87
- <button data-path="${esc(dir.path)}" onclick="newFolderSelectFolder(this.dataset.path)"
88
- class="px-3 py-2 text-xs text-blue-600 dark:text-blue-400 hover:underline shrink-0">${window.t('modal.new_folder.browse_select_btn')}</button>
89
- </div>`,
93
+ <button data-path="${esc(dir.path)}" onclick="newFolderLoadBrowse(this.dataset.path)"
94
+ class="w-full flex items-center gap-2 px-3 py-2 text-sm text-left hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
95
+ <span class="text-gray-400 shrink-0">&#128193;</span>
96
+ <span class="truncate text-gray-700 dark:text-gray-300">${esc(dir.name)}</span>
97
+ </button>`,
90
98
  )
91
99
  .join("")
92
100
  : `<p class="px-3 py-3 text-xs text-gray-400 text-center">${window.t('modal.new_folder.no_subfolders')}</p>`;
@@ -100,25 +108,6 @@ function newFolderBrowseUp() {
100
108
  if (_newFolderBrowseParent) newFolderLoadBrowse(_newFolderBrowseParent);
101
109
  }
102
110
 
103
- function newFolderSelectCurrentLocation() {
104
- newFolderSelectFolder(_newFolderBrowseCurrent || _newFolderDocsFolder);
105
- }
106
-
107
- function newFolderSelectFolder(absPath) {
108
- _newFolderSelectedPath = absPath;
109
- _newFolderBrowseCurrent = absPath;
110
- const rel = absPath.startsWith(_newFolderDocsFolder + "/")
111
- ? absPath.slice(_newFolderDocsFolder.length)
112
- : absPath === _newFolderDocsFolder
113
- ? ""
114
- : absPath;
115
- document.getElementById("new-folder-location-display").textContent = rel
116
- ? rel
117
- : "/ (root)";
118
- document.getElementById("new-folder-browser").classList.add("hidden");
119
- newFolderUpdatePreview();
120
- }
121
-
122
111
  function newFolderUpdatePreview() {
123
112
  const name = document.getElementById("new-folder-name").value.trim();
124
113
  const previewEl = document.getElementById("new-folder-preview");
@@ -58,6 +58,96 @@ function colorTextPickSwatch(btn) {
58
58
  snippetUpdatePreview();
59
59
  }
60
60
 
61
+ function _stripMdInline(s) {
62
+ return s
63
+ .replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
64
+ .replace(/`([^`]+)`/g, "$1")
65
+ .replace(/\*\*([^*]+)\*\*/g, "$1")
66
+ .replace(/__([^_]+)__/g, "$1")
67
+ .replace(/(^|[^*])\*([^*]+)\*(?!\*)/g, "$1$2")
68
+ .replace(/(^|[^_])_([^_]+)_(?!_)/g, "$1$2")
69
+ .trim();
70
+ }
71
+
72
+ function _slugifyHeading(text) {
73
+ return text
74
+ .toLowerCase()
75
+ .replace(/[^\w\s-]/g, "")
76
+ .trim()
77
+ .replace(/\s+/g, "-");
78
+ }
79
+
80
+ function _extractHeadingsFromMarkdown(content) {
81
+ const out = [];
82
+ const lines = (content || "").split("\n");
83
+ let inFence = false;
84
+ for (const line of lines) {
85
+ if (/^```/.test(line)) {
86
+ inFence = !inFence;
87
+ continue;
88
+ }
89
+ if (inFence) continue;
90
+ const m = line.match(/^(#{1,6})\s+(.+?)\s*#*\s*$/);
91
+ if (!m) continue;
92
+ const text = _stripMdInline(m[2]);
93
+ const slug = _slugifyHeading(text);
94
+ if (slug) out.push({ level: m[1].length, text, slug });
95
+ }
96
+ return out;
97
+ }
98
+
99
+ function _collectEditorHeadings() {
100
+ const editor = document.getElementById("doc-editor");
101
+ return _extractHeadingsFromMarkdown(editor ? editor.value : "");
102
+ }
103
+
104
+ function _renderAnchorOptions(sel, headings, emptyKey) {
105
+ if (!sel) return;
106
+ if (headings.length === 0) {
107
+ sel.innerHTML = `<option value="" disabled selected>${window.t(emptyKey)}</option>`;
108
+ return;
109
+ }
110
+ sel.innerHTML = headings
111
+ .map((h) => {
112
+ const indent = "· ".repeat(Math.max(0, h.level - 1));
113
+ return `<option value="${esc(h.slug)}">${esc(indent + h.text)}</option>`;
114
+ })
115
+ .join("");
116
+ }
117
+
118
+ function _populateAnchorSelect() {
119
+ _renderAnchorOptions(
120
+ document.getElementById("snip-anchor-id"),
121
+ _collectEditorHeadings(),
122
+ 'snippet.link_anchor_no_headings',
123
+ );
124
+ }
125
+
126
+ async function snippetAnchorDocChanged() {
127
+ const docSel = document.getElementById("snip-anchor-doc-select");
128
+ const anchorSel = document.getElementById("snip-anchor-doc-id");
129
+ if (!docSel || !anchorSel) return;
130
+ const docId = docSel.value;
131
+ if (!docId) {
132
+ _renderAnchorOptions(anchorSel, [], 'snippet.link_anchor_no_headings');
133
+ snippetUpdatePreview();
134
+ return;
135
+ }
136
+ anchorSel.innerHTML = `<option value="" disabled selected>${window.t('common.loading')}</option>`;
137
+ try {
138
+ const doc = await fetch("/api/documents/" + encodeURIComponent(docId))
139
+ .then((r) => {
140
+ if (!r.ok) throw new Error(r.statusText);
141
+ return r.json();
142
+ });
143
+ const headings = _extractHeadingsFromMarkdown(doc.content || "");
144
+ _renderAnchorOptions(anchorSel, headings, 'snippet.link_anchor_no_headings');
145
+ } catch {
146
+ _renderAnchorOptions(anchorSel, [], 'snippet.link_anchor_no_headings');
147
+ }
148
+ snippetUpdatePreview();
149
+ }
150
+
61
151
  function openSnippetsModal() {
62
152
  const editor = document.getElementById("doc-editor");
63
153
  _snippetSelStart = editor.selectionStart;
@@ -68,6 +158,8 @@ function openSnippetsModal() {
68
158
  .join("");
69
159
  document.getElementById("snip-doc-select").innerHTML = docOpts;
70
160
  document.getElementById("snip-anchor-doc-select").innerHTML = docOpts;
161
+ _populateAnchorSelect();
162
+ snippetAnchorDocChanged();
71
163
 
72
164
  const msgEl = document.getElementById("snippet-detect-msg");
73
165
  const selectedText = editor.value.slice(
@@ -443,7 +535,18 @@ function parseAndFillSnippet(text, type) {
443
535
  const m = t.match(/^\[([\s\S]*?)\]\(#([\s\S]*?)\)$/);
444
536
  if (m) {
445
537
  document.getElementById("snip-anchor-text").value = m[1];
446
- document.getElementById("snip-anchor-id").value = m[2];
538
+ const sel = document.getElementById("snip-anchor-id");
539
+ const wanted = m[2];
540
+ const hasOpt = Array.from(sel.options).some(
541
+ (o) => o.value === wanted,
542
+ );
543
+ if (!hasOpt) {
544
+ const opt = document.createElement("option");
545
+ opt.value = wanted;
546
+ opt.textContent = wanted;
547
+ sel.insertBefore(opt, sel.firstChild);
548
+ }
549
+ sel.value = wanted;
447
550
  }
448
551
  break;
449
552
  }
@@ -460,7 +563,22 @@ function parseAndFillSnippet(text, type) {
460
563
  }
461
564
  }
462
565
  document.getElementById("snip-anchor-doc-text").value = m[1];
463
- document.getElementById("snip-anchor-doc-id").value = m[3];
566
+ const wanted = m[3];
567
+ snippetAnchorDocChanged().then(() => {
568
+ const anchorSel = document.getElementById("snip-anchor-doc-id");
569
+ if (!anchorSel) return;
570
+ const hasOpt = Array.from(anchorSel.options).some(
571
+ (o) => o.value === wanted,
572
+ );
573
+ if (!hasOpt) {
574
+ const opt = document.createElement("option");
575
+ opt.value = wanted;
576
+ opt.textContent = wanted;
577
+ anchorSel.insertBefore(opt, anchorSel.firstChild);
578
+ }
579
+ anchorSel.value = wanted;
580
+ snippetUpdatePreview();
581
+ });
464
582
  }
465
583
  break;
466
584
  }
@@ -0,0 +1,6 @@
1
+ # tata
2
+ [vers tutu](?doc=2026_04_21_19_47_%255BGeneral%255D_tutu#tutu)
3
+
4
+ <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
5
+
6
+ # tata-anchored
@@ -0,0 +1,11 @@
1
+ # tutu
2
+
3
+ [Vers Titi](?doc=2026_04_21_19_52_%255BGeneral%255D_titi#titi)
4
+
5
+ [Vers Tata](?doc=2026_04_21_19_47_%255BGeneral%255D_tata#tata)
6
+
7
+ [Vers tata anchored](?doc=2026_04_21_19_47_%255BGeneral%255D_tata#tata-anchored)
8
+
9
+ <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
10
+
11
+ # tutu-anchored
@@ -0,0 +1,5 @@
1
+ # Titi
2
+
3
+ [Tata](?doc=2026_04_21_19_47_%255BGeneral%255D_tata)
4
+
5
+ [Tata anchored](?doc=2026_04_21_19_47_%255BGeneral%255D_tata#tata-anchored)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "living-documentation",
3
- "version": "7.2.0",
3
+ "version": "7.4.0",
4
4
  "description": "A CLI tool that serves a local Markdown documentation viewer",
5
5
  "main": "dist/src/server.js",
6
6
  "bin": {