living-documentation 3.8.0 → 4.1.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.

Potentially problematic release.


This version of living-documentation might be problematic. Click here for more details.

@@ -265,10 +265,10 @@
265
265
  <header
266
266
  class="flex items-center gap-1 px-2 h-12 shrink-0 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 shadow-sm z-10"
267
267
  >
268
- <a
269
- href="/"
268
+ <button
269
+ onclick="history.back()"
270
270
  class="tool-btn !w-auto px-2 text-xs font-medium text-gray-500 dark:text-gray-400"
271
- >← Docs</a
271
+ >← Back</button
272
272
  >
273
273
  <div class="w-px h-6 bg-gray-200 dark:bg-gray-700 mx-0.5"></div>
274
274
  <button id="btnSidebar" class="tool-btn" title="Mes diagrammes">
@@ -685,6 +685,16 @@
685
685
  >
686
686
 
687
687
  </button>
688
+ <button
689
+ id="btnNodeLink"
690
+ class="tool-btn !w-6 !h-6"
691
+ title="Ajouter / modifier un lien"
692
+ >
693
+ <svg width="13" height="13" viewBox="0 0 13 13" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
694
+ <path d="M5 7.5a3.5 3.5 0 0 0 5 0l1.5-1.5a3.5 3.5 0 0 0-5-5L5 2.5"/>
695
+ <path d="M8 5.5a3.5 3.5 0 0 0-5 0L1.5 7a3.5 3.5 0 0 0 5 5L8 10.5"/>
696
+ </svg>
697
+ </button>
688
698
  <div class="panel-sep"></div>
689
699
  <button
690
700
  id="btnNodeFontDecrease"
@@ -924,6 +934,74 @@
924
934
  </svg>
925
935
  PNG
926
936
  </button>
937
+
938
+ <div class="panel-sep"></div>
939
+
940
+ <!-- Group -->
941
+ <button
942
+ id="btnGroup"
943
+ class="tool-btn !w-8 !h-7"
944
+ title="Grouper la sélection"
945
+ >
946
+ <svg width="22" height="14" viewBox="0 0 22 14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round">
947
+ <rect x="0.7" y="1" width="20.6" height="12" rx="2" stroke-dasharray="2.5,1.5"/>
948
+ <rect x="4" y="4.5" width="3" height="5" rx="1" fill="currentColor"/>
949
+ <rect x="15" y="4.5" width="3" height="5" rx="1" fill="currentColor"/>
950
+ </svg>
951
+ </button>
952
+
953
+ <!-- Ungroup -->
954
+ <button
955
+ id="btnUngroup"
956
+ class="tool-btn !w-8 !h-7"
957
+ title="Dégrouper"
958
+ >
959
+ <svg width="26" height="14" viewBox="0 0 26 14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round">
960
+ <rect x="0.7" y="1" width="9" height="12" rx="2" stroke-dasharray="2.5,1.5"/>
961
+ <rect x="3" y="4.5" width="3" height="5" rx="1" fill="currentColor"/>
962
+ <rect x="16.3" y="1" width="9" height="12" rx="2" stroke-dasharray="2.5,1.5"/>
963
+ <rect x="19" y="4.5" width="3" height="5" rx="1" fill="currentColor"/>
964
+ </svg>
965
+ </button>
966
+ </div>
967
+
968
+ <!-- Link panel -->
969
+ <div id="linkPanel" class="float-panel hidden" style="min-width:260px; flex-direction:column; align-items:stretch; gap:0.5rem; padding:0.75rem;">
970
+ <div class="text-xs font-semibold text-gray-500 dark:text-gray-400 mb-1">Lien sur cette forme</div>
971
+
972
+ <label class="flex items-center gap-2 text-xs cursor-pointer">
973
+ <input type="radio" name="linkType" id="linkTypeUrl" value="url" class="accent-orange-500"/>
974
+ URL externe
975
+ </label>
976
+ <div id="linkUrlRow">
977
+ <input id="linkUrlInput" type="url" placeholder="https://…"
978
+ class="w-full text-xs border border-gray-300 dark:border-gray-600 rounded px-2 py-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-1 focus:ring-orange-400"/>
979
+ </div>
980
+
981
+ <label class="flex items-center gap-2 text-xs cursor-pointer">
982
+ <input type="radio" name="linkType" id="linkTypeDiagram" value="diagram" class="accent-orange-500"/>
983
+ Diagramme existant
984
+ </label>
985
+ <div id="linkDiagramRow" class="hidden">
986
+ <select id="linkDiagramSelect"
987
+ class="w-full text-xs border border-gray-300 dark:border-gray-600 rounded px-2 py-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-1 focus:ring-orange-400">
988
+ </select>
989
+ </div>
990
+
991
+ <label class="flex items-center gap-2 text-xs cursor-pointer">
992
+ <input type="radio" name="linkType" id="linkTypeNew" value="new" class="accent-orange-500"/>
993
+ Nouveau diagramme
994
+ </label>
995
+ <div id="linkNewRow" class="hidden">
996
+ <input id="linkNewName" type="text" placeholder="Nom du diagramme…"
997
+ class="w-full text-xs border border-gray-300 dark:border-gray-600 rounded px-2 py-1 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-1 focus:ring-orange-400"/>
998
+ </div>
999
+
1000
+ <div class="flex gap-1 mt-1">
1001
+ <button id="btnLinkSave" class="flex-1 text-xs bg-orange-500 hover:bg-orange-600 text-white rounded px-2 py-1">Enregistrer</button>
1002
+ <button id="btnLinkRemove" class="text-xs border border-gray-300 dark:border-gray-600 rounded px-2 py-1 hover:bg-red-50 dark:hover:bg-red-900/20 hover:text-red-600">Retirer</button>
1003
+ <button id="btnLinkCancel" class="text-xs border border-gray-300 dark:border-gray-600 rounded px-2 py-1 hover:bg-gray-100 dark:hover:bg-gray-700">✕</button>
1004
+ </div>
927
1005
  </div>
928
1006
 
929
1007
  <!-- Edge panel -->
@@ -17,6 +17,8 @@
17
17
 
18
18
  <!-- WordCloud2.js — vendored from npm, no CDN dependency -->
19
19
  <script src="/vendor/wordcloud2.js"></script>
20
+ <!-- Word Cloud logic — stop words, browser, rendering -->
21
+ <script src="/wordcloud.js"></script>
20
22
 
21
23
  <script>
22
24
  tailwind.config = {
@@ -281,7 +283,7 @@
281
283
  <article id="doc-view" class="hidden max-w-4xl mx-auto px-6 py-8">
282
284
  <!-- Doc header -->
283
285
  <header
284
- class="mb-8 pb-6 border-b border-gray-100 dark:border-gray-800"
286
+ class="sticky top-0 z-10 bg-gray-50 dark:bg-gray-950 -mx-6 px-6 pt-8 mb-8 pb-6 border-b border-gray-300 dark:border-gray-800"
285
287
  >
286
288
  <div class="flex items-start justify-between gap-4 flex-wrap">
287
289
  <div class="flex-1 min-w-0">
@@ -348,14 +350,16 @@
348
350
  </div>
349
351
  </div>
350
352
  </div>
353
+ <!-- Search match notice (inside sticky header) -->
354
+ <div
355
+ id="search-notice"
356
+ class="hidden mt-4 rounded-lg bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 text-sm text-yellow-800 dark:text-yellow-300 overflow-hidden"
357
+ >
358
+ <div id="search-notice-title" class="px-3 py-2 font-medium border-b border-yellow-200 dark:border-yellow-800"></div>
359
+ <ol id="search-notice-list" class="max-h-40 overflow-y-auto divide-y divide-yellow-100 dark:divide-yellow-900/40 list-none m-0 p-0"></ol>
360
+ </div>
351
361
  </header>
352
362
 
353
- <!-- Search match notice -->
354
- <div
355
- id="search-notice"
356
- class="hidden mb-4 px-3 py-2 rounded-lg bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 text-sm text-yellow-800 dark:text-yellow-300"
357
- ></div>
358
-
359
363
  <!-- Rendered markdown -->
360
364
  <div
361
365
  id="doc-content"
@@ -391,6 +395,73 @@
391
395
  &#10005; Close
392
396
  </button>
393
397
  </div>
398
+ <!-- Search root toolbar -->
399
+ <div class="border-b border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 shrink-0">
400
+ <!-- Row 1: path + browse + launch -->
401
+ <div class="flex items-center gap-3 px-6 py-3">
402
+ <label class="text-xs font-medium text-gray-500 dark:text-gray-400 shrink-0">Search root</label>
403
+ <input
404
+ id="wc-root"
405
+ type="text"
406
+ readonly
407
+ spellcheck="false"
408
+ class="flex-1 px-3 py-1.5 text-sm rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 font-mono cursor-default focus:outline-none"
409
+ />
410
+ <button
411
+ onclick="wcToggleBrowser()"
412
+ class="text-sm px-3 py-1.5 rounded-lg border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors shrink-0"
413
+ >
414
+ &#128193; Browse
415
+ </button>
416
+ <button
417
+ onclick="launchWordCloud()"
418
+ class="text-sm px-4 py-1.5 rounded-lg bg-blue-600 hover:bg-blue-700 text-white font-semibold transition-colors shrink-0"
419
+ >
420
+ &#9654; Launch
421
+ </button>
422
+ </div>
423
+ <!-- Row 2: extension checkboxes -->
424
+ <div class="flex items-center gap-x-4 gap-y-2 px-6 pb-3 flex-wrap">
425
+ <span class="text-xs font-medium text-gray-500 dark:text-gray-400 shrink-0">Extensions</span>
426
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="md" checked /> .md</label>
427
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="txt" /> .txt</label>
428
+ <span class="text-gray-200 dark:text-gray-700 text-xs">|</span>
429
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="ts" /> .ts</label>
430
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="tsx" /> .tsx</label>
431
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="js" /> .js</label>
432
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="jsx" /> .jsx</label>
433
+ <span class="text-gray-200 dark:text-gray-700 text-xs">|</span>
434
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="java" /> .java</label>
435
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="kt" /> .kt</label>
436
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="py" /> .py</label>
437
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="go" /> .go</label>
438
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="rs" /> .rs</label>
439
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="cs" /> .cs</label>
440
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="swift" /> .swift</label>
441
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="rb" /> .rb</label>
442
+ <span class="text-gray-200 dark:text-gray-700 text-xs">|</span>
443
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="html" /> .html</label>
444
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="css" /> .css</label>
445
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="scss" /> .scss</label>
446
+ <span class="text-gray-200 dark:text-gray-700 text-xs">|</span>
447
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="yml" /> .yml</label>
448
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="yaml" /> .yaml</label>
449
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="json" /> .json</label>
450
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="xml" /> .xml</label>
451
+ <label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300 cursor-pointer"><input type="checkbox" class="wc-ext" value="toml" /> .toml</label>
452
+ </div>
453
+ <!-- Row 3: inline folder browser (collapsed by default) -->
454
+ <div id="wc-browser" class="hidden border-t border-gray-200 dark:border-gray-700">
455
+ <div class="flex items-center gap-2 px-3 py-2 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
456
+ <button id="wc-browse-up" onclick="wcBrowseUp()"
457
+ class="text-xs text-blue-600 dark:text-blue-400 hover:underline disabled:opacity-30 disabled:pointer-events-none shrink-0">
458
+ &#8593; Up
459
+ </button>
460
+ <span id="wc-browse-path" class="font-mono text-xs text-gray-400 dark:text-gray-500 truncate flex-1 text-right"></span>
461
+ </div>
462
+ <div id="wc-browse-list" class="divide-y divide-gray-100 dark:divide-gray-800 max-h-52 overflow-y-auto"></div>
463
+ </div>
464
+ </div>
394
465
  <div
395
466
  id="wc-body"
396
467
  class="flex-1 relative overflow-hidden flex items-center justify-center"
@@ -419,6 +490,7 @@
419
490
  applyDarkMode(loadDarkPref());
420
491
  setupDarkToggle();
421
492
  setupSearch();
493
+ wcRestorePrefs();
422
494
  await loadConfig();
423
495
  await loadDocuments();
424
496
 
@@ -622,8 +694,8 @@
622
694
  // Highlight search matches in content
623
695
  const notice = document.getElementById("search-notice");
624
696
  if (searchQuery) {
625
- highlightMatches(contentEl, searchQuery);
626
- notice.textContent = `Showing matches for "${searchQuery}"`;
697
+ const matches = highlightMatches(contentEl, searchQuery);
698
+ buildSearchNotice(matches, searchQuery);
627
699
  notice.classList.remove("hidden");
628
700
  } else {
629
701
  notice.classList.add("hidden");
@@ -685,7 +757,10 @@
685
757
  }
686
758
 
687
759
  // ── Edit mode ──────────────────────────────────────────────
760
+ let _editScrollTop = 0;
761
+
688
762
  function enterEditMode() {
763
+ _editScrollTop = document.getElementById("content-area").scrollTop;
689
764
  const editor = document.getElementById("doc-editor");
690
765
  editor.value = currentDocContent;
691
766
  document.getElementById("doc-content").classList.add("hidden");
@@ -704,6 +779,7 @@
704
779
  document.getElementById("edit-actions").classList.add("hidden");
705
780
  document.getElementById("view-actions").classList.remove("hidden");
706
781
  document.getElementById("edit-save-msg").textContent = "";
782
+ document.getElementById("content-area").scrollTop = _editScrollTop;
707
783
  }
708
784
 
709
785
  async function handleEditorPaste(e) {
@@ -816,16 +892,50 @@
816
892
  const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
817
893
  const nodes = [];
818
894
  while (walker.nextNode()) nodes.push(walker.currentNode);
819
- const re = new RegExp(
820
- `(${q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`,
821
- "gi",
822
- );
895
+ const re = new RegExp(`(${q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`, "gi");
896
+ let idx = 0;
823
897
  nodes.forEach((node) => {
824
898
  if (!node.textContent.toLowerCase().includes(q.toLowerCase())) return;
825
899
  const span = document.createElement("span");
826
- span.innerHTML = node.textContent.replace(re, "<mark>$1</mark>");
900
+ span.innerHTML = node.textContent.replace(re, (m) => `<mark id="match-${idx++}">${m}</mark>`);
827
901
  node.parentNode.replaceChild(span, node);
828
902
  });
903
+ // Build snippet list from inserted marks
904
+ const matches = [];
905
+ el.querySelectorAll("mark[id^='match-']").forEach((mark) => {
906
+ const block = mark.closest("p, li, td, h1, h2, h3, h4, h5, h6, pre") || mark.parentElement;
907
+ const full = block.textContent.trim();
908
+ const pos = full.toLowerCase().indexOf(q.toLowerCase());
909
+ const start = Math.max(0, pos - 40);
910
+ const end = Math.min(full.length, pos + q.length + 40);
911
+ const snippet = (start > 0 ? "…" : "") + full.slice(start, end) + (end < full.length ? "…" : "");
912
+ matches.push({ id: mark.id, snippet });
913
+ });
914
+ return matches;
915
+ }
916
+
917
+ function buildSearchNotice(matches, q) {
918
+ document.getElementById("search-notice-title").textContent =
919
+ `Showing ${matches.length} match${matches.length === 1 ? "" : "es"} for "${q}"`;
920
+ const list = document.getElementById("search-notice-list");
921
+ list.innerHTML = matches.map((m, i) =>
922
+ `<li>
923
+ <button onclick="scrollToMatch('${m.id}')"
924
+ class="w-full text-left px-3 py-1.5 hover:bg-yellow-100 dark:hover:bg-yellow-900/30 transition-colors">
925
+ <span class="text-yellow-600 dark:text-yellow-500 font-mono text-xs mr-2">${i + 1}.</span
926
+ ><span class="text-xs">${esc(m.snippet)}</span>
927
+ </button>
928
+ </li>`
929
+ ).join("");
930
+ }
931
+
932
+ function scrollToMatch(id) {
933
+ const mark = document.getElementById(id);
934
+ const container = document.getElementById("content-area");
935
+ if (!mark || !container) return;
936
+ const markTop = mark.getBoundingClientRect().top;
937
+ const containerTop = container.getBoundingClientRect().top;
938
+ container.scrollTop += markTop - containerTop - container.clientHeight / 3;
829
939
  }
830
940
 
831
941
  function esc(str) {
@@ -837,244 +947,6 @@
837
947
  .replace(/'/g, "&#39;");
838
948
  }
839
949
 
840
- // ── Word Cloud ─────────────────────────────────────────────
841
- const WC_STOP_WORDS = new Set([
842
- // English
843
- "the",
844
- "and",
845
- "for",
846
- "are",
847
- "but",
848
- "not",
849
- "you",
850
- "all",
851
- "this",
852
- "that",
853
- "with",
854
- "have",
855
- "from",
856
- "they",
857
- "will",
858
- "one",
859
- "been",
860
- "can",
861
- "has",
862
- "was",
863
- "more",
864
- "also",
865
- "when",
866
- "there",
867
- "their",
868
- "what",
869
- "about",
870
- "which",
871
- "would",
872
- "into",
873
- "than",
874
- "then",
875
- "each",
876
- "just",
877
- "over",
878
- "after",
879
- "such",
880
- "here",
881
- "its",
882
- "your",
883
- "our",
884
- "some",
885
- "were",
886
- "very",
887
- "only",
888
- "out",
889
- "had",
890
- "she",
891
- "his",
892
- "her",
893
- "him",
894
- "who",
895
- "how",
896
- "any",
897
- "other",
898
- "these",
899
- "those",
900
- "being",
901
- "may",
902
- "use",
903
- "used",
904
- "using",
905
- "should",
906
- "could",
907
- "would",
908
- "shall",
909
- "must",
910
- "need",
911
- "via",
912
- "per",
913
- "like",
914
- // French
915
- "les",
916
- "des",
917
- "une",
918
- "pour",
919
- "pas",
920
- "sur",
921
- "par",
922
- "est",
923
- "qui",
924
- "que",
925
- "dans",
926
- "avec",
927
- "sont",
928
- "plus",
929
- "tout",
930
- "aux",
931
- "mais",
932
- "comme",
933
- "vous",
934
- "nous",
935
- "leur",
936
- "lui",
937
- "elle",
938
- "ils",
939
- "elles",
940
- "ces",
941
- "ses",
942
- "mon",
943
- "ton",
944
- "son",
945
- "mes",
946
- "tes",
947
- "ainsi",
948
- "donc",
949
- "alors",
950
- "car",
951
- "peut",
952
- "fait",
953
- "encore",
954
- "bien",
955
- "aussi",
956
- "très",
957
- "même",
958
- "entre",
959
- "vers",
960
- "dont",
961
- "sans",
962
- "sous",
963
- ]);
964
-
965
- async function openWordCloud() {
966
- const overlay = document.getElementById("wc-overlay");
967
- const status = document.getElementById("wc-status");
968
- const canvas = document.getElementById("wc-canvas");
969
- overlay.classList.remove("hidden");
970
- status.textContent = "Loading documents…";
971
- status.classList.remove("hidden");
972
- canvas.classList.add("hidden");
973
-
974
- try {
975
- const docs = allDocs.length
976
- ? allDocs
977
- : await fetch("/api/documents").then((r) => r.json());
978
- status.textContent = `Analyzing ${docs.length} documents…`;
979
-
980
- const results = await Promise.all(
981
- docs.map((doc) =>
982
- fetch("/api/documents/" + doc.id)
983
- .then((r) => (r.ok ? r.json() : null))
984
- .catch(() => null),
985
- ),
986
- );
987
-
988
- const freq = {};
989
- for (const doc of results) {
990
- console.log("doc = ", doc);
991
- if (!doc?.content) continue;
992
- for (const w of extractWordsFromMarkdown(doc.content)) {
993
- freq[w] = (freq[w] || 0) + 1;
994
- }
995
- }
996
-
997
- const list = Object.entries(freq)
998
- .filter(([, n]) => n >= 2)
999
- .sort((a, b) => b[1] - a[1])
1000
- .slice(0, 150);
1001
-
1002
- if (!list.length) {
1003
- status.textContent = "Not enough words found.";
1004
- return;
1005
- }
1006
-
1007
- renderWordCloud(list);
1008
- } catch (err) {
1009
- status.textContent = "Error: " + err.message;
1010
- }
1011
- }
1012
-
1013
- function extractWordsFromMarkdown(text) {
1014
- return text
1015
- .replace(/```[\s\S]*?```/g, "")
1016
- .replace(/`[^`\n]+`/g, "")
1017
- .replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
1018
- .replace(/https?:\/\/\S+/g, "")
1019
- .replace(/[#*_~>`|!\[\](){}=\-+]/g, " ")
1020
- .toLowerCase()
1021
- .split(/[^a-zàâäéèêëïîôùûü']+/)
1022
- .map((w) => w.replace(/^'+|'+$/g, ""))
1023
- .filter((w) => w.length > 3 && !WC_STOP_WORDS.has(w));
1024
- }
1025
-
1026
- function renderWordCloud(list) {
1027
- const canvas = document.getElementById("wc-canvas");
1028
- const body = document.getElementById("wc-body");
1029
- canvas.width = body.clientWidth;
1030
- canvas.height = body.clientHeight;
1031
-
1032
- const isDark = document.documentElement.classList.contains("dark");
1033
- const colors = isDark
1034
- ? [
1035
- "#60a5fa",
1036
- "#34d399",
1037
- "#f9a8d4",
1038
- "#a78bfa",
1039
- "#fbbf24",
1040
- "#6ee7b7",
1041
- "#93c5fd",
1042
- "#fb923c",
1043
- ]
1044
- : [
1045
- "#1d4ed8",
1046
- "#047857",
1047
- "#7c3aed",
1048
- "#b45309",
1049
- "#be123c",
1050
- "#0369a1",
1051
- "#4338ca",
1052
- "#c2410c",
1053
- ];
1054
-
1055
- const maxFreq = list[0][1];
1056
- const wordList = list.map(([w, n]) => [
1057
- w,
1058
- Math.max(10, Math.round((72 * n) / maxFreq)),
1059
- ]);
1060
-
1061
- document.getElementById("wc-status").classList.add("hidden");
1062
- canvas.classList.remove("hidden");
1063
-
1064
- WordCloud(canvas, {
1065
- list: wordList,
1066
- gridSize: Math.round((8 * canvas.width) / 1024),
1067
- fontFamily: "ui-sans-serif, system-ui, sans-serif",
1068
- color: () => colors[Math.floor(Math.random() * colors.length)],
1069
- backgroundColor: isDark ? "#030712" : "#ffffff",
1070
- rotateRatio: 0.3,
1071
- minSize: 10,
1072
- });
1073
- }
1074
-
1075
- function closeWordCloud() {
1076
- document.getElementById("wc-overlay").classList.add("hidden");
1077
- }
1078
950
 
1079
951
  // Browser back/forward
1080
952
  window.addEventListener("popstate", (e) => {