living-ai-documentation 1.4.0 → 1.8.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/documents.js +43 -25
- package/dist/src/frontend/i18n/en.json +37 -0
- package/dist/src/frontend/i18n/fr.json +37 -0
- package/dist/src/frontend/index.html +86 -3
- package/dist/src/frontend/inline-snippet-edit.js +416 -0
- package/dist/src/frontend/snippets.js +287 -44
- package/dist/src/frontend/validate.js +23 -8
- package/dist/src/lib/status.js +1 -1
- package/dist/src/lib/status.js.map +1 -1
- package/package.json +1 -1
|
@@ -60,6 +60,10 @@ function _wireDocContent(html) {
|
|
|
60
60
|
wrapper.appendChild(t);
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
if (typeof initInlineSnippetEditing === "function") {
|
|
64
|
+
initInlineSnippetEditing(contentEl);
|
|
65
|
+
}
|
|
66
|
+
|
|
63
67
|
const notice = document.getElementById("search-notice");
|
|
64
68
|
const isMetaQuery =
|
|
65
69
|
typeof searchQuery === "string" &&
|
|
@@ -429,6 +433,44 @@ async function confirmDeleteDocument() {
|
|
|
429
433
|
}
|
|
430
434
|
|
|
431
435
|
// ── Save (in-place edit) ─────────────────────────────────────────────────────
|
|
436
|
+
async function saveCurrentDocumentContent(content) {
|
|
437
|
+
if (!currentDocId) return;
|
|
438
|
+
const res = await fetch("/api/documents/" + currentDocId, {
|
|
439
|
+
method: "PUT",
|
|
440
|
+
headers: { "Content-Type": "application/json" },
|
|
441
|
+
body: JSON.stringify({ content }),
|
|
442
|
+
});
|
|
443
|
+
if (!res.ok) throw new Error(await res.text());
|
|
444
|
+
|
|
445
|
+
currentDocContent = content;
|
|
446
|
+
|
|
447
|
+
// Re-fetch rendered HTML and update view
|
|
448
|
+
const doc = await fetch("/api/documents/" + currentDocId).then((r) =>
|
|
449
|
+
r.json(),
|
|
450
|
+
);
|
|
451
|
+
_lastDocHtml = doc.html;
|
|
452
|
+
_lastDocIdRendered = currentDocId;
|
|
453
|
+
_wireDocContent(doc.html);
|
|
454
|
+
|
|
455
|
+
if (typeof applyAnnotationHighlights === "function") {
|
|
456
|
+
applyAnnotationHighlights();
|
|
457
|
+
}
|
|
458
|
+
if (typeof renderElevator === "function") renderElevator();
|
|
459
|
+
|
|
460
|
+
const fileLinkMatches = content.match(/\]\(\s*\.?\/files\/[^)\s]+/g);
|
|
461
|
+
const fileLinkCount = fileLinkMatches ? fileLinkMatches.length : 0;
|
|
462
|
+
if (fileLinkCount > 0) fileAttachmentCounts[currentDocId] = fileLinkCount;
|
|
463
|
+
else delete fileAttachmentCounts[currentDocId];
|
|
464
|
+
refreshSidebar();
|
|
465
|
+
|
|
466
|
+
if (typeof updateValidateButtonForCurrentDoc === "function") {
|
|
467
|
+
updateValidateButtonForCurrentDoc();
|
|
468
|
+
}
|
|
469
|
+
if (typeof loadMetadataReport === "function") {
|
|
470
|
+
loadMetadataReport(currentDocId);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
432
474
|
async function saveDocument() {
|
|
433
475
|
if (!currentDocId) return;
|
|
434
476
|
const content = document.getElementById("doc-editor").value;
|
|
@@ -437,31 +479,7 @@ async function saveDocument() {
|
|
|
437
479
|
msgEl.className = "text-xs text-gray-400";
|
|
438
480
|
|
|
439
481
|
try {
|
|
440
|
-
|
|
441
|
-
method: "PUT",
|
|
442
|
-
headers: { "Content-Type": "application/json" },
|
|
443
|
-
body: JSON.stringify({ content }),
|
|
444
|
-
});
|
|
445
|
-
if (!res.ok) throw new Error(await res.text());
|
|
446
|
-
|
|
447
|
-
currentDocContent = content;
|
|
448
|
-
|
|
449
|
-
// Re-fetch rendered HTML and update view
|
|
450
|
-
const doc = await fetch("/api/documents/" + currentDocId).then((r) =>
|
|
451
|
-
r.json(),
|
|
452
|
-
);
|
|
453
|
-
_lastDocHtml = doc.html;
|
|
454
|
-
_lastDocIdRendered = currentDocId;
|
|
455
|
-
_wireDocContent(doc.html);
|
|
456
|
-
|
|
457
|
-
applyAnnotationHighlights();
|
|
458
|
-
renderElevator();
|
|
459
|
-
|
|
460
|
-
const fileLinkMatches = content.match(/\]\(\s*\.?\/files\/[^)\s]+/g);
|
|
461
|
-
const fileLinkCount = fileLinkMatches ? fileLinkMatches.length : 0;
|
|
462
|
-
if (fileLinkCount > 0) fileAttachmentCounts[currentDocId] = fileLinkCount;
|
|
463
|
-
else delete fileAttachmentCounts[currentDocId];
|
|
464
|
-
refreshSidebar();
|
|
482
|
+
await saveCurrentDocumentContent(content);
|
|
465
483
|
|
|
466
484
|
exitEditMode();
|
|
467
485
|
} catch (err) {
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
"doc.validate_btn": "Validate",
|
|
71
71
|
"doc.validate_title": "Validate this document?",
|
|
72
72
|
"doc.validate_message": "The frontmatter status will change from \"To be validated\" to \"Accepted\".",
|
|
73
|
+
"doc.validate_worklog_message": "The frontmatter status will change from \"To be validated\" to \"Done\".",
|
|
73
74
|
"doc.validate_detail_low_accuracy": "Reliability is currently {accuracy}. Confirming will also re-baseline source-file hashes, bringing reliability back to 100%.",
|
|
74
75
|
"doc.validate_confirm": "Validate",
|
|
75
76
|
"doc.validate_failed": "Validation failed: ",
|
|
@@ -282,8 +283,16 @@
|
|
|
282
283
|
"snippet.link_anchor_select_hint": "(pick a heading from the document)",
|
|
283
284
|
"snippet.link_anchor_no_headings": "No headings detected in this document",
|
|
284
285
|
"snippet.link_target_doc_label": "Target document",
|
|
286
|
+
"snippet.ordered_list_content_label": "Items",
|
|
287
|
+
"snippet.ordered_list_content_placeholder": "One item per line…",
|
|
288
|
+
"snippet.unordered_list_content_label": "Items",
|
|
289
|
+
"snippet.unordered_list_content_placeholder": "One item per line…",
|
|
285
290
|
"snippet.code_lang_label": "Language",
|
|
286
291
|
"snippet.code_lang_hint": "(e.g. javascript, python, bash…)",
|
|
292
|
+
"snippet.code_content_label": "Code",
|
|
293
|
+
"snippet.code_content_placeholder": "Paste or edit code here…",
|
|
294
|
+
"snippet.blockquote_content_label": "Quote",
|
|
295
|
+
"snippet.blockquote_content_placeholder": "Quote text…",
|
|
287
296
|
"snippet.image_alt_label": "Alt text",
|
|
288
297
|
"snippet.image_alt_placeholder": "Image description",
|
|
289
298
|
"snippet.image_url_label": "Image URL",
|
|
@@ -295,6 +304,34 @@
|
|
|
295
304
|
"snippet.tree_add_btn": "+ Add item",
|
|
296
305
|
"snippet.markdown_preview_label": "Markdown preview",
|
|
297
306
|
"snippet.insert_btn": "Insert",
|
|
307
|
+
"snippet.inline_edit_btn": "Edit inline",
|
|
308
|
+
"snippet.inline_modal_title": "Edit inline snippet",
|
|
309
|
+
"snippet.inline_save_btn": "Save",
|
|
310
|
+
"snippet.inline_save_failed": "Inline edit failed: ",
|
|
311
|
+
"snippet.inline_delete_btn": "Delete block",
|
|
312
|
+
"snippet.inline_delete_title": "Delete block",
|
|
313
|
+
"snippet.inline_delete_message": "Delete this block from the document?",
|
|
314
|
+
"snippet.inline_delete_detail": "This action will remove the selected Markdown block.",
|
|
315
|
+
"snippet.inline_delete_confirm_btn": "Delete",
|
|
316
|
+
"snippet.inline_delete_failed": "Inline block deletion failed: ",
|
|
317
|
+
"snippet.inline_insert_btn": "Insert a snippet here",
|
|
318
|
+
"snippet.inline_insert_modal_title": "Insert a snippet",
|
|
319
|
+
"snippet.inline_insert_failed": "Inline insertion failed: ",
|
|
320
|
+
"snippet.inline_edit_btn_table": "Edit table",
|
|
321
|
+
"snippet.inline_edit_btn_code_block": "Edit code block",
|
|
322
|
+
"snippet.inline_edit_btn_blockquote": "Edit blockquote",
|
|
323
|
+
"snippet.inline_edit_btn_ordered_list": "Edit numbered list",
|
|
324
|
+
"snippet.inline_edit_btn_unordered_list": "Edit bullet list",
|
|
325
|
+
"snippet.inline_edit_btn_tree": "Edit tree",
|
|
326
|
+
"snippet.inline_edit_btn_colored_section": "Edit colored section",
|
|
327
|
+
"snippet.inline_edit_btn_colored_text": "Edit colored text",
|
|
328
|
+
"snippet.inline_edit_btn_collapsible": "Edit collapsible block",
|
|
329
|
+
"snippet.inline_edit_btn_link": "Edit link",
|
|
330
|
+
"snippet.inline_edit_btn_doc_link": "Edit document link",
|
|
331
|
+
"snippet.inline_edit_btn_anchor_link": "Edit anchor link",
|
|
332
|
+
"snippet.inline_edit_btn_anchor_doc_link": "Edit document + anchor link",
|
|
333
|
+
"snippet.inline_edit_btn_image": "Edit image",
|
|
334
|
+
"snippet.inline_edit_btn_separator": "Edit separator",
|
|
298
335
|
"snippet.diagram_existing": "Existing diagram",
|
|
299
336
|
"snippet.diagram_new": "New diagram",
|
|
300
337
|
"snippet.diagram_select_label": "Select diagram",
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
"doc.validate_btn": "Valider",
|
|
71
71
|
"doc.validate_title": "Valider ce document ?",
|
|
72
72
|
"doc.validate_message": "Le statut frontmatter va passer de « To be validated » à « Accepted ».",
|
|
73
|
+
"doc.validate_worklog_message": "Le statut frontmatter va passer de « To be validated » à « Done ».",
|
|
73
74
|
"doc.validate_detail_low_accuracy": "La fiabilité actuelle est de {accuracy}. Confirmer va aussi réinitialiser les hashes des fichiers source, ramenant la fiabilité à 100 %.",
|
|
74
75
|
"doc.validate_confirm": "Valider",
|
|
75
76
|
"doc.validate_failed": "Échec de la validation : ",
|
|
@@ -282,8 +283,16 @@
|
|
|
282
283
|
"snippet.link_anchor_select_hint": "(choisissez un titre du document)",
|
|
283
284
|
"snippet.link_anchor_no_headings": "Aucun titre détecté dans ce document",
|
|
284
285
|
"snippet.link_target_doc_label": "Document cible",
|
|
286
|
+
"snippet.ordered_list_content_label": "Éléments",
|
|
287
|
+
"snippet.ordered_list_content_placeholder": "Un élément par ligne…",
|
|
288
|
+
"snippet.unordered_list_content_label": "Éléments",
|
|
289
|
+
"snippet.unordered_list_content_placeholder": "Un élément par ligne…",
|
|
285
290
|
"snippet.code_lang_label": "Langage",
|
|
286
291
|
"snippet.code_lang_hint": "(ex : javascript, python, bash…)",
|
|
292
|
+
"snippet.code_content_label": "Code",
|
|
293
|
+
"snippet.code_content_placeholder": "Collez ou modifiez le code ici…",
|
|
294
|
+
"snippet.blockquote_content_label": "Citation",
|
|
295
|
+
"snippet.blockquote_content_placeholder": "Texte de la citation…",
|
|
287
296
|
"snippet.image_alt_label": "Texte alternatif",
|
|
288
297
|
"snippet.image_alt_placeholder": "Description de l'image",
|
|
289
298
|
"snippet.image_url_label": "URL de l'image",
|
|
@@ -295,6 +304,34 @@
|
|
|
295
304
|
"snippet.tree_add_btn": "+ Ajouter un élément",
|
|
296
305
|
"snippet.markdown_preview_label": "Aperçu du markdown",
|
|
297
306
|
"snippet.insert_btn": "Insérer",
|
|
307
|
+
"snippet.inline_edit_btn": "Édition inline",
|
|
308
|
+
"snippet.inline_modal_title": "Modifier le snippet inline",
|
|
309
|
+
"snippet.inline_save_btn": "Enregistrer",
|
|
310
|
+
"snippet.inline_save_failed": "Échec de l'édition inline : ",
|
|
311
|
+
"snippet.inline_delete_btn": "Supprimer le bloc",
|
|
312
|
+
"snippet.inline_delete_title": "Supprimer le bloc",
|
|
313
|
+
"snippet.inline_delete_message": "Supprimer ce bloc du document ?",
|
|
314
|
+
"snippet.inline_delete_detail": "Cette action retirera le bloc Markdown sélectionné.",
|
|
315
|
+
"snippet.inline_delete_confirm_btn": "Supprimer",
|
|
316
|
+
"snippet.inline_delete_failed": "Échec de la suppression inline : ",
|
|
317
|
+
"snippet.inline_insert_btn": "Insérer un snippet ici",
|
|
318
|
+
"snippet.inline_insert_modal_title": "Insérer un snippet",
|
|
319
|
+
"snippet.inline_insert_failed": "Échec de l'insertion inline : ",
|
|
320
|
+
"snippet.inline_edit_btn_table": "Éditer le tableau",
|
|
321
|
+
"snippet.inline_edit_btn_code_block": "Éditer le bloc de code",
|
|
322
|
+
"snippet.inline_edit_btn_blockquote": "Éditer la citation",
|
|
323
|
+
"snippet.inline_edit_btn_ordered_list": "Éditer la liste numérotée",
|
|
324
|
+
"snippet.inline_edit_btn_unordered_list": "Éditer la liste à puces",
|
|
325
|
+
"snippet.inline_edit_btn_tree": "Éditer l'arborescence",
|
|
326
|
+
"snippet.inline_edit_btn_colored_section": "Éditer la section colorée",
|
|
327
|
+
"snippet.inline_edit_btn_colored_text": "Éditer le texte coloré",
|
|
328
|
+
"snippet.inline_edit_btn_collapsible": "Éditer le bloc repliable",
|
|
329
|
+
"snippet.inline_edit_btn_link": "Éditer le lien",
|
|
330
|
+
"snippet.inline_edit_btn_doc_link": "Éditer le lien vers un document",
|
|
331
|
+
"snippet.inline_edit_btn_anchor_link": "Éditer le lien d'ancre",
|
|
332
|
+
"snippet.inline_edit_btn_anchor_doc_link": "Éditer le lien document + ancre",
|
|
333
|
+
"snippet.inline_edit_btn_image": "Éditer l'image",
|
|
334
|
+
"snippet.inline_edit_btn_separator": "Éditer le séparateur",
|
|
298
335
|
"snippet.diagram_existing": "Diagramme existant",
|
|
299
336
|
"snippet.diagram_new": "Nouveau diagramme",
|
|
300
337
|
"snippet.diagram_select_label": "Sélectionner un diagramme",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
<script defer src="/documents.js"></script>
|
|
49
49
|
<script defer src="/misc.js"></script>
|
|
50
50
|
<script defer src="/snippets.js"></script>
|
|
51
|
+
<script defer src="/inline-snippet-edit.js"></script>
|
|
51
52
|
<script defer src="/annotations.js"></script>
|
|
52
53
|
<script defer src="/metadata.js"></script>
|
|
53
54
|
<script defer src="/accuracy-gauge.js"></script>
|
|
@@ -1425,12 +1426,13 @@
|
|
|
1425
1426
|
<div
|
|
1426
1427
|
id="snippets-modal"
|
|
1427
1428
|
class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
|
1428
|
-
|
|
1429
|
+
>
|
|
1429
1430
|
<div
|
|
1431
|
+
id="snippet-modal-card"
|
|
1430
1432
|
class="bg-white dark:bg-gray-900 rounded-xl shadow-xl w-full max-w-lg mx-4 p-6 space-y-5 max-h-[90vh] overflow-y-auto"
|
|
1431
1433
|
>
|
|
1432
1434
|
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-50">
|
|
1433
|
-
<span data-i18n="snippet.modal_title">🧩 Insert a snippet</span>
|
|
1435
|
+
<span id="snippet-modal-title" data-i18n="snippet.modal_title">🧩 Insert a snippet</span>
|
|
1434
1436
|
</h3>
|
|
1435
1437
|
|
|
1436
1438
|
<div
|
|
@@ -1636,6 +1638,44 @@
|
|
|
1636
1638
|
</div>
|
|
1637
1639
|
</div>
|
|
1638
1640
|
|
|
1641
|
+
<!-- Panel: ordered-list -->
|
|
1642
|
+
<div id="snip-panel-ordered-list" class="hidden space-y-3">
|
|
1643
|
+
<div class="space-y-1.5">
|
|
1644
|
+
<label
|
|
1645
|
+
data-i18n="snippet.ordered_list_content_label"
|
|
1646
|
+
class="block text-xs font-medium text-gray-500 dark:text-gray-400"
|
|
1647
|
+
>Items</label
|
|
1648
|
+
>
|
|
1649
|
+
<textarea
|
|
1650
|
+
id="snip-ordered-list-content"
|
|
1651
|
+
rows="10"
|
|
1652
|
+
data-i18n-placeholder="snippet.ordered_list_content_placeholder"
|
|
1653
|
+
placeholder="One item per line…"
|
|
1654
|
+
oninput="snippetUpdatePreview()"
|
|
1655
|
+
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 resize-y leading-relaxed"
|
|
1656
|
+
></textarea>
|
|
1657
|
+
</div>
|
|
1658
|
+
</div>
|
|
1659
|
+
|
|
1660
|
+
<!-- Panel: unordered-list -->
|
|
1661
|
+
<div id="snip-panel-unordered-list" class="hidden space-y-3">
|
|
1662
|
+
<div class="space-y-1.5">
|
|
1663
|
+
<label
|
|
1664
|
+
data-i18n="snippet.unordered_list_content_label"
|
|
1665
|
+
class="block text-xs font-medium text-gray-500 dark:text-gray-400"
|
|
1666
|
+
>Items</label
|
|
1667
|
+
>
|
|
1668
|
+
<textarea
|
|
1669
|
+
id="snip-unordered-list-content"
|
|
1670
|
+
rows="10"
|
|
1671
|
+
data-i18n-placeholder="snippet.unordered_list_content_placeholder"
|
|
1672
|
+
placeholder="One item per line…"
|
|
1673
|
+
oninput="snippetUpdatePreview()"
|
|
1674
|
+
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 resize-y leading-relaxed"
|
|
1675
|
+
></textarea>
|
|
1676
|
+
</div>
|
|
1677
|
+
</div>
|
|
1678
|
+
|
|
1639
1679
|
<!-- Panel: code-block -->
|
|
1640
1680
|
<div id="snip-panel-code-block" class="hidden space-y-3">
|
|
1641
1681
|
<div class="space-y-1.5">
|
|
@@ -1654,6 +1694,40 @@
|
|
|
1654
1694
|
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"
|
|
1655
1695
|
/>
|
|
1656
1696
|
</div>
|
|
1697
|
+
<div class="space-y-1.5">
|
|
1698
|
+
<label
|
|
1699
|
+
data-i18n="snippet.code_content_label"
|
|
1700
|
+
class="block text-xs font-medium text-gray-500 dark:text-gray-400"
|
|
1701
|
+
>Code</label
|
|
1702
|
+
>
|
|
1703
|
+
<textarea
|
|
1704
|
+
id="snip-code-content"
|
|
1705
|
+
rows="12"
|
|
1706
|
+
data-i18n-placeholder="snippet.code_content_placeholder"
|
|
1707
|
+
placeholder="Paste or edit code here…"
|
|
1708
|
+
oninput="snippetUpdatePreview()"
|
|
1709
|
+
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 resize-y font-mono leading-relaxed"
|
|
1710
|
+
></textarea>
|
|
1711
|
+
</div>
|
|
1712
|
+
</div>
|
|
1713
|
+
|
|
1714
|
+
<!-- Panel: blockquote -->
|
|
1715
|
+
<div id="snip-panel-blockquote" class="hidden space-y-3">
|
|
1716
|
+
<div class="space-y-1.5">
|
|
1717
|
+
<label
|
|
1718
|
+
data-i18n="snippet.blockquote_content_label"
|
|
1719
|
+
class="block text-xs font-medium text-gray-500 dark:text-gray-400"
|
|
1720
|
+
>Quote</label
|
|
1721
|
+
>
|
|
1722
|
+
<textarea
|
|
1723
|
+
id="snip-blockquote-content"
|
|
1724
|
+
rows="8"
|
|
1725
|
+
data-i18n-placeholder="snippet.blockquote_content_placeholder"
|
|
1726
|
+
placeholder="Quote text…"
|
|
1727
|
+
oninput="snippetUpdatePreview()"
|
|
1728
|
+
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 resize-y leading-relaxed"
|
|
1729
|
+
></textarea>
|
|
1730
|
+
</div>
|
|
1657
1731
|
</div>
|
|
1658
1732
|
|
|
1659
1733
|
<!-- Panel: image -->
|
|
@@ -2045,7 +2119,7 @@
|
|
|
2045
2119
|
</div>
|
|
2046
2120
|
|
|
2047
2121
|
<!-- Markdown preview -->
|
|
2048
|
-
<div class="space-y-1.5">
|
|
2122
|
+
<div id="snippet-preview-wrap" class="space-y-1.5">
|
|
2049
2123
|
<label
|
|
2050
2124
|
data-i18n="snippet.markdown_preview_label"
|
|
2051
2125
|
class="block text-xs font-medium text-gray-500 dark:text-gray-400"
|
|
@@ -2066,8 +2140,17 @@
|
|
|
2066
2140
|
>
|
|
2067
2141
|
Cancel
|
|
2068
2142
|
</button>
|
|
2143
|
+
<button
|
|
2144
|
+
onclick="deleteInlineSnippetBlock()"
|
|
2145
|
+
id="snippet-delete-btn"
|
|
2146
|
+
data-i18n="snippet.inline_delete_btn"
|
|
2147
|
+
class="hidden text-sm px-4 py-2 rounded-lg border border-red-200 dark:border-red-800 text-red-600 dark:text-red-300 hover:bg-red-50 dark:hover:bg-red-900/30 font-semibold transition-colors"
|
|
2148
|
+
>
|
|
2149
|
+
Delete block
|
|
2150
|
+
</button>
|
|
2069
2151
|
<button
|
|
2070
2152
|
onclick="insertSnippet()"
|
|
2153
|
+
id="snippet-submit-btn"
|
|
2071
2154
|
data-i18n="snippet.insert_btn"
|
|
2072
2155
|
class="text-sm px-4 py-2 rounded-lg bg-blue-600 hover:bg-blue-700 text-white font-semibold transition-colors"
|
|
2073
2156
|
>
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
// ── Inline snippet editing from rendered viewer ─────────────────────────────
|
|
2
|
+
// Right-clicking a rendered fragment that maps to a known snippet offers an
|
|
3
|
+
// inline edit action. The actual form stays the Snippets modal; this file only
|
|
4
|
+
// maps rendered DOM elements back to source Markdown ranges.
|
|
5
|
+
|
|
6
|
+
const _INLINE_SNIPPET_TYPES = new Set([
|
|
7
|
+
"anchor-doc-link",
|
|
8
|
+
"doc-link",
|
|
9
|
+
"anchor-link",
|
|
10
|
+
"image",
|
|
11
|
+
"link",
|
|
12
|
+
"collapsible",
|
|
13
|
+
"colored-text",
|
|
14
|
+
"colored-section",
|
|
15
|
+
"tree",
|
|
16
|
+
"code-block",
|
|
17
|
+
"table",
|
|
18
|
+
"blockquote",
|
|
19
|
+
"separator",
|
|
20
|
+
"ordered-list",
|
|
21
|
+
"unordered-list",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const _INLINE_EDIT_AFFORDANCE_BY_TYPE = {
|
|
25
|
+
table: { labelKey: "snippet.inline_edit_btn_table", iconClass: "fa-solid fa-table-cells" },
|
|
26
|
+
"code-block": { labelKey: "snippet.inline_edit_btn_code_block", iconClass: "fa-solid fa-code" },
|
|
27
|
+
blockquote: { labelKey: "snippet.inline_edit_btn_blockquote", iconClass: "fa-solid fa-quote-right" },
|
|
28
|
+
"ordered-list": { labelKey: "snippet.inline_edit_btn_ordered_list", iconClass: "fa-solid fa-list-ol" },
|
|
29
|
+
"unordered-list": { labelKey: "snippet.inline_edit_btn_unordered_list", iconClass: "fa-solid fa-list-ul" },
|
|
30
|
+
tree: { labelKey: "snippet.inline_edit_btn_tree", iconClass: "fa-solid fa-folder-tree" },
|
|
31
|
+
"colored-section": { labelKey: "snippet.inline_edit_btn_colored_section", iconClass: "fa-solid fa-fill-drip" },
|
|
32
|
+
"colored-text": { labelKey: "snippet.inline_edit_btn_colored_text", iconClass: "fa-solid fa-highlighter" },
|
|
33
|
+
collapsible: { labelKey: "snippet.inline_edit_btn_collapsible", iconClass: "fa-solid fa-caret-right" },
|
|
34
|
+
link: { labelKey: "snippet.inline_edit_btn_link", iconClass: "fa-solid fa-link" },
|
|
35
|
+
"doc-link": { labelKey: "snippet.inline_edit_btn_doc_link", iconClass: "fa-solid fa-file-lines" },
|
|
36
|
+
"anchor-link": { labelKey: "snippet.inline_edit_btn_anchor_link", iconClass: "fa-solid fa-anchor" },
|
|
37
|
+
"anchor-doc-link": { labelKey: "snippet.inline_edit_btn_anchor_doc_link", iconClass: "fa-solid fa-anchor" },
|
|
38
|
+
image: { labelKey: "snippet.inline_edit_btn_image", iconClass: "fa-solid fa-image" },
|
|
39
|
+
separator: { labelKey: "snippet.inline_edit_btn_separator", iconClass: "fa-solid fa-minus" },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function _inlineEditAffordance(type) {
|
|
43
|
+
const known = _INLINE_EDIT_AFFORDANCE_BY_TYPE[type];
|
|
44
|
+
if (known) return known;
|
|
45
|
+
return { labelKey: "snippet.inline_edit_btn", iconClass: "fa-solid fa-pen-to-square" };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const _INLINE_TYPE_SELECTORS = [
|
|
49
|
+
{ types: ["collapsible"], selector: "details" },
|
|
50
|
+
{ types: ["colored-section"], selector: 'div[style*="border-left"]' },
|
|
51
|
+
{ types: ["colored-text"], selector: 'span[style*="color"]' },
|
|
52
|
+
{ types: ["tree", "code-block"], selector: "pre" },
|
|
53
|
+
{ types: ["table"], selector: "table" },
|
|
54
|
+
{ types: ["blockquote"], selector: "blockquote" },
|
|
55
|
+
{ types: ["separator"], selector: "hr" },
|
|
56
|
+
{ types: ["ordered-list"], selector: "ol" },
|
|
57
|
+
{ types: ["unordered-list"], selector: "ul" },
|
|
58
|
+
{ types: ["image"], selector: "img" },
|
|
59
|
+
{
|
|
60
|
+
types: ["anchor-doc-link", "doc-link", "anchor-link", "link"],
|
|
61
|
+
selector: "a[href]",
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
let _inlineSnippetPopup = null;
|
|
66
|
+
|
|
67
|
+
function _inlineRangesOverlap(a, b) {
|
|
68
|
+
return a.start < b.end && b.start < a.end;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function _inlineAddRange(ranges, start, raw) {
|
|
72
|
+
if (!raw || !raw.trim()) return;
|
|
73
|
+
const type = detectSnippetType(raw);
|
|
74
|
+
if (!type || !_INLINE_SNIPPET_TYPES.has(type)) return;
|
|
75
|
+
const end = start + raw.length;
|
|
76
|
+
const candidate = { start, end, type };
|
|
77
|
+
if (ranges.some((existing) => _inlineRangesOverlap(existing, candidate))) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
ranges.push(candidate);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function _inlineAddRegexRanges(ranges, content, regex, groupIndex = 0) {
|
|
84
|
+
let match;
|
|
85
|
+
while ((match = regex.exec(content))) {
|
|
86
|
+
const raw = match[groupIndex];
|
|
87
|
+
const start =
|
|
88
|
+
match.index + (groupIndex > 0 ? match[0].indexOf(raw) : 0);
|
|
89
|
+
_inlineAddRange(ranges, start, raw);
|
|
90
|
+
if (match[0].length === 0) regex.lastIndex += 1;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function _inlineLineIndentBefore(content, idx) {
|
|
95
|
+
let i = idx;
|
|
96
|
+
while (i > 0 && content[i - 1] !== "\n") i -= 1;
|
|
97
|
+
const prefix = content.slice(i, idx);
|
|
98
|
+
return /^[ \t]+$/.test(prefix) ? prefix : "";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function _inlineAddCodeBlockRanges(ranges, content) {
|
|
102
|
+
const regex = /```[\s\S]*?```/g;
|
|
103
|
+
let match;
|
|
104
|
+
while ((match = regex.exec(content))) {
|
|
105
|
+
const raw = match[0];
|
|
106
|
+
if (!raw.trim()) {
|
|
107
|
+
if (raw.length === 0) regex.lastIndex += 1;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const type = detectSnippetType(raw);
|
|
111
|
+
if (!type || !_INLINE_SNIPPET_TYPES.has(type)) continue;
|
|
112
|
+
const indent = _inlineLineIndentBefore(content, match.index);
|
|
113
|
+
const start = match.index - indent.length;
|
|
114
|
+
const end = match.index + raw.length;
|
|
115
|
+
const candidate = { start, end, type, indent };
|
|
116
|
+
if (ranges.some((existing) => _inlineRangesOverlap(existing, candidate))) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
ranges.push(candidate);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function _inlineCollectSnippetRanges(content) {
|
|
124
|
+
const ranges = [];
|
|
125
|
+
|
|
126
|
+
_inlineAddCodeBlockRanges(ranges, content);
|
|
127
|
+
_inlineAddRegexRanges(ranges, content, /<details[\s\S]*?<\/details>/gi);
|
|
128
|
+
_inlineAddRegexRanges(
|
|
129
|
+
ranges,
|
|
130
|
+
content,
|
|
131
|
+
/<div\b[^>]*border-left[^>]*>[\s\S]*?<\/div>/gi,
|
|
132
|
+
);
|
|
133
|
+
_inlineAddRegexRanges(
|
|
134
|
+
ranges,
|
|
135
|
+
content,
|
|
136
|
+
/<span\b[^>]*color:[^>]*>[\s\S]*?<\/span>/gi,
|
|
137
|
+
);
|
|
138
|
+
_inlineAddRegexRanges(
|
|
139
|
+
ranges,
|
|
140
|
+
content,
|
|
141
|
+
/(?:^|\n)((?:\|[^\n]*\|\n)\|[ \t:|-]*\|(?:\n\|[^\n]*\|)*)/g,
|
|
142
|
+
1,
|
|
143
|
+
);
|
|
144
|
+
_inlineAddRegexRanges(ranges, content, /^> .*(?:\n>.*)*/gm);
|
|
145
|
+
_inlineAddRegexRanges(ranges, content, /^1\. .*(?:\n(?:\d+\.| {3,}\d+\.) .*)*/gm);
|
|
146
|
+
_inlineAddRegexRanges(ranges, content, /^- .*(?:\n(?:- | {2,}- ).*)*/gm);
|
|
147
|
+
_inlineAddRegexRanges(ranges, content, /!\[[^\]\n]*\]\([^)]+\)/g);
|
|
148
|
+
_inlineAddRegexRanges(ranges, content, /\[[^\]\n]+\]\([^)]+\)/g);
|
|
149
|
+
|
|
150
|
+
const frontmatter = content.match(/^---\s*\n[\s\S]*?\n---/);
|
|
151
|
+
_inlineAddRegexRanges(ranges, content, /^---$/gm);
|
|
152
|
+
return ranges
|
|
153
|
+
.filter((range) => {
|
|
154
|
+
if (range.type !== "separator") return true;
|
|
155
|
+
return !frontmatter || range.start > frontmatter[0].length;
|
|
156
|
+
})
|
|
157
|
+
.sort((a, b) => a.start - b.start);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function _inlineTopLevelListElements(contentEl, selector) {
|
|
161
|
+
return Array.from(contentEl.querySelectorAll(selector)).filter(
|
|
162
|
+
(el) => !el.parentElement.closest("ol, ul"),
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function _inlineAssignElements(contentEl, candidates) {
|
|
167
|
+
contentEl
|
|
168
|
+
.querySelectorAll("[data-inline-snippet-index]")
|
|
169
|
+
.forEach((el) => {
|
|
170
|
+
el.removeAttribute("data-inline-snippet-index");
|
|
171
|
+
el.classList.remove("ld-inline-snippet-target");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
for (const { types, selector } of _INLINE_TYPE_SELECTORS) {
|
|
175
|
+
const matching = candidates.filter((candidate) =>
|
|
176
|
+
types.includes(candidate.type),
|
|
177
|
+
);
|
|
178
|
+
if (!matching.length) continue;
|
|
179
|
+
const elements =
|
|
180
|
+
selector === "ol" || selector === "ul"
|
|
181
|
+
? _inlineTopLevelListElements(contentEl, selector)
|
|
182
|
+
: Array.from(contentEl.querySelectorAll(selector));
|
|
183
|
+
const limit = Math.min(matching.length, elements.length);
|
|
184
|
+
for (let i = 0; i < limit; i += 1) {
|
|
185
|
+
elements[i].dataset.inlineSnippetIndex = String(
|
|
186
|
+
candidates.indexOf(matching[i]),
|
|
187
|
+
);
|
|
188
|
+
elements[i].classList.add("ld-inline-snippet-target");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function _inlineClosePopup() {
|
|
194
|
+
if (_inlineSnippetPopup) {
|
|
195
|
+
_inlineSnippetPopup.remove();
|
|
196
|
+
_inlineSnippetPopup = null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function _inlineShowPopup(event, { iconClass, labelKey, onActivate, dataAction, dataType }) {
|
|
201
|
+
_inlineClosePopup();
|
|
202
|
+
const popup = document.createElement("div");
|
|
203
|
+
popup.id = "inline-snippet-popup";
|
|
204
|
+
if (dataAction) popup.dataset.action = dataAction;
|
|
205
|
+
if (dataType) popup.dataset.snippetType = dataType;
|
|
206
|
+
popup.className =
|
|
207
|
+
"fixed z-50 rounded-lg border border-blue-200 dark:border-blue-800 bg-white dark:bg-gray-900 shadow-lg p-1";
|
|
208
|
+
const btn = document.createElement("button");
|
|
209
|
+
btn.type = "button";
|
|
210
|
+
btn.className =
|
|
211
|
+
"inline-flex items-center gap-2 rounded-md px-3 py-2 text-sm font-semibold text-blue-700 dark:text-blue-300 hover:bg-blue-50 dark:hover:bg-blue-900/30";
|
|
212
|
+
const icon = document.createElement("i");
|
|
213
|
+
icon.className = iconClass;
|
|
214
|
+
icon.setAttribute("aria-hidden", "true");
|
|
215
|
+
const label = document.createElement("span");
|
|
216
|
+
label.textContent = window.t(labelKey);
|
|
217
|
+
btn.append(icon, label);
|
|
218
|
+
btn.addEventListener("click", (e) => {
|
|
219
|
+
e.preventDefault();
|
|
220
|
+
e.stopPropagation();
|
|
221
|
+
_inlineClosePopup();
|
|
222
|
+
onActivate();
|
|
223
|
+
});
|
|
224
|
+
popup.appendChild(btn);
|
|
225
|
+
document.body.appendChild(popup);
|
|
226
|
+
|
|
227
|
+
const margin = 8;
|
|
228
|
+
const rect = popup.getBoundingClientRect();
|
|
229
|
+
const left = Math.min(
|
|
230
|
+
window.innerWidth - rect.width - margin,
|
|
231
|
+
Math.max(margin, event.clientX),
|
|
232
|
+
);
|
|
233
|
+
const top = Math.max(margin, event.clientY - rect.height - margin);
|
|
234
|
+
popup.style.left = `${left}px`;
|
|
235
|
+
popup.style.top = `${top}px`;
|
|
236
|
+
_inlineSnippetPopup = popup;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function _inlineTopLevelBlock(contentEl, target) {
|
|
240
|
+
let block = target instanceof Element ? target : null;
|
|
241
|
+
while (block && block.parentElement && block.parentElement !== contentEl) {
|
|
242
|
+
block = block.parentElement;
|
|
243
|
+
}
|
|
244
|
+
if (!block || block === contentEl || block.parentElement !== contentEl) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
return block;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function _inlineNearestBlockByY(contentEl, clientY) {
|
|
251
|
+
if (typeof clientY !== "number") return null;
|
|
252
|
+
const children = Array.from(contentEl.children);
|
|
253
|
+
if (!children.length) return null;
|
|
254
|
+
let above = null;
|
|
255
|
+
let below = null;
|
|
256
|
+
for (const child of children) {
|
|
257
|
+
const rect = child.getBoundingClientRect();
|
|
258
|
+
if (rect.bottom < clientY) above = child;
|
|
259
|
+
else if (rect.top > clientY) {
|
|
260
|
+
if (!below) below = child;
|
|
261
|
+
} else return child;
|
|
262
|
+
}
|
|
263
|
+
return above || below;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function _inlineResolveBlockEnd(block, candidates) {
|
|
267
|
+
if (block.matches("[data-inline-snippet-index]")) {
|
|
268
|
+
const range = candidates[Number(block.dataset.inlineSnippetIndex)];
|
|
269
|
+
if (range) return range.end;
|
|
270
|
+
}
|
|
271
|
+
let anchorIdx = null;
|
|
272
|
+
const desc = Array.from(
|
|
273
|
+
block.querySelectorAll("[data-inline-snippet-index]"),
|
|
274
|
+
);
|
|
275
|
+
if (desc.length > 0) {
|
|
276
|
+
let maxEnd = -1;
|
|
277
|
+
for (const d of desc) {
|
|
278
|
+
const range = candidates[Number(d.dataset.inlineSnippetIndex)];
|
|
279
|
+
if (range && range.end > maxEnd) maxEnd = range.end;
|
|
280
|
+
}
|
|
281
|
+
if (maxEnd >= 0) anchorIdx = maxEnd;
|
|
282
|
+
} else {
|
|
283
|
+
const text = (block.textContent || "").trim();
|
|
284
|
+
if (text) {
|
|
285
|
+
const firstLine = text.split("\n")[0].trim().slice(0, 60);
|
|
286
|
+
if (firstLine) {
|
|
287
|
+
const idx = currentDocContent.indexOf(firstLine);
|
|
288
|
+
if (idx >= 0) anchorIdx = idx;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (anchorIdx === null) return null;
|
|
293
|
+
const blankIdx = currentDocContent.indexOf("\n\n", anchorIdx);
|
|
294
|
+
return blankIdx >= 0 ? blankIdx : currentDocContent.length;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function _inlineResolveBlockStart(block, candidates) {
|
|
298
|
+
if (block.matches("[data-inline-snippet-index]")) {
|
|
299
|
+
const range = candidates[Number(block.dataset.inlineSnippetIndex)];
|
|
300
|
+
if (range) return range.start;
|
|
301
|
+
}
|
|
302
|
+
const desc = Array.from(
|
|
303
|
+
block.querySelectorAll("[data-inline-snippet-index]"),
|
|
304
|
+
);
|
|
305
|
+
if (desc.length > 0) {
|
|
306
|
+
let minStart = Infinity;
|
|
307
|
+
for (const d of desc) {
|
|
308
|
+
const range = candidates[Number(d.dataset.inlineSnippetIndex)];
|
|
309
|
+
if (range && range.start < minStart) minStart = range.start;
|
|
310
|
+
}
|
|
311
|
+
if (minStart < Infinity) return minStart;
|
|
312
|
+
}
|
|
313
|
+
const text = (block.textContent || "").trim();
|
|
314
|
+
if (text) {
|
|
315
|
+
const firstLine = text.split("\n")[0].trim().slice(0, 60);
|
|
316
|
+
if (firstLine) {
|
|
317
|
+
const idx = currentDocContent.indexOf(firstLine);
|
|
318
|
+
if (idx >= 0) return idx;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function _inlineFindInsertPosition(contentEl, target, candidates, clientY) {
|
|
325
|
+
if (typeof currentDocContent !== "string") return null;
|
|
326
|
+
const topBlock =
|
|
327
|
+
_inlineTopLevelBlock(contentEl, target) ||
|
|
328
|
+
_inlineNearestBlockByY(contentEl, clientY);
|
|
329
|
+
if (!topBlock) return currentDocContent.length;
|
|
330
|
+
|
|
331
|
+
const directEnd = _inlineResolveBlockEnd(topBlock, candidates);
|
|
332
|
+
if (directEnd !== null) return directEnd;
|
|
333
|
+
|
|
334
|
+
let prev = topBlock.previousElementSibling;
|
|
335
|
+
while (prev) {
|
|
336
|
+
const end = _inlineResolveBlockEnd(prev, candidates);
|
|
337
|
+
if (end !== null) {
|
|
338
|
+
const next = currentDocContent.indexOf("\n\n", end + 1);
|
|
339
|
+
return next >= 0 ? next : currentDocContent.length;
|
|
340
|
+
}
|
|
341
|
+
prev = prev.previousElementSibling;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
let nxt = topBlock.nextElementSibling;
|
|
345
|
+
while (nxt) {
|
|
346
|
+
const start = _inlineResolveBlockStart(nxt, candidates);
|
|
347
|
+
if (start !== null) {
|
|
348
|
+
const prevBlank = currentDocContent.lastIndexOf("\n\n", start);
|
|
349
|
+
return prevBlank >= 0 ? prevBlank : 0;
|
|
350
|
+
}
|
|
351
|
+
nxt = nxt.nextElementSibling;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return currentDocContent.length;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function initInlineSnippetEditing(contentEl) {
|
|
358
|
+
if (!contentEl || typeof currentDocContent !== "string") return;
|
|
359
|
+
const candidates = _inlineCollectSnippetRanges(currentDocContent);
|
|
360
|
+
_inlineAssignElements(contentEl, candidates);
|
|
361
|
+
|
|
362
|
+
if (contentEl._inlineSnippetContextHandler) {
|
|
363
|
+
contentEl.removeEventListener(
|
|
364
|
+
"contextmenu",
|
|
365
|
+
contentEl._inlineSnippetContextHandler,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
contentEl._inlineSnippetContextHandler = (event) => {
|
|
369
|
+
const target = event.target.closest("[data-inline-snippet-index]");
|
|
370
|
+
if (target && contentEl.contains(target)) {
|
|
371
|
+
const range = candidates[Number(target.dataset.inlineSnippetIndex)];
|
|
372
|
+
if (!range) return;
|
|
373
|
+
event.preventDefault();
|
|
374
|
+
const affordance = _inlineEditAffordance(range.type);
|
|
375
|
+
_inlineShowPopup(event, {
|
|
376
|
+
iconClass: affordance.iconClass,
|
|
377
|
+
labelKey: affordance.labelKey,
|
|
378
|
+
dataAction: "edit",
|
|
379
|
+
dataType: range.type,
|
|
380
|
+
onActivate: () => openSnippetsModalForInlineEdit(range),
|
|
381
|
+
});
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (!contentEl.contains(event.target)) return;
|
|
385
|
+
const insertPos = _inlineFindInsertPosition(
|
|
386
|
+
contentEl,
|
|
387
|
+
event.target,
|
|
388
|
+
candidates,
|
|
389
|
+
event.clientY,
|
|
390
|
+
);
|
|
391
|
+
if (insertPos === null) return;
|
|
392
|
+
event.preventDefault();
|
|
393
|
+
_inlineShowPopup(event, {
|
|
394
|
+
iconClass: "fa-solid fa-plus",
|
|
395
|
+
labelKey: "snippet.inline_insert_btn",
|
|
396
|
+
dataAction: "insert",
|
|
397
|
+
onActivate: () => openSnippetsModalForInlineInsert(insertPos),
|
|
398
|
+
});
|
|
399
|
+
};
|
|
400
|
+
contentEl.addEventListener(
|
|
401
|
+
"contextmenu",
|
|
402
|
+
contentEl._inlineSnippetContextHandler,
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
document.addEventListener("click", (event) => {
|
|
407
|
+
if (_inlineSnippetPopup && !_inlineSnippetPopup.contains(event.target)) {
|
|
408
|
+
_inlineClosePopup();
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
document.addEventListener("keydown", (event) => {
|
|
413
|
+
if (event.key === "Escape") _inlineClosePopup();
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
window.initInlineSnippetEditing = initInlineSnippetEditing;
|
|
@@ -6,13 +6,19 @@
|
|
|
6
6
|
|
|
7
7
|
let _snippetSelStart = 0;
|
|
8
8
|
let _snippetSelEnd = 0;
|
|
9
|
+
let _snippetInlineEdit = false;
|
|
10
|
+
let _snippetInlineInsert = false;
|
|
11
|
+
let _snippetInlineIndent = "";
|
|
9
12
|
const _SNIPPET_PANELS = [
|
|
10
13
|
"collapsible",
|
|
11
14
|
"link",
|
|
12
15
|
"doc-link",
|
|
13
16
|
"anchor-link",
|
|
14
17
|
"anchor-doc-link",
|
|
18
|
+
"ordered-list",
|
|
19
|
+
"unordered-list",
|
|
15
20
|
"code-block",
|
|
21
|
+
"blockquote",
|
|
16
22
|
"image",
|
|
17
23
|
"table",
|
|
18
24
|
"tree",
|
|
@@ -626,11 +632,44 @@ async function snippetAnchorDocChanged() {
|
|
|
626
632
|
snippetUpdatePreview();
|
|
627
633
|
}
|
|
628
634
|
|
|
629
|
-
function
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
635
|
+
function _setSnippetModalMode(mode) {
|
|
636
|
+
const isInlineEdit = mode === "inline-edit";
|
|
637
|
+
const isInlineInsert = mode === "inline-insert";
|
|
638
|
+
const isInline = isInlineEdit || isInlineInsert;
|
|
639
|
+
_snippetInlineEdit = isInlineEdit;
|
|
640
|
+
_snippetInlineInsert = isInlineInsert;
|
|
641
|
+
if (!isInlineEdit) _snippetInlineIndent = "";
|
|
642
|
+
const title = document.getElementById("snippet-modal-title");
|
|
643
|
+
if (title) {
|
|
644
|
+
let key = "snippet.modal_title";
|
|
645
|
+
if (isInlineEdit) key = "snippet.inline_modal_title";
|
|
646
|
+
else if (isInlineInsert) key = "snippet.inline_insert_modal_title";
|
|
647
|
+
title.textContent = window.t(key);
|
|
648
|
+
}
|
|
649
|
+
const submit = document.getElementById("snippet-submit-btn");
|
|
650
|
+
if (submit) {
|
|
651
|
+
submit.textContent = window.t(
|
|
652
|
+
isInlineEdit ? "snippet.inline_save_btn" : "snippet.insert_btn",
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
const typeSelect = document.getElementById("snippet-type");
|
|
656
|
+
if (typeSelect) {
|
|
657
|
+
typeSelect.disabled = isInlineEdit;
|
|
658
|
+
typeSelect.classList.toggle("cursor-not-allowed", isInlineEdit);
|
|
659
|
+
typeSelect.classList.toggle("opacity-70", isInlineEdit);
|
|
660
|
+
}
|
|
661
|
+
const deleteBtn = document.getElementById("snippet-delete-btn");
|
|
662
|
+
if (deleteBtn) {
|
|
663
|
+
deleteBtn.classList.toggle("hidden", !isInlineEdit);
|
|
664
|
+
}
|
|
665
|
+
const card = document.getElementById("snippet-modal-card");
|
|
666
|
+
if (card) {
|
|
667
|
+
card.classList.toggle("max-w-lg", !isInline);
|
|
668
|
+
card.classList.toggle("max-w-5xl", isInline);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
633
671
|
|
|
672
|
+
function _openSnippetsModalForText(selectedText, detectedOverride = null) {
|
|
634
673
|
const docOpts = allDocs
|
|
635
674
|
.map((d) => `<option value="${d.id}">${d.title}</option>`)
|
|
636
675
|
.join("");
|
|
@@ -640,13 +679,9 @@ function openSnippetsModal() {
|
|
|
640
679
|
snippetAnchorDocChanged();
|
|
641
680
|
|
|
642
681
|
const msgEl = document.getElementById("snippet-detect-msg");
|
|
643
|
-
const selectedText = editor.value.slice(
|
|
644
|
-
_snippetSelStart,
|
|
645
|
-
_snippetSelEnd,
|
|
646
|
-
);
|
|
647
682
|
|
|
648
683
|
if (selectedText) {
|
|
649
|
-
const detected = detectSnippetType(selectedText);
|
|
684
|
+
const detected = detectedOverride || detectSnippetType(selectedText);
|
|
650
685
|
if (detected) {
|
|
651
686
|
document.getElementById("snippet-type").value = detected;
|
|
652
687
|
snippetTypeChanged();
|
|
@@ -688,8 +723,39 @@ function openSnippetsModal() {
|
|
|
688
723
|
document.getElementById("snippets-modal").classList.remove("hidden");
|
|
689
724
|
}
|
|
690
725
|
|
|
726
|
+
function openSnippetsModal() {
|
|
727
|
+
const editor = document.getElementById("doc-editor");
|
|
728
|
+
_snippetSelStart = editor.selectionStart;
|
|
729
|
+
_snippetSelEnd = editor.selectionEnd;
|
|
730
|
+
_setSnippetModalMode("insert");
|
|
731
|
+
_openSnippetsModalForText(editor.value.slice(_snippetSelStart, _snippetSelEnd));
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function openSnippetsModalForInlineEdit(range) {
|
|
735
|
+
if (!range || typeof currentDocContent !== "string") return;
|
|
736
|
+
_snippetSelStart = range.start;
|
|
737
|
+
_snippetSelEnd = range.end;
|
|
738
|
+
_setSnippetModalMode("inline-edit");
|
|
739
|
+
_snippetInlineIndent = range.indent || "";
|
|
740
|
+
const selectedText = currentDocContent.slice(_snippetSelStart, _snippetSelEnd);
|
|
741
|
+
_openSnippetsModalForText(selectedText, range.type || null);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function openSnippetsModalForInlineInsert(insertPos) {
|
|
745
|
+
if (typeof currentDocContent !== "string") return;
|
|
746
|
+
const pos = Math.max(
|
|
747
|
+
0,
|
|
748
|
+
Math.min(currentDocContent.length, Number(insertPos) || 0),
|
|
749
|
+
);
|
|
750
|
+
_snippetSelStart = pos;
|
|
751
|
+
_snippetSelEnd = pos;
|
|
752
|
+
_setSnippetModalMode("inline-insert");
|
|
753
|
+
_openSnippetsModalForText("");
|
|
754
|
+
}
|
|
755
|
+
|
|
691
756
|
function closeSnippetsModal() {
|
|
692
757
|
document.getElementById("snippets-modal").classList.add("hidden");
|
|
758
|
+
_setSnippetModalMode("insert");
|
|
693
759
|
}
|
|
694
760
|
|
|
695
761
|
function snippetTypeChanged() {
|
|
@@ -698,8 +764,21 @@ function snippetTypeChanged() {
|
|
|
698
764
|
const panel = document.getElementById("snip-panel-" + p);
|
|
699
765
|
if (panel) panel.classList.toggle("hidden", p !== type);
|
|
700
766
|
});
|
|
701
|
-
const previewWrap = document.getElementById("snippet-preview")
|
|
702
|
-
if (previewWrap)
|
|
767
|
+
const previewWrap = document.getElementById("snippet-preview-wrap");
|
|
768
|
+
if (previewWrap) {
|
|
769
|
+
previewWrap.classList.toggle(
|
|
770
|
+
"hidden",
|
|
771
|
+
type === "attachment" ||
|
|
772
|
+
type === "table" ||
|
|
773
|
+
type === "code-block" ||
|
|
774
|
+
type === "blockquote" ||
|
|
775
|
+
type === "ordered-list" ||
|
|
776
|
+
type === "unordered-list" ||
|
|
777
|
+
type === "colored-section" ||
|
|
778
|
+
type === "colored-text" ||
|
|
779
|
+
type === "tree",
|
|
780
|
+
);
|
|
781
|
+
}
|
|
703
782
|
|
|
704
783
|
if (type === "table") tableInit();
|
|
705
784
|
else if (type === "tree") treeInit();
|
|
@@ -821,32 +900,77 @@ function buildSnippetMarkdown() {
|
|
|
821
900
|
window.t('snippet.link_anchor_placeholder');
|
|
822
901
|
return `[${text}](?doc=${encodeURIComponent(docId)}#${anchor})`;
|
|
823
902
|
}
|
|
824
|
-
case "ordered-list":
|
|
825
|
-
|
|
826
|
-
"
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
903
|
+
case "ordered-list": {
|
|
904
|
+
const content =
|
|
905
|
+
document.getElementById("snip-ordered-list-content").value ||
|
|
906
|
+
[
|
|
907
|
+
"Élément 1",
|
|
908
|
+
"Élément 2",
|
|
909
|
+
" Sous-élément 2.1",
|
|
910
|
+
" Sous-élément 2.2",
|
|
911
|
+
"Élément 3",
|
|
912
|
+
" Sous-élément 3.1",
|
|
913
|
+
" Sous-sous-élément 3.1.1",
|
|
914
|
+
].join("\n");
|
|
915
|
+
const countersByIndent = new Map();
|
|
916
|
+
return content
|
|
917
|
+
.split("\n")
|
|
918
|
+
.filter((line) => line.trim())
|
|
919
|
+
.map((line) => {
|
|
920
|
+
const indent = line.match(/^\s*/)[0];
|
|
921
|
+
const indentLength = indent.length;
|
|
922
|
+
for (const knownIndent of Array.from(countersByIndent.keys())) {
|
|
923
|
+
if (knownIndent > indentLength) countersByIndent.delete(knownIndent);
|
|
924
|
+
}
|
|
925
|
+
const next = (countersByIndent.get(indentLength) || 0) + 1;
|
|
926
|
+
countersByIndent.set(indentLength, next);
|
|
927
|
+
return `${indent}${next}. ${line.trim()}`;
|
|
928
|
+
})
|
|
929
|
+
.join("\n");
|
|
930
|
+
}
|
|
931
|
+
case "unordered-list": {
|
|
932
|
+
const content =
|
|
933
|
+
document.getElementById("snip-unordered-list-content").value ||
|
|
934
|
+
[
|
|
935
|
+
"Élément 1",
|
|
936
|
+
"Élément 2",
|
|
937
|
+
" Sous-élément 2.1",
|
|
938
|
+
" Sous-élément 2.2",
|
|
939
|
+
"Élément 3",
|
|
940
|
+
" Sous-élément 3.1",
|
|
941
|
+
" Sous-sous-élément 3.1.1",
|
|
942
|
+
].join("\n");
|
|
943
|
+
return content
|
|
944
|
+
.split("\n")
|
|
945
|
+
.filter((line) => line.trim())
|
|
946
|
+
.map((line) => {
|
|
947
|
+
const indent = line.match(/^\s*/)[0];
|
|
948
|
+
return `${indent}- ${line.trim()}`;
|
|
949
|
+
})
|
|
950
|
+
.join("\n");
|
|
951
|
+
}
|
|
844
952
|
case "code-block": {
|
|
845
953
|
const lang = document.getElementById("snip-code-lang").value || "";
|
|
846
|
-
|
|
954
|
+
const code =
|
|
955
|
+
document.getElementById("snip-code-content").value || "// code ici";
|
|
956
|
+
const block = `\`\`\`${lang}\n${code}\n\`\`\``;
|
|
957
|
+
if (_snippetInlineEdit && _snippetInlineIndent) {
|
|
958
|
+
return block
|
|
959
|
+
.split("\n")
|
|
960
|
+
.map((line) => _snippetInlineIndent + line)
|
|
961
|
+
.join("\n");
|
|
962
|
+
}
|
|
963
|
+
return block;
|
|
964
|
+
}
|
|
965
|
+
case "blockquote": {
|
|
966
|
+
const content =
|
|
967
|
+
document.getElementById("snip-blockquote-content").value ||
|
|
968
|
+
"Citation ici\n\n— Auteur";
|
|
969
|
+
return content
|
|
970
|
+
.split("\n")
|
|
971
|
+
.map((line) => (line.trim() ? `> ${line}` : ">"))
|
|
972
|
+
.join("\n");
|
|
847
973
|
}
|
|
848
|
-
case "blockquote":
|
|
849
|
-
return `> Citation ici\n>\n> — Auteur`;
|
|
850
974
|
case "separator":
|
|
851
975
|
return `\n---\n`;
|
|
852
976
|
case "image": {
|
|
@@ -903,8 +1027,14 @@ function snippetUpdatePreview() {
|
|
|
903
1027
|
buildSnippetMarkdown();
|
|
904
1028
|
}
|
|
905
1029
|
|
|
906
|
-
function insertSnippet() {
|
|
1030
|
+
async function insertSnippet() {
|
|
907
1031
|
const type = document.getElementById("snippet-type").value;
|
|
1032
|
+
if (
|
|
1033
|
+
(_snippetInlineEdit || _snippetInlineInsert) &&
|
|
1034
|
+
(type === "diagram" || type === "attachment")
|
|
1035
|
+
) {
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
908
1038
|
if (type === "diagram") {
|
|
909
1039
|
insertDiagramSnippet();
|
|
910
1040
|
return;
|
|
@@ -915,7 +1045,48 @@ function insertSnippet() {
|
|
|
915
1045
|
return;
|
|
916
1046
|
}
|
|
917
1047
|
const text = buildSnippetMarkdown();
|
|
1048
|
+
const wasInlineEdit = _snippetInlineEdit;
|
|
1049
|
+
const wasInlineInsert = _snippetInlineInsert;
|
|
918
1050
|
closeSnippetsModal();
|
|
1051
|
+
if (wasInlineEdit) {
|
|
1052
|
+
const before = currentDocContent.slice(0, _snippetSelStart);
|
|
1053
|
+
const after = currentDocContent.slice(_snippetSelEnd);
|
|
1054
|
+
try {
|
|
1055
|
+
await saveCurrentDocumentContent(before + text + after);
|
|
1056
|
+
} catch (err) {
|
|
1057
|
+
alert(
|
|
1058
|
+
window.t("snippet.inline_save_failed") +
|
|
1059
|
+
(err && err.message ? err.message : String(err)),
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
if (wasInlineInsert) {
|
|
1065
|
+
const before = currentDocContent.slice(0, _snippetSelStart);
|
|
1066
|
+
const after = currentDocContent.slice(_snippetSelStart);
|
|
1067
|
+
const leadingBlank =
|
|
1068
|
+
before.length === 0 || /\n\n$/.test(before)
|
|
1069
|
+
? ""
|
|
1070
|
+
: before.endsWith("\n")
|
|
1071
|
+
? "\n"
|
|
1072
|
+
: "\n\n";
|
|
1073
|
+
const trailingBlank =
|
|
1074
|
+
after.length === 0 || /^\n\n/.test(after)
|
|
1075
|
+
? ""
|
|
1076
|
+
: after.startsWith("\n")
|
|
1077
|
+
? "\n"
|
|
1078
|
+
: "\n\n";
|
|
1079
|
+
const payload = leadingBlank + text + trailingBlank;
|
|
1080
|
+
try {
|
|
1081
|
+
await saveCurrentDocumentContent(before + payload + after);
|
|
1082
|
+
} catch (err) {
|
|
1083
|
+
alert(
|
|
1084
|
+
window.t("snippet.inline_insert_failed") +
|
|
1085
|
+
(err && err.message ? err.message : String(err)),
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
919
1090
|
const editor = document.getElementById("doc-editor");
|
|
920
1091
|
const before = editor.value.slice(0, _snippetSelStart);
|
|
921
1092
|
const after = editor.value.slice(_snippetSelEnd);
|
|
@@ -925,6 +1096,34 @@ function insertSnippet() {
|
|
|
925
1096
|
editor.focus();
|
|
926
1097
|
}
|
|
927
1098
|
|
|
1099
|
+
async function deleteInlineSnippetBlock() {
|
|
1100
|
+
if (!_snippetInlineEdit) return;
|
|
1101
|
+
const ok =
|
|
1102
|
+
typeof showConfirm === "function"
|
|
1103
|
+
? await showConfirm({
|
|
1104
|
+
title: window.t("snippet.inline_delete_title"),
|
|
1105
|
+
message: window.t("snippet.inline_delete_message"),
|
|
1106
|
+
detail: window.t("snippet.inline_delete_detail"),
|
|
1107
|
+
confirmLabel: window.t("snippet.inline_delete_confirm_btn"),
|
|
1108
|
+
danger: true,
|
|
1109
|
+
detailTone: "warning",
|
|
1110
|
+
})
|
|
1111
|
+
: confirm(window.t("snippet.inline_delete_message"));
|
|
1112
|
+
if (!ok) return;
|
|
1113
|
+
|
|
1114
|
+
const before = currentDocContent.slice(0, _snippetSelStart);
|
|
1115
|
+
const after = currentDocContent.slice(_snippetSelEnd);
|
|
1116
|
+
closeSnippetsModal();
|
|
1117
|
+
try {
|
|
1118
|
+
await saveCurrentDocumentContent(before + after);
|
|
1119
|
+
} catch (err) {
|
|
1120
|
+
alert(
|
|
1121
|
+
window.t("snippet.inline_delete_failed") +
|
|
1122
|
+
(err && err.message ? err.message : String(err)),
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
928
1127
|
async function insertDiagramSnippet() {
|
|
929
1128
|
const isNew = document.getElementById("snip-diag-mode-new").checked;
|
|
930
1129
|
const imgName =
|
|
@@ -979,6 +1178,22 @@ async function insertDiagramSnippet() {
|
|
|
979
1178
|
}
|
|
980
1179
|
|
|
981
1180
|
// ── Snippet parsing (detection lives in /snippet-detect.js) ────────────────
|
|
1181
|
+
function _parseMarkdownTableCells(line) {
|
|
1182
|
+
return line
|
|
1183
|
+
.trim()
|
|
1184
|
+
.split("|")
|
|
1185
|
+
.slice(1, -1)
|
|
1186
|
+
.map((cell) => cell.trim());
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function _isMarkdownTableSeparatorLine(line) {
|
|
1190
|
+
const cells = _parseMarkdownTableCells(line);
|
|
1191
|
+
return (
|
|
1192
|
+
cells.length > 0 &&
|
|
1193
|
+
cells.every((cell) => /^:?-{3,}:?$/.test(cell.replace(/\s+/g, "")))
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
982
1197
|
function parseAndFillSnippet(text, type) {
|
|
983
1198
|
const t = text.trim();
|
|
984
1199
|
switch (type) {
|
|
@@ -1065,9 +1280,42 @@ function parseAndFillSnippet(text, type) {
|
|
|
1065
1280
|
}
|
|
1066
1281
|
break;
|
|
1067
1282
|
}
|
|
1283
|
+
case "ordered-list": {
|
|
1284
|
+
document.getElementById("snip-ordered-list-content").value = t
|
|
1285
|
+
.split("\n")
|
|
1286
|
+
.map((line) => line.replace(/^(\s*)\d+\.\s?/, "$1"))
|
|
1287
|
+
.join("\n");
|
|
1288
|
+
break;
|
|
1289
|
+
}
|
|
1290
|
+
case "unordered-list": {
|
|
1291
|
+
document.getElementById("snip-unordered-list-content").value = t
|
|
1292
|
+
.split("\n")
|
|
1293
|
+
.map((line) => line.replace(/^(\s*)[-*+]\s?/, "$1"))
|
|
1294
|
+
.join("\n");
|
|
1295
|
+
break;
|
|
1296
|
+
}
|
|
1068
1297
|
case "code-block": {
|
|
1069
|
-
const m = t.match(/^```
|
|
1070
|
-
document.getElementById("snip-code-lang").value = m ? m[1] : "";
|
|
1298
|
+
const m = t.match(/^```[ \t]*([^\n]*)\n([\s\S]*?)\n[ \t]*```$/);
|
|
1299
|
+
document.getElementById("snip-code-lang").value = m ? m[1].trim() : "";
|
|
1300
|
+
let codeContent = m ? m[2] : "";
|
|
1301
|
+
if (m && _snippetInlineIndent) {
|
|
1302
|
+
const escapedIndent = _snippetInlineIndent.replace(
|
|
1303
|
+
/[.*+?^${}()|[\]\\]/g,
|
|
1304
|
+
"\\$&",
|
|
1305
|
+
);
|
|
1306
|
+
codeContent = codeContent.replace(
|
|
1307
|
+
new RegExp("^" + escapedIndent, "gm"),
|
|
1308
|
+
"",
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
document.getElementById("snip-code-content").value = codeContent;
|
|
1312
|
+
break;
|
|
1313
|
+
}
|
|
1314
|
+
case "blockquote": {
|
|
1315
|
+
document.getElementById("snip-blockquote-content").value = t
|
|
1316
|
+
.split("\n")
|
|
1317
|
+
.map((line) => line.replace(/^>\s?/, ""))
|
|
1318
|
+
.join("\n");
|
|
1071
1319
|
break;
|
|
1072
1320
|
}
|
|
1073
1321
|
case "image": {
|
|
@@ -1083,14 +1331,9 @@ function parseAndFillSnippet(text, type) {
|
|
|
1083
1331
|
.split("\n")
|
|
1084
1332
|
.filter((l) => /^\|.*\|$/.test(l.trim()));
|
|
1085
1333
|
const dataLines = allLines.filter(
|
|
1086
|
-
(l) =>
|
|
1087
|
-
);
|
|
1088
|
-
_tableData = dataLines.map((line) =>
|
|
1089
|
-
line
|
|
1090
|
-
.split("|")
|
|
1091
|
-
.slice(1, -1)
|
|
1092
|
-
.map((c) => c.trim()),
|
|
1334
|
+
(l) => !_isMarkdownTableSeparatorLine(l),
|
|
1093
1335
|
);
|
|
1336
|
+
_tableData = dataLines.map(_parseMarkdownTableCells);
|
|
1094
1337
|
const maxCols = Math.max(..._tableData.map((r) => r.length));
|
|
1095
1338
|
_tableData.forEach((row) => {
|
|
1096
1339
|
while (row.length < maxCols) row.push("");
|
|
@@ -5,15 +5,21 @@
|
|
|
5
5
|
// below 100%, the confirmation modal also warns that source-file hashes will be
|
|
6
6
|
// re-baselined, and POST /api/metadata/:id/refresh is called after the PUT.
|
|
7
7
|
|
|
8
|
-
// Frontmatter
|
|
9
|
-
//
|
|
10
|
-
|
|
8
|
+
// Frontmatter may use this project's historical `**key:** value` convention or
|
|
9
|
+
// regular YAML-style `key: value` lines. The status line is the only field this
|
|
10
|
+
// module touches.
|
|
11
|
+
const _STATUS_LINE_RE = /^(\s*(?:\*\*status:\*\*|status:)\s*).+?\s*$/im;
|
|
12
|
+
|
|
13
|
+
function isWorklogDocument(docId) {
|
|
14
|
+
if (/%5BWORKLOG%5D/i.test(docId)) return true;
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
11
17
|
|
|
12
18
|
function getDocStatus(content) {
|
|
13
19
|
if (typeof content !== "string") return null;
|
|
14
20
|
const fence = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
15
21
|
if (!fence) return null;
|
|
16
|
-
const m = fence[1].match(
|
|
22
|
+
const m = fence[1].match(/^\s*(?:\*\*status:\*\*|status:)\s*(.+?)\s*$/im);
|
|
17
23
|
return m ? m[1].trim() : null;
|
|
18
24
|
}
|
|
19
25
|
|
|
@@ -27,7 +33,7 @@ function updateValidateButtonForCurrentDoc() {
|
|
|
27
33
|
const status = getDocStatus(
|
|
28
34
|
typeof currentDocContent !== "undefined" ? currentDocContent : "",
|
|
29
35
|
);
|
|
30
|
-
if (status === "
|
|
36
|
+
if (status && status.toUpperCase() === "TO BE VALIDATED") {
|
|
31
37
|
btn.classList.remove("hidden");
|
|
32
38
|
} else {
|
|
33
39
|
btn.classList.add("hidden");
|
|
@@ -53,12 +59,18 @@ async function validateCurrentDoc() {
|
|
|
53
59
|
const pct = Math.round(accuracy * 100);
|
|
54
60
|
const lowAccuracy = pct < 100;
|
|
55
61
|
const detail = lowAccuracy
|
|
56
|
-
? window
|
|
62
|
+
? window
|
|
63
|
+
.t("doc.validate_detail_low_accuracy")
|
|
64
|
+
.replace("{accuracy}", pct + "%")
|
|
57
65
|
: "";
|
|
58
66
|
|
|
59
67
|
const ok = await window.showConfirm({
|
|
60
68
|
title: window.t("doc.validate_title"),
|
|
61
|
-
message: window.t(
|
|
69
|
+
message: window.t(
|
|
70
|
+
isWorklogDocument(currentDocId)
|
|
71
|
+
? "doc.validate_worklog_message"
|
|
72
|
+
: "doc.validate_message",
|
|
73
|
+
),
|
|
62
74
|
detail,
|
|
63
75
|
detailTone: lowAccuracy ? "warning" : undefined,
|
|
64
76
|
confirmLabel: window.t("doc.validate_confirm"),
|
|
@@ -69,7 +81,10 @@ async function validateCurrentDoc() {
|
|
|
69
81
|
if (btn) btn.disabled = true;
|
|
70
82
|
|
|
71
83
|
try {
|
|
72
|
-
const newContent = _replaceStatus(
|
|
84
|
+
const newContent = _replaceStatus(
|
|
85
|
+
currentDocContent,
|
|
86
|
+
isWorklogDocument(currentDocId) ? "Done" : "Accepted",
|
|
87
|
+
);
|
|
73
88
|
if (newContent === currentDocContent) {
|
|
74
89
|
throw new Error("status line not found in frontmatter");
|
|
75
90
|
}
|
package/dist/src/lib/status.js
CHANGED
|
@@ -16,7 +16,7 @@ exports.assertNotSuperSeeded = assertNotSuperSeeded;
|
|
|
16
16
|
const fs_1 = __importDefault(require("fs"));
|
|
17
17
|
const path_1 = __importDefault(require("path"));
|
|
18
18
|
const config_1 = require("./config");
|
|
19
|
-
const STATUS_LINE_RE =
|
|
19
|
+
const STATUS_LINE_RE = /^\s*(?:\*\*status:\*\*|status:)\s*(.+?)\s*$/im;
|
|
20
20
|
const FRONTMATTER_FENCE_RE = /^---\s*\n([\s\S]*?)\n---/;
|
|
21
21
|
function parseDocStatus(content) {
|
|
22
22
|
if (typeof content !== "string")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status.js","sourceRoot":"","sources":["../../../src/lib/status.ts"],"names":[],"mappings":";AAAA,gFAAgF;AAChF,gFAAgF;AAChF,+EAA+E;AAC/E,8EAA8E;AAC9E,+EAA+E;AAC/E,0EAA0E;;;;;;AAS1E,wCAMC;AAmBD,sCAYC;AAiBD,oDAQC;AArED,4CAAoB;AACpB,gDAAwB;AACxB,qCAAsC;AAEtC,MAAM,cAAc,GAAG,+
|
|
1
|
+
{"version":3,"file":"status.js","sourceRoot":"","sources":["../../../src/lib/status.ts"],"names":[],"mappings":";AAAA,gFAAgF;AAChF,gFAAgF;AAChF,+EAA+E;AAC/E,8EAA8E;AAC9E,+EAA+E;AAC/E,0EAA0E;;;;;;AAS1E,wCAMC;AAmBD,sCAYC;AAiBD,oDAQC;AArED,4CAAoB;AACpB,gDAAwB;AACxB,qCAAsC;AAEtC,MAAM,cAAc,GAAG,+CAA+C,CAAC;AACvE,MAAM,oBAAoB,GAAG,0BAA0B,CAAC;AAExD,SAAgB,cAAc,CAAC,OAAe;IAC5C,IAAI,OAAO,OAAO,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC7C,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,oBAAoB,CAAC,CAAC;IAClD,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;IACzC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAChC,CAAC;AAED,8EAA8E;AAC9E,6EAA6E;AAC7E,6EAA6E;AAC7E,SAAS,kBAAkB,CACzB,QAAgB,EAChB,YAAoB,EACpB,UAAoB;IAEpB,IAAI,cAAI,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;QAClC,MAAM,SAAS,GAAG,YAAY,GAAG,KAAK,CAAC;QACvC,OAAO,UAAU,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC;IAC3D,CAAC;IACD,MAAM,QAAQ,GAAG,cAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,YAAY,GAAG,KAAK,CAAC,CAAC;IAC9D,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,cAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,cAAI,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACzE,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAgB,aAAa,CAC3B,QAAgB,EAChB,YAAoB,EACpB,UAAoB;IAEpB,MAAM,QAAQ,GAAG,kBAAkB,CAAC,QAAQ,EAAE,YAAY,EAAE,UAAU,CAAC,CAAC;IACxE,IAAI,CAAC,QAAQ,IAAI,CAAC,YAAE,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IACvD,IAAI,CAAC;QACH,OAAO,cAAc,CAAC,YAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;IAC5D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAEY,QAAA,kBAAkB,GAAG,aAAa,CAAC;AAEhD,2EAA2E;AAC3E,8EAA8E;AAC9E,gEAAgE;AAChE,MAAa,wBAAyB,SAAQ,KAAK;IAEjD,YACE,OAAO,GAAG,iDAAiD;QAE3D,KAAK,CAAC,OAAO,CAAC,CAAC;QAJR,SAAI,GAAG,sBAAsB,CAAC;QAKrC,IAAI,CAAC,IAAI,GAAG,0BAA0B,CAAC;IACzC,CAAC;CACF;AARD,4DAQC;AAED,SAAgB,oBAAoB,CAClC,QAAgB,EAChB,YAAoB;IAEpB,MAAM,EAAE,UAAU,GAAG,EAAE,EAAE,GAAG,IAAA,mBAAU,EAAC,QAAQ,CAAC,CAAC;IACjD,IAAI,aAAa,CAAC,QAAQ,EAAE,YAAY,EAAE,UAAU,CAAC,KAAK,0BAAkB,EAAE,CAAC;QAC7E,MAAM,IAAI,wBAAwB,EAAE,CAAC;IACvC,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "living-ai-documentation",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Local Markdown documentation hub with a built-in MCP server — coding agents create ADRs, draw diagrams and detect drift while you code.",
|
|
5
5
|
"main": "dist/src/server.js",
|
|
6
6
|
"bin": {
|