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.
- package/dist/src/frontend/boot.js +6 -1
- package/dist/src/frontend/documents.js +26 -14
- package/dist/src/frontend/i18n/en.json +2 -3
- package/dist/src/frontend/i18n/fr.json +2 -3
- package/dist/src/frontend/index.html +15 -26
- package/dist/src/frontend/new-doc-modal.js +15 -36
- package/dist/src/frontend/new-folder-modal.js +17 -28
- package/dist/src/frontend/snippets.js +120 -2
- package/dist/starting-doc/2026_04_21_19_47_[General]_tata.md +6 -0
- package/dist/starting-doc/2026_04_21_19_47_[General]_tutu.md +11 -0
- package/dist/starting-doc/2026_04_21_19_52_[General]_titi.md +5 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
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
|
↑ 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.
|
|
1231
|
-
>(
|
|
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
|
-
<
|
|
1229
|
+
<select
|
|
1235
1230
|
id="snip-anchor-id"
|
|
1236
|
-
|
|
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="
|
|
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.
|
|
1279
|
-
>(
|
|
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
|
-
<
|
|
1274
|
+
<select
|
|
1283
1275
|
id="snip-anchor-doc-id"
|
|
1284
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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="
|
|
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">📁</span>
|
|
92
98
|
<span class="text-gray-700 dark:text-gray-300 truncate">${esc(dir.name)}</span>
|
|
93
|
-
</button
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
<
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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">📁</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")
|
|
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
|
-
|
|
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,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
|