living-documentation 7.2.0 → 7.3.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 -0
- package/dist/src/frontend/i18n/fr.json +2 -0
- package/dist/src/frontend/index.html +11 -17
- 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
|
|
|
@@ -152,6 +152,8 @@
|
|
|
152
152
|
"snippet.link_anchor_label": "Anchor",
|
|
153
153
|
"snippet.link_anchor_placeholder": "my-heading",
|
|
154
154
|
"snippet.link_anchor_hint": "(without #, e.g. my-heading)",
|
|
155
|
+
"snippet.link_anchor_select_hint": "(pick a heading from the document)",
|
|
156
|
+
"snippet.link_anchor_no_headings": "No headings detected in this document",
|
|
155
157
|
"snippet.link_target_doc_label": "Target document",
|
|
156
158
|
"snippet.code_lang_label": "Language",
|
|
157
159
|
"snippet.code_lang_hint": "(e.g. javascript, python, bash…)",
|
|
@@ -152,6 +152,8 @@
|
|
|
152
152
|
"snippet.link_anchor_label": "Ancre",
|
|
153
153
|
"snippet.link_anchor_placeholder": "mon-titre",
|
|
154
154
|
"snippet.link_anchor_hint": "(sans #, ex : mon-titre)",
|
|
155
|
+
"snippet.link_anchor_select_hint": "(choisissez un titre du document)",
|
|
156
|
+
"snippet.link_anchor_no_headings": "Aucun titre détecté dans ce document",
|
|
155
157
|
"snippet.link_target_doc_label": "Document cible",
|
|
156
158
|
"snippet.code_lang_label": "Langage",
|
|
157
159
|
"snippet.code_lang_hint": "(ex : javascript, python, bash…)",
|
|
@@ -1227,18 +1227,15 @@
|
|
|
1227
1227
|
<label
|
|
1228
1228
|
class="block text-xs font-medium text-gray-500 dark:text-gray-400"
|
|
1229
1229
|
><span data-i18n="snippet.link_anchor_label">Anchor</span>
|
|
1230
|
-
<span data-i18n="snippet.
|
|
1231
|
-
>(
|
|
1230
|
+
<span data-i18n="snippet.link_anchor_select_hint" class="font-normal text-gray-400"
|
|
1231
|
+
>(pick a heading from the document)</span
|
|
1232
1232
|
></label
|
|
1233
1233
|
>
|
|
1234
|
-
<
|
|
1234
|
+
<select
|
|
1235
1235
|
id="snip-anchor-id"
|
|
1236
|
-
|
|
1237
|
-
data-i18n-placeholder="snippet.link_anchor_placeholder"
|
|
1238
|
-
placeholder="my-heading"
|
|
1239
|
-
oninput="snippetUpdatePreview()"
|
|
1236
|
+
onchange="snippetUpdatePreview()"
|
|
1240
1237
|
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
|
-
|
|
1238
|
+
></select>
|
|
1242
1239
|
</div>
|
|
1243
1240
|
</div>
|
|
1244
1241
|
|
|
@@ -1252,7 +1249,7 @@
|
|
|
1252
1249
|
>
|
|
1253
1250
|
<select
|
|
1254
1251
|
id="snip-anchor-doc-select"
|
|
1255
|
-
onchange="
|
|
1252
|
+
onchange="snippetAnchorDocChanged()"
|
|
1256
1253
|
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
1254
|
></select>
|
|
1258
1255
|
</div>
|
|
@@ -1275,18 +1272,15 @@
|
|
|
1275
1272
|
<label
|
|
1276
1273
|
class="block text-xs font-medium text-gray-500 dark:text-gray-400"
|
|
1277
1274
|
><span data-i18n="snippet.link_anchor_label">Anchor</span>
|
|
1278
|
-
<span data-i18n="snippet.
|
|
1279
|
-
>(
|
|
1275
|
+
<span data-i18n="snippet.link_anchor_select_hint" class="font-normal text-gray-400"
|
|
1276
|
+
>(pick a heading from the document)</span
|
|
1280
1277
|
></label
|
|
1281
1278
|
>
|
|
1282
|
-
<
|
|
1279
|
+
<select
|
|
1283
1280
|
id="snip-anchor-doc-id"
|
|
1284
|
-
|
|
1285
|
-
data-i18n-placeholder="snippet.link_anchor_placeholder"
|
|
1286
|
-
placeholder="my-heading"
|
|
1287
|
-
oninput="snippetUpdatePreview()"
|
|
1281
|
+
onchange="snippetUpdatePreview()"
|
|
1288
1282
|
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
|
-
|
|
1283
|
+
></select>
|
|
1290
1284
|
</div>
|
|
1291
1285
|
</div>
|
|
1292
1286
|
|
|
@@ -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
|