living-documentation 7.0.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/LICENSE +661 -0
- package/README.md +329 -0
- package/dist/bin/cli.d.ts +3 -0
- package/dist/bin/cli.d.ts.map +1 -0
- package/dist/bin/cli.js +62 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/src/frontend/admin.html +1073 -0
- package/dist/src/frontend/annotations.js +546 -0
- package/dist/src/frontend/boot.js +90 -0
- package/dist/src/frontend/config.js +19 -0
- package/dist/src/frontend/dark-mode.js +20 -0
- package/dist/src/frontend/diagram/alignment.js +161 -0
- package/dist/src/frontend/diagram/clipboard.js +172 -0
- package/dist/src/frontend/diagram/constants.js +109 -0
- package/dist/src/frontend/diagram/debug.js +43 -0
- package/dist/src/frontend/diagram/edge-panel.js +260 -0
- package/dist/src/frontend/diagram/edge-rendering.js +12 -0
- package/dist/src/frontend/diagram/grid.js +78 -0
- package/dist/src/frontend/diagram/groups.js +102 -0
- package/dist/src/frontend/diagram/history.js +153 -0
- package/dist/src/frontend/diagram/image-name-modal.js +48 -0
- package/dist/src/frontend/diagram/image-upload.js +36 -0
- package/dist/src/frontend/diagram/label-editor.js +115 -0
- package/dist/src/frontend/diagram/link-panel.js +144 -0
- package/dist/src/frontend/diagram/main.js +299 -0
- package/dist/src/frontend/diagram/network.js +1473 -0
- package/dist/src/frontend/diagram/node-panel.js +267 -0
- package/dist/src/frontend/diagram/node-rendering.js +773 -0
- package/dist/src/frontend/diagram/persistence.js +161 -0
- package/dist/src/frontend/diagram/ports.js +386 -0
- package/dist/src/frontend/diagram/selection-overlay.js +336 -0
- package/dist/src/frontend/diagram/state.js +39 -0
- package/dist/src/frontend/diagram/t.js +3 -0
- package/dist/src/frontend/diagram/toast.js +21 -0
- package/dist/src/frontend/diagram/unlock-hold.js +182 -0
- package/dist/src/frontend/diagram/zoom.js +20 -0
- package/dist/src/frontend/diagram-link-modal.js +137 -0
- package/dist/src/frontend/diagram.html +1279 -0
- package/dist/src/frontend/documents.js +373 -0
- package/dist/src/frontend/export.js +338 -0
- package/dist/src/frontend/i18n/en.json +406 -0
- package/dist/src/frontend/i18n/fr.json +406 -0
- package/dist/src/frontend/i18n.js +32 -0
- package/dist/src/frontend/image-paste.js +101 -0
- package/dist/src/frontend/index.html +2314 -0
- package/dist/src/frontend/misc.js +25 -0
- package/dist/src/frontend/new-doc-modal.js +260 -0
- package/dist/src/frontend/new-folder-modal.js +174 -0
- package/dist/src/frontend/search.js +157 -0
- package/dist/src/frontend/sidebar-helpers.js +58 -0
- package/dist/src/frontend/sidebar.js +182 -0
- package/dist/src/frontend/snippet-detect.js +25 -0
- package/dist/src/frontend/snippet-table.js +85 -0
- package/dist/src/frontend/snippet-tree.js +94 -0
- package/dist/src/frontend/snippets.js +534 -0
- package/dist/src/frontend/state.js +28 -0
- package/dist/src/frontend/utils.js +21 -0
- package/dist/src/frontend/vendor/wordcloud2.js +1187 -0
- package/dist/src/frontend/wordcloud.js +693 -0
- package/dist/src/lib/config.d.ts +17 -0
- package/dist/src/lib/config.d.ts.map +1 -0
- package/dist/src/lib/config.js +79 -0
- package/dist/src/lib/config.js.map +1 -0
- package/dist/src/lib/parser.d.ts +11 -0
- package/dist/src/lib/parser.d.ts.map +1 -0
- package/dist/src/lib/parser.js +111 -0
- package/dist/src/lib/parser.js.map +1 -0
- package/dist/src/mcp/server.d.ts +3 -0
- package/dist/src/mcp/server.d.ts.map +1 -0
- package/dist/src/mcp/server.js +986 -0
- package/dist/src/mcp/server.js.map +1 -0
- package/dist/src/mcp/tools/diagrams.d.ts +44 -0
- package/dist/src/mcp/tools/diagrams.d.ts.map +1 -0
- package/dist/src/mcp/tools/diagrams.js +245 -0
- package/dist/src/mcp/tools/diagrams.js.map +1 -0
- package/dist/src/mcp/tools/documents.d.ts +26 -0
- package/dist/src/mcp/tools/documents.d.ts.map +1 -0
- package/dist/src/mcp/tools/documents.js +127 -0
- package/dist/src/mcp/tools/documents.js.map +1 -0
- package/dist/src/mcp/tools/source.d.ts +29 -0
- package/dist/src/mcp/tools/source.d.ts.map +1 -0
- package/dist/src/mcp/tools/source.js +200 -0
- package/dist/src/mcp/tools/source.js.map +1 -0
- package/dist/src/routes/annotations.d.ts +3 -0
- package/dist/src/routes/annotations.d.ts.map +1 -0
- package/dist/src/routes/annotations.js +83 -0
- package/dist/src/routes/annotations.js.map +1 -0
- package/dist/src/routes/browse.d.ts +3 -0
- package/dist/src/routes/browse.d.ts.map +1 -0
- package/dist/src/routes/browse.js +75 -0
- package/dist/src/routes/browse.js.map +1 -0
- package/dist/src/routes/config.d.ts +3 -0
- package/dist/src/routes/config.d.ts.map +1 -0
- package/dist/src/routes/config.js +97 -0
- package/dist/src/routes/config.js.map +1 -0
- package/dist/src/routes/diagrams.d.ts +3 -0
- package/dist/src/routes/diagrams.d.ts.map +1 -0
- package/dist/src/routes/diagrams.js +69 -0
- package/dist/src/routes/diagrams.js.map +1 -0
- package/dist/src/routes/documents.d.ts +8 -0
- package/dist/src/routes/documents.d.ts.map +1 -0
- package/dist/src/routes/documents.js +332 -0
- package/dist/src/routes/documents.js.map +1 -0
- package/dist/src/routes/export.d.ts +3 -0
- package/dist/src/routes/export.d.ts.map +1 -0
- package/dist/src/routes/export.js +277 -0
- package/dist/src/routes/export.js.map +1 -0
- package/dist/src/routes/images.d.ts +3 -0
- package/dist/src/routes/images.d.ts.map +1 -0
- package/dist/src/routes/images.js +49 -0
- package/dist/src/routes/images.js.map +1 -0
- package/dist/src/routes/wordcloud.d.ts +3 -0
- package/dist/src/routes/wordcloud.d.ts.map +1 -0
- package/dist/src/routes/wordcloud.js +95 -0
- package/dist/src/routes/wordcloud.js.map +1 -0
- package/dist/src/server.d.ts +7 -0
- package/dist/src/server.d.ts.map +1 -0
- package/dist/src/server.js +76 -0
- package/dist/src/server.js.map +1 -0
- package/dist/starting-doc/.annotations.json +3 -0
- package/dist/starting-doc/.diagrams.json +1884 -0
- package/dist/starting-doc/.living-doc.json +39 -0
- package/dist/starting-doc/1_tutorial/2026_04_11_13_25_[General]_crer_vos_dossiers.md +16 -0
- package/dist/starting-doc/1_tutorial/2026_04_11_18_58_[General]_creer_un_document_dans_un_dossier.md +9 -0
- package/dist/starting-doc/1_tutorial/2026_04_12_09_00_[General]_editer_et_sauvegarder.md +39 -0
- package/dist/starting-doc/1_tutorial/2026_04_12_10_00_[General]_utiliser_les_snippets.md +71 -0
- package/dist/starting-doc/2026_04_08_20_52_[General]_welcome.md +17 -0
- package/dist/starting-doc/2026_04_11_12_55_[General]_premiers_pas.md +271 -0
- package/dist/starting-doc/2_guide/2026_04_08_00_04_[DOCUMENT]_utilisation_des_images_plein_ecran_lien_clickable.md +40 -0
- package/dist/starting-doc/2_guide/2026_04_08_23_38_[Configuration]_demarrage_de_living_documentation.md +32 -0
- package/dist/starting-doc/2_guide/2026_04_09_09_00_[NAVIGATION]_recherche_plein_texte.md +65 -0
- package/dist/starting-doc/2_guide/2026_04_09_10_00_[EXPORT]_exporter_en_pdf.md +43 -0
- package/dist/starting-doc/2_guide/2026_04_09_11_00_[Configuration]_configurer_le_panneau_admin.md +55 -0
- package/dist/starting-doc/2_guide/2026_04_09_12_00_[Configuration]_extra_files.md +68 -0
- package/dist/starting-doc/2_guide/2026_04_09_13_00_[WORDCLOUD]_word_cloud.md +54 -0
- package/dist/starting-doc/2_guide/2026_04_09_14_00_[DIAGRAM]_creer_et_lier_un_diagramme.md +77 -0
- package/dist/starting-doc/3_concept/2026_04_08_20_58_[DOCUMENTING]_ADRS.md +20 -0
- package/dist/starting-doc/3_concept/2026_04_08_22_15_[DOCUMENTING]_living_documentation.md +17 -0
- package/dist/starting-doc/3_concept/2026_04_08_22_46_[METHODOLOGY]_diataxis_architecture_du_contenu.md +16 -0
- package/dist/starting-doc/4_reference/2026_04_08_23_14_[FUNDAMENTALS]_the_living_documentation_tool.md +41 -0
- package/dist/starting-doc/4_reference/2026_04_09_01_00_[REFERENCE]_raccourcis_clavier.md +61 -0
- package/dist/starting-doc/4_reference/2026_04_09_02_00_[REFERENCE]_tokens_pattern_nommage.md +75 -0
- package/dist/starting-doc/4_reference/2026_04_09_03_00_[REFERENCE]_types_de_snippets.md +68 -0
- package/dist/starting-doc/4_reference/2026_04_11_17_31_[FUNDAMENTALS]_architecturer_une_documentation.md +12 -0
- package/dist/starting-doc/4_reference/2026_04_12_14_07_[FUNDAMENTALS]_dossiers_et_catgories.md +89 -0
- package/dist/starting-doc/images/admin_screenshot.png +0 -0
- package/dist/starting-doc/images/ajout-document.png +0 -0
- package/dist/starting-doc/images/ajouter-document-categorie.png +0 -0
- package/dist/starting-doc/images/ajouter_un_document_dans_un_dossier.png +0 -0
- package/dist/starting-doc/images/architecturer_une_documentation_reference.png +0 -0
- package/dist/starting-doc/images/cr_er_un_document.png +0 -0
- package/dist/starting-doc/images/creation-nouveau-dossier.png +0 -0
- package/dist/starting-doc/images/creer-document-context-engineering.png +0 -0
- package/dist/starting-doc/images/creer-dossier-only-tutoriel.png +0 -0
- package/dist/starting-doc/images/creer-dossier-tutoriel.png +0 -0
- package/dist/starting-doc/images/creer-dossiers-done.png +0 -0
- package/dist/starting-doc/images/creer-un-document.png +0 -0
- package/dist/starting-doc/images/creer-vos-dossiers-tutoriel.png +0 -0
- package/dist/starting-doc/images/creer-vos-dossiers.png +0 -0
- package/dist/starting-doc/images/decouverte_adrs.png +0 -0
- package/dist/starting-doc/images/diataxis.png +0 -0
- package/dist/starting-doc/images/diataxis_callout.png +0 -0
- package/dist/starting-doc/images/document-cree.png +0 -0
- package/dist/starting-doc/images/liens_snippets.png +0 -0
- package/dist/starting-doc/images/living_documentation.png +0 -0
- package/dist/starting-doc/images/npm_logo.png +0 -0
- package/dist/starting-doc/images/popup-creer-document.png +0 -0
- package/dist/starting-doc/images/popup-creer-dossier.png +0 -0
- package/dist/starting-doc/images/popup-dossier-cree.png +0 -0
- package/dist/starting-doc/images/quatre-dossiers-crees.png +0 -0
- package/dist/starting-doc/images/screenshot-living-doc.png +0 -0
- package/dist/starting-doc/images/the_living_documentation_tool.png +0 -0
- package/package.json +49 -0
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
// ── Word Cloud — domain language analyzer ────────────────────────────────────
|
|
2
|
+
// Loaded by index.html as a plain <script>; all symbols are global.
|
|
3
|
+
// Goal: extract BUSINESS/DOMAIN words only — filter out all programming noise.
|
|
4
|
+
|
|
5
|
+
// ── Human stop words (English + French) ──────────────────────────────────────
|
|
6
|
+
const WC_HUMAN_STOP_WORDS = new Set([
|
|
7
|
+
// English
|
|
8
|
+
"the","a","an","and","or","but","not","nor","so","yet","both","either",
|
|
9
|
+
"neither","is","are","was","were","be","been","being","have","has","had",
|
|
10
|
+
"do","does","did","will","would","shall","should","may","might","can",
|
|
11
|
+
"could","must","need","dare","ought","used","to","of","in","for","on",
|
|
12
|
+
"with","at","by","from","up","about","into","through","during","before",
|
|
13
|
+
"after","above","below","between","out","off","over","under","again",
|
|
14
|
+
"further","then","once","here","there","when","where","why","how","all",
|
|
15
|
+
"each","few","more","most","other","some","such","no","only","own","same",
|
|
16
|
+
"than","too","very","just","also","still","already","always","never",
|
|
17
|
+
"often","sometimes","usually","this","that","these","those","its","your",
|
|
18
|
+
"our","their","his","her","him","she","you","they","we","me","my","it",
|
|
19
|
+
"who","which","what","whose","whom","if","though","although","because",
|
|
20
|
+
"since","while","unless","until","whether","as","like","via","per","i",
|
|
21
|
+
"am","he","us","see","now","new","old","got","let","put","say","said",
|
|
22
|
+
"says","get","use","make","made","take","come","came","go","went","know",
|
|
23
|
+
"think","look","want","give","seem","back","down","even","much","well",
|
|
24
|
+
"good","work","tell","keep","hold","turn","move","play","live","help",
|
|
25
|
+
"run","start","off","way","too","few","show","hear","etc","eg","ie",
|
|
26
|
+
"one","two","three","four","five","six","seven","eight","nine","ten",
|
|
27
|
+
// French
|
|
28
|
+
"le","la","les","un","une","des","du","de","d","l","au","aux","ce","ci",
|
|
29
|
+
"cet","cette","ces","mon","ma","mes","ton","ta","tes","son","sa","ses",
|
|
30
|
+
"notre","nos","votre","vos","leur","leurs","je","tu","il","elle","on",
|
|
31
|
+
"nous","vous","ils","elles","me","te","se","lui","y","en","qui","que",
|
|
32
|
+
"quoi","dont","où","et","ou","mais","donc","or","ni","car","si","ne",
|
|
33
|
+
"pas","plus","moins","très","bien","mal","peu","beaucoup","trop","assez",
|
|
34
|
+
"aussi","comme","pour","par","avec","sans","dans","sur","sous","entre",
|
|
35
|
+
"vers","chez","contre","avant","après","pendant","depuis","selon","malgré",
|
|
36
|
+
"sauf","est","sont","était","être","avoir","fait","faire","dit","dire",
|
|
37
|
+
"peut","pouvoir","doit","devoir","veut","vouloir","sait","savoir","va",
|
|
38
|
+
"aller","vient","venir","tout","tous","toute","toutes","autre","autres",
|
|
39
|
+
"même","chaque","quel","quelle","quels","quelles","aucun","aucune",
|
|
40
|
+
"certain","certaine","plusieurs","quelque","quelques","tel","telle","tels",
|
|
41
|
+
"telles","cela","ceci","ça","celui","celle","ceux","celles","ici","voici",
|
|
42
|
+
"voilà","alors","ainsi","encore","déjà","jamais","toujours","souvent",
|
|
43
|
+
"parfois","rien","personne","chose","fois","jour","temps","part","cas",
|
|
44
|
+
"point","lieu","monde","cependant","néanmoins","pourtant","toutefois",
|
|
45
|
+
"quand","lorsque","puisque","parce","afin","puis","lors","notamment",
|
|
46
|
+
"surtout","seulement","vraiment","enfin","ensuite","ailleurs","là",
|
|
47
|
+
"ici","voici","voilà","dont","lequel","laquelle","lesquels","lesquelles",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
// ── Programming stop words (aggressive) ──────────────────────────────────────
|
|
51
|
+
const WC_PROG_STOP_WORDS = new Set([
|
|
52
|
+
// Language keywords (all combined)
|
|
53
|
+
"public","private","protected","static","void","class","interface",
|
|
54
|
+
"abstract","final","const","let","var","function","return","if","else",
|
|
55
|
+
"for","while","do","switch","case","break","continue","try","catch",
|
|
56
|
+
"throw","throws","finally","new","this","self","super","import","export",
|
|
57
|
+
"from","package","module","require","include","using","namespace","struct",
|
|
58
|
+
"enum","trait","impl","fn","def","func","fun","val","mut","async","await",
|
|
59
|
+
"yield","lambda","extends","implements","override","virtual","sealed",
|
|
60
|
+
"data","object","companion","where","when","match","guard","defer","go",
|
|
61
|
+
"chan","select","type","alias","as","is","in","of","with","true","false",
|
|
62
|
+
"null","nil","none","undefined","println","printf","fmt","log","print",
|
|
63
|
+
"main","args","argv","init","setup","teardown","describe","test","expect",
|
|
64
|
+
"assert","before","after","not","and","or","delete","typeof","instanceof",
|
|
65
|
+
"readonly","keyof","infer","never","unknown","declare","satisfies",
|
|
66
|
+
// Generic CRUD / architectural patterns
|
|
67
|
+
"get","set","add","remove","update","create","find","fetch","load","save",
|
|
68
|
+
"put","post","patch","list","map","filter","reduce","sort","merge","parse",
|
|
69
|
+
"format","convert","transform","validate","check","handle","process",
|
|
70
|
+
"execute","start","stop","reset","clear","close","open","read","write",
|
|
71
|
+
"send","receive","emit","trigger","dispatch","subscribe","unsubscribe",
|
|
72
|
+
"listen","notify","register","unregister","bind","apply","call","invoke",
|
|
73
|
+
// Architecture / structural
|
|
74
|
+
"callback","handler","listener","middleware","interceptor","resolver",
|
|
75
|
+
"provider","consumer","producer","builder","factory","adapter","wrapper",
|
|
76
|
+
"proxy","decorator","observer","visitor","strategy","command","controller",
|
|
77
|
+
"service","repository","dao","dto","entity","model","schema","config",
|
|
78
|
+
"context","state","store","action","reducer","selector","hook","ref",
|
|
79
|
+
"effect","memo","component","module","plugin","extension","util","utils",
|
|
80
|
+
"helper","helpers","common","shared","base","default","index","app","core",
|
|
81
|
+
"lib","src","test","spec","mock","stub","fixture","impl","internal","api",
|
|
82
|
+
"sdk","client","server",
|
|
83
|
+
// Request/response
|
|
84
|
+
"request","response","status","code","error","exception","message","result",
|
|
85
|
+
// Generic data identifiers
|
|
86
|
+
"data","value","key","name","label","title","description","item","element",
|
|
87
|
+
"node","child","parent","root","container","wrapper","layout","view","page",
|
|
88
|
+
"screen","panel","header","footer","sidebar","navbar","body","content",
|
|
89
|
+
"section","row","col","column","grid","flex","block","inline","span","div",
|
|
90
|
+
"input","output","param","params","arg","option","options","props","attr",
|
|
91
|
+
"attrs","field","fields","property","properties","method","methods",
|
|
92
|
+
// Primitive types
|
|
93
|
+
"string","number","boolean","integer","float","double","long","short",
|
|
94
|
+
"byte","char","array","object","map","set","list","vector","queue","stack",
|
|
95
|
+
"hash","table","tree","graph","pair","tuple","optional","nullable","void",
|
|
96
|
+
"unit","any","bool","bytes","none","int","str","bool",
|
|
97
|
+
// Quantifiers / positions
|
|
98
|
+
"all","each","every","some","many","single","multi","first","last","next",
|
|
99
|
+
"prev","previous","current","old","temp","tmp","new","top","bottom",
|
|
100
|
+
"left","right","center","middle","start","end","begin","min","max",
|
|
101
|
+
// Metrics / sizes
|
|
102
|
+
"count","total","sum","avg","size","length","width","height","depth",
|
|
103
|
+
"flag","num","idx",
|
|
104
|
+
// Common abbreviations
|
|
105
|
+
"id","uid","uuid","num","str","val","obj","arr","fn","cb","ctx","req",
|
|
106
|
+
"res","err","msg","src","dst","evt","doc","db","io","fs","os","env","sys",
|
|
107
|
+
// Protocols / formats
|
|
108
|
+
"http","https","url","uri","path","port","host","tcp","udp","ssl","tls",
|
|
109
|
+
"html","css","json","xml","yml","yaml","csv","sql","regex","utf","ascii",
|
|
110
|
+
"rgb","rgba","hex","px","em","rem","vh","vw","auto","inherit","initial",
|
|
111
|
+
// Generic single-concept words too vague to be domain
|
|
112
|
+
"application","file","local","source","messages","logging","alerting",
|
|
113
|
+
"programmed","offset","offsets","zone","zoned","decimal","big","integer",
|
|
114
|
+
"constructor","mapper","captor","publisher","verify","subscriber",
|
|
115
|
+
"org","edr","ack","ref","ssr","ssg","csr","isr",
|
|
116
|
+
// Testing frameworks & matchers
|
|
117
|
+
"mockito","junit","vitest","jest","mocha","rspec","pytest","cypress",
|
|
118
|
+
"playwright","captor","spy","given","then","verify","times","never",
|
|
119
|
+
"invocation","argumentcaptor","inorder","donothing","doreturn",
|
|
120
|
+
// Company/framework names that pollute
|
|
121
|
+
"ippon","springframework","jakarta","javax",
|
|
122
|
+
// Dev annotations
|
|
123
|
+
"todo","fixme","hack","note","deprecated","experimental","eslint","prettier",
|
|
124
|
+
// Framework / tool names
|
|
125
|
+
"typescript","javascript","java","kotlin","python","golang","rust","csharp",
|
|
126
|
+
"swift","ruby","spring","boot","javax","jakarta","hibernate","lombok",
|
|
127
|
+
"gradle","maven","webpack","vite","react","angular","vue","svelte","nuxt",
|
|
128
|
+
"express","fastapi","flask","django","rails","fiber","gin","actix","tokio",
|
|
129
|
+
"asyncio","junit","jest","mocha","pytest","rspec","docker","kubernetes",
|
|
130
|
+
"aws","gcp","azure","terraform","ansible","nginx","apache","linux",
|
|
131
|
+
// Next.js / fullstack ecosystem
|
|
132
|
+
"nextjs","remix","astro","sveltekit","gatsby","nuxtjs","qwik","solid",
|
|
133
|
+
"vercel","netlify","cloudflare","supabase","firebase","mongodb","postgres",
|
|
134
|
+
"prisma","drizzle","typeorm","sequelize","mongoose","redis","neon",
|
|
135
|
+
"trpc","graphql","grpc","rest","openapi","swagger","zod","yup","valibot",
|
|
136
|
+
"tailwind","shadcn","radix","headless","chakra","mantine","antd","mui",
|
|
137
|
+
"tanstack","tanstackquery","zustand","jotai","recoil","redux","mobx",
|
|
138
|
+
"storybook","cypress","playwright","vitest","turbopack","turborepo",
|
|
139
|
+
// Next.js file-convention exports / function names
|
|
140
|
+
"getserversideprops","getstaticprops","getstaticpaths","generatestaticparams",
|
|
141
|
+
"generatemetadata","serveraction","revalidate","notfound","redirect",
|
|
142
|
+
"permanentredirect","unstablecache","unstablenostore","cookies","headers",
|
|
143
|
+
"userouter","usepathname","usesearchparams","useparams","useformstate",
|
|
144
|
+
"useformstatus","useoptimistic","useserveraction",
|
|
145
|
+
// JSX / React specific
|
|
146
|
+
"classname","onclick","onchange","onsubmit","onfocus","onblur","onkeydown",
|
|
147
|
+
"onkeyup","onmousedown","onmouseup","defaultvalue","htmlfor","tabindex",
|
|
148
|
+
"strokewidth","viewbox","fillopacity","strokeopacity","pathdata",
|
|
149
|
+
"children","fragment","portal","suspense","errorboundary","strictmode",
|
|
150
|
+
"createelement","createcontext","createref","forwardref",
|
|
151
|
+
"usestate","useeffect","useref","usecontext","usememo","usecallback",
|
|
152
|
+
"usereducer","useid","usetransition","usedeferredvalue","useimperativehandle",
|
|
153
|
+
// Concurrency / runtime
|
|
154
|
+
"mutex","lock","thread","goroutine","coroutine","channel","buffer","stream",
|
|
155
|
+
"pipe","observable","promise","future","completable","deferred","disposable",
|
|
156
|
+
"lifecycle","scope",
|
|
157
|
+
// Meta
|
|
158
|
+
"annotation","attribute","metadata","reflection","generic","template",
|
|
159
|
+
"macro","preprocessor","compiler","runtime","debug","release","production",
|
|
160
|
+
"development","staging","build","deploy","lint","coverage","report",
|
|
161
|
+
// Common short noise
|
|
162
|
+
"the","and","for","are","but","not","you","all","this","that","with",
|
|
163
|
+
"have","from","they","will","been","can","has","was","its","our","their",
|
|
164
|
+
"more","also","when","what","about","which","would","into","than","then",
|
|
165
|
+
"each","just","over","after","such","here","some","were","very","only",
|
|
166
|
+
]);
|
|
167
|
+
|
|
168
|
+
function wcIsStopWord(w) {
|
|
169
|
+
return WC_HUMAN_STOP_WORDS.has(w) || WC_PROG_STOP_WORDS.has(w)
|
|
170
|
+
|| w.length < 3 || /^\d+$/.test(w) || /^[^a-z]/.test(w);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── Identifier splitter ───────────────────────────────────────────────────────
|
|
174
|
+
function splitIdentifier(word) {
|
|
175
|
+
return word
|
|
176
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
177
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
|
|
178
|
+
.replace(/[_\-./]+/g, ' ')
|
|
179
|
+
.toLowerCase()
|
|
180
|
+
.trim()
|
|
181
|
+
.split(/\s+/)
|
|
182
|
+
.filter((w) => w.length >= 3);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Text tokenizer ────────────────────────────────────────────────────────────
|
|
186
|
+
function wcTokenize(text) {
|
|
187
|
+
return text
|
|
188
|
+
.toLowerCase()
|
|
189
|
+
.split(/[^a-zàâäéèêëïîôùûü]+/)
|
|
190
|
+
.filter((w) => w.length >= 3);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── Language-aware word extractor ─────────────────────────────────────────────
|
|
194
|
+
const WC_PROSE_EXTS = new Set(['md', 'txt', 'mdx']);
|
|
195
|
+
const WC_CODE_EXTS = new Set(['ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs', 'java', 'kt', 'py', 'go', 'rs', 'cs', 'swift', 'rb', 'prisma', 'graphql', 'gql']);
|
|
196
|
+
const WC_CONFIG_EXTS = new Set(['html', 'css', 'scss', 'yml', 'yaml', 'json', 'xml', 'toml', 'env']);
|
|
197
|
+
// Extensions where JSX tags should be stripped before identifier extraction
|
|
198
|
+
const WC_JSX_EXTS = new Set(['tsx', 'jsx']);
|
|
199
|
+
|
|
200
|
+
function extractWordsFromFile(text, ext) {
|
|
201
|
+
const words = [];
|
|
202
|
+
|
|
203
|
+
if (WC_PROSE_EXTS.has(ext)) {
|
|
204
|
+
// Prose: strip code blocks, markdown syntax, extract plain text
|
|
205
|
+
const clean = text
|
|
206
|
+
.replace(/^\s*(import|export\s+\{[^}]*\}|export\s+\*|package|require|#include|from\s+['"][^'"]+['"]\s*(import)?)\b.*/gm, '')
|
|
207
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
208
|
+
.replace(/`[^`\n]+`/g, ' ')
|
|
209
|
+
.replace(/!\[[^\]]*\]\([^)]*\)/g, ' ')
|
|
210
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
211
|
+
.replace(/https?:\/\/\S+/g, ' ')
|
|
212
|
+
.replace(/[#*_~>`|!\[\](){}=+\-]/g, ' ');
|
|
213
|
+
words.push(...wcTokenize(clean));
|
|
214
|
+
|
|
215
|
+
} else if (WC_CODE_EXTS.has(ext)) {
|
|
216
|
+
// For JSX/TSX: strip JSX tags and common HTML attribute names before processing
|
|
217
|
+
// so that <div className="..." onClick={...}> doesn't contribute noise words
|
|
218
|
+
if (WC_JSX_EXTS.has(ext)) {
|
|
219
|
+
// Remove JSX opening/closing tags but keep their text content
|
|
220
|
+
text = text.replace(/<\/?[A-Z][A-Za-z0-9.]*[^>]*>/g, ' '); // React components
|
|
221
|
+
text = text.replace(/<\/?(div|span|main|section|article|aside|nav|header|footer|form|input|button|select|option|textarea|table|thead|tbody|tr|td|th|ul|ol|li|a|img|p|h[1-6]|label|figure|figcaption|picture|video|audio|canvas|svg|path|circle|rect|g|defs|use|link|meta|head|html|body|script|style|br|hr|strong|em|code|pre|blockquote|small|sub|sup)\b[^>]*>/gi, ' ');
|
|
222
|
+
// Remove JSX attribute names (lowercase words before =)
|
|
223
|
+
text = text.replace(/\b(className|onClick|onChange|onSubmit|onFocus|onBlur|onKeyDown|onKeyUp|onMouseDown|onMouseUp|htmlFor|tabIndex|strokeWidth|viewBox|fillOpacity|strokeOpacity|defaultValue|placeholder|disabled|checked|readOnly|required|multiple|autoFocus|autoComplete|type|href|src|alt|width|height|style|key|ref)\s*=/g, ' =');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Strip import/package/namespace/using/require lines before any extraction
|
|
227
|
+
text = text.replace(/^\s*(import\s+[\w.*{},\s'"@/-]+;?|from\s+['"][^'"]+['"]\s*;?|package\s+[\w.]+;?|namespace\s+[\w.]+;?|using\s+[\w.]+;?|require\s*\(?\s*['"][^'"]+['"]\s*\)?;?)\s*$/gm, '');
|
|
228
|
+
|
|
229
|
+
// Extract from line comments (// # --)
|
|
230
|
+
text.replace(/(?:\/\/|#(?!.*[{}[\]<>])| -- )\s*(.+)$/gm, (_, c) => {
|
|
231
|
+
words.push(...wcTokenize(c));
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Extract from block comments /* */
|
|
235
|
+
text.replace(/\/\*[\s\S]*?\*\//g, (c) => {
|
|
236
|
+
words.push(...wcTokenize(c));
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Extract from docstrings """ """ and ''' '''
|
|
240
|
+
text.replace(/"""[\s\S]*?"""|'''[\s\S]*?'''/g, (c) => {
|
|
241
|
+
words.push(...wcTokenize(c));
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Extract from string literals: only human-readable (starts with letter, min 4 chars)
|
|
245
|
+
text.replace(/["'`]([A-Za-zÀ-ÿ][A-Za-zÀ-ÿ\s,.'éàèùâêîôûç]{3,})["'`]/g, (_, s) => {
|
|
246
|
+
words.push(...wcTokenize(s));
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Split all identifiers (camelCase, PascalCase, snake_case, etc.)
|
|
250
|
+
text.replace(/\b([A-Za-z][A-Za-z0-9_]{2,})\b/g, (_, id) => {
|
|
251
|
+
words.push(...splitIdentifier(id));
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
} else if (WC_CONFIG_EXTS.has(ext)) {
|
|
255
|
+
// Config: extract string values and split identifiers
|
|
256
|
+
text.replace(/:\s*["']([^"']{3,})["']/g, (_, v) => {
|
|
257
|
+
words.push(...wcTokenize(v));
|
|
258
|
+
});
|
|
259
|
+
text.replace(/\b([A-Za-z][A-Za-z0-9_]{2,})\b/g, (_, id) => {
|
|
260
|
+
words.push(...splitIdentifier(id));
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return words.filter((w) => !wcIsStopWord(w));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── State ─────────────────────────────────────────────────────────────────────
|
|
268
|
+
let _wcWordFreq = null; // Map<word, number> — total occurrence count
|
|
269
|
+
let _wcWordFiles = null; // Map<word, Set<path>> — which files contain the word
|
|
270
|
+
let _wcTotalFiles = 0;
|
|
271
|
+
|
|
272
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
273
|
+
function wcEsc(s) {
|
|
274
|
+
return String(s)
|
|
275
|
+
.replace(/&/g, '&').replace(/</g, '<')
|
|
276
|
+
.replace(/>/g, '>').replace(/"/g, '"');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ── Render ────────────────────────────────────────────────────────────────────
|
|
280
|
+
function wcRender() {
|
|
281
|
+
const minFiles = Math.max(1, parseInt(document.getElementById('wc-min-files').value) || 1);
|
|
282
|
+
|
|
283
|
+
const allWords = [..._wcWordFreq.entries()].sort((a, b) => b[1] - a[1]);
|
|
284
|
+
const list = allWords
|
|
285
|
+
.filter(([w]) => (_wcWordFiles.get(w) || new Set()).size >= minFiles)
|
|
286
|
+
.slice(0, 150);
|
|
287
|
+
|
|
288
|
+
const status = document.getElementById('wc-status');
|
|
289
|
+
const canvas = document.getElementById('wc-canvas');
|
|
290
|
+
|
|
291
|
+
if (!list.length) {
|
|
292
|
+
const total = allWords.length;
|
|
293
|
+
status.textContent = total
|
|
294
|
+
? `${total} domain word(s) found but none appear in ${minFiles}+ file(s). Lower the "Min files" threshold to 1.`
|
|
295
|
+
: 'No domain words extracted. Try adding more file extensions or a broader folder.';
|
|
296
|
+
status.classList.remove('hidden');
|
|
297
|
+
canvas.classList.add('hidden');
|
|
298
|
+
document.getElementById('wc-sidebar').classList.add('hidden');
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Stats
|
|
303
|
+
const uniqueDomain = [..._wcWordFreq.keys()]
|
|
304
|
+
.filter((w) => (_wcWordFiles.get(w) || new Set()).size >= minFiles).length;
|
|
305
|
+
document.getElementById('wc-stats').innerHTML =
|
|
306
|
+
`<div>${_wcTotalFiles} file(s) scanned</div>` +
|
|
307
|
+
`<div>${uniqueDomain} unique domain words</div>` +
|
|
308
|
+
`<div>${list.length} shown</div>`;
|
|
309
|
+
|
|
310
|
+
// Top-50 sidebar list
|
|
311
|
+
const topList = document.getElementById('wc-top-list');
|
|
312
|
+
const top50 = list.slice(0, 50);
|
|
313
|
+
const maxF = top50[0][1];
|
|
314
|
+
topList.innerHTML = top50.map(([w, n], i) => {
|
|
315
|
+
const pct = Math.round((n / maxF) * 100);
|
|
316
|
+
return `<div onclick="wcShowDetail('${wcEsc(w)}')"
|
|
317
|
+
class="flex items-center gap-2 px-3 py-1 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors">
|
|
318
|
+
<span class="text-gray-400 dark:text-gray-600 w-5 text-right shrink-0 tabular-nums text-xs">${i + 1}</span>
|
|
319
|
+
<div class="flex-1 min-w-0">
|
|
320
|
+
<div class="flex items-center justify-between gap-1">
|
|
321
|
+
<span class="text-gray-800 dark:text-gray-200 font-medium truncate text-xs">${wcEsc(w)}</span>
|
|
322
|
+
<span class="text-gray-400 dark:text-gray-600 tabular-nums shrink-0 text-xs">${n}</span>
|
|
323
|
+
</div>
|
|
324
|
+
<div class="h-1 bg-gray-100 dark:bg-gray-800 rounded-full mt-0.5">
|
|
325
|
+
<div class="h-1 bg-blue-400 dark:bg-blue-600 rounded-full" style="width:${pct}%"></div>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
</div>`;
|
|
329
|
+
}).join('');
|
|
330
|
+
|
|
331
|
+
// Show sidebar first so the layout settles before we measure the canvas area
|
|
332
|
+
document.getElementById('wc-sidebar').classList.remove('hidden');
|
|
333
|
+
status.classList.add('hidden');
|
|
334
|
+
|
|
335
|
+
const isDark = document.documentElement.classList.contains('dark');
|
|
336
|
+
const colors = isDark
|
|
337
|
+
? ['#60a5fa', '#34d399', '#f9a8d4', '#a78bfa', '#fbbf24', '#6ee7b7', '#93c5fd', '#fb923c', '#4ade80', '#f472b6']
|
|
338
|
+
: ['#1d4ed8', '#047857', '#7c3aed', '#b45309', '#be123c', '#0369a1', '#4338ca', '#c2410c', '#15803d', '#9333ea'];
|
|
339
|
+
|
|
340
|
+
// rAF: wait for the browser to reflow after showing the sidebar,
|
|
341
|
+
// then measure the actual canvas container size before rendering.
|
|
342
|
+
requestAnimationFrame(() => {
|
|
343
|
+
const wrap = document.getElementById('wc-canvas-wrap');
|
|
344
|
+
canvas.width = Math.max(400, wrap.offsetWidth);
|
|
345
|
+
canvas.height = Math.max(300, wrap.offsetHeight);
|
|
346
|
+
canvas.style.width = canvas.width + 'px';
|
|
347
|
+
canvas.style.height = canvas.height + 'px';
|
|
348
|
+
canvas.classList.remove('hidden');
|
|
349
|
+
|
|
350
|
+
// Scale font sizes to the canvas area so words fill the available space.
|
|
351
|
+
const fontScale = Math.sqrt((canvas.width * canvas.height) / (800 * 500));
|
|
352
|
+
const maxFont = Math.min(220, Math.round(72 * fontScale));
|
|
353
|
+
const wordList = top50.map(([w, n]) => [w, Math.max(12, Math.round((maxFont * n) / maxF))]);
|
|
354
|
+
|
|
355
|
+
WordCloud(canvas, {
|
|
356
|
+
list: wordList,
|
|
357
|
+
gridSize: Math.round((4 * canvas.width) / 1024),
|
|
358
|
+
fontFamily: 'ui-sans-serif, system-ui, sans-serif',
|
|
359
|
+
color: () => colors[Math.floor(Math.random() * colors.length)],
|
|
360
|
+
backgroundColor: isDark ? '#030712' : '#ffffff',
|
|
361
|
+
rotateRatio: 0.2,
|
|
362
|
+
minSize: 6,
|
|
363
|
+
shrinkToFit: true,
|
|
364
|
+
click: (item) => wcShowDetail(item[0]),
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function wcShowDetail(word) {
|
|
370
|
+
const files = [...(_wcWordFiles.get(word) || [])].sort();
|
|
371
|
+
const freq = _wcWordFreq.get(word) || 0;
|
|
372
|
+
document.getElementById('wc-detail-word').textContent = `${word} ×${freq}`;
|
|
373
|
+
document.getElementById('wc-detail-files').innerHTML = files.length
|
|
374
|
+
? files.map((f) => `
|
|
375
|
+
<div class="flex items-center gap-1 group py-0.5">
|
|
376
|
+
<span class="text-gray-600 dark:text-gray-400 break-all font-mono flex-1 text-xs">${wcEsc(f)}</span>
|
|
377
|
+
<button onclick="wcAddExclude('${wcEsc(f)}')" title="Exclude this file"
|
|
378
|
+
class="shrink-0 opacity-0 group-hover:opacity-100 text-red-400 hover:text-red-600 text-xs leading-none px-1 transition-opacity">⊖</button>
|
|
379
|
+
</div>`).join('')
|
|
380
|
+
: '<div class="text-gray-400 text-center py-2">—</div>';
|
|
381
|
+
document.getElementById('wc-detail').classList.remove('hidden');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function wcCloseDetail() {
|
|
385
|
+
document.getElementById('wc-detail').classList.add('hidden');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function wcGetExcludeSet() {
|
|
389
|
+
const ta = document.getElementById('wc-exclude');
|
|
390
|
+
return new Set(
|
|
391
|
+
(ta.value || '').split(/[\n,]/).map((s) => s.trim()).filter(Boolean)
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function wcAddExclude(item) {
|
|
396
|
+
const existing = wcGetExcludeSet();
|
|
397
|
+
if (existing.has(item)) return;
|
|
398
|
+
existing.add(item);
|
|
399
|
+
const ta = document.getElementById('wc-exclude');
|
|
400
|
+
ta.value = [...existing].join(', ');
|
|
401
|
+
localStorage.setItem('wc-exclude', ta.value);
|
|
402
|
+
// Refresh exclude browser and detail panel if open
|
|
403
|
+
if (!document.getElementById('wc-exclude-browser').classList.contains('hidden')) {
|
|
404
|
+
wcExclLoadBrowse(_wcExclBrowseCurrent);
|
|
405
|
+
}
|
|
406
|
+
wcRefreshDetailPanel();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function wcOnExcludeChange() {
|
|
410
|
+
const ta = document.getElementById('wc-exclude');
|
|
411
|
+
localStorage.setItem('wc-exclude', ta.value);
|
|
412
|
+
if (!document.getElementById('wc-exclude-browser').classList.contains('hidden')) {
|
|
413
|
+
wcExclLoadBrowse(_wcExclBrowseCurrent);
|
|
414
|
+
}
|
|
415
|
+
wcRefreshDetailPanel();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function wcApplyFilter() {
|
|
419
|
+
if (_wcWordFreq) wcRender();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ── Exclude browser ───────────────────────────────────────────────────────────
|
|
423
|
+
|
|
424
|
+
let _wcExclBrowseParent = null;
|
|
425
|
+
let _wcExclBrowseCurrent = '';
|
|
426
|
+
|
|
427
|
+
function wcToggleExcludeBrowser() {
|
|
428
|
+
const browser = document.getElementById('wc-exclude-browser');
|
|
429
|
+
const isHidden = browser.classList.toggle('hidden');
|
|
430
|
+
if (!isHidden) {
|
|
431
|
+
const start = _wcExclBrowseCurrent || document.getElementById('wc-root').value || '/';
|
|
432
|
+
wcExclLoadBrowse(start);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function wcExclLoadBrowse(dirPath) {
|
|
437
|
+
const list = document.getElementById('wc-excl-browse-list');
|
|
438
|
+
list.innerHTML = '<p class="px-3 py-4 text-xs text-gray-400 text-center">Loading…</p>';
|
|
439
|
+
try {
|
|
440
|
+
const data = await fetch('/api/browse?all=1&path=' + encodeURIComponent(dirPath)).then((r) => r.json());
|
|
441
|
+
_wcExclBrowseCurrent = data.current;
|
|
442
|
+
_wcExclBrowseParent = data.parent;
|
|
443
|
+
document.getElementById('wc-excl-browse-path').textContent = data.current;
|
|
444
|
+
document.getElementById('wc-excl-browse-up').disabled = !data.parent;
|
|
445
|
+
|
|
446
|
+
const root = document.getElementById('wc-root').value.trim();
|
|
447
|
+
|
|
448
|
+
// Helper: compute the entry to add — relative to root if possible, else basename
|
|
449
|
+
function exclusionEntry(absPath, name) {
|
|
450
|
+
if (root && absPath.startsWith(root + '/')) {
|
|
451
|
+
return absPath.slice(root.length + 1); // relative path
|
|
452
|
+
}
|
|
453
|
+
return name;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const excluded = wcGetExcludeSet();
|
|
457
|
+
|
|
458
|
+
const rows = data.dirs
|
|
459
|
+
.filter((dir) => !excluded.has(exclusionEntry(dir.path, dir.name)))
|
|
460
|
+
.map((dir) => {
|
|
461
|
+
const entry = exclusionEntry(dir.path, dir.name);
|
|
462
|
+
return `<div class="group flex items-center hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
|
|
463
|
+
<button data-path="${wcEsc(dir.path)}" onclick="wcExclLoadBrowse(this.dataset.path)"
|
|
464
|
+
class="flex-1 flex items-center gap-2 px-3 py-2 text-sm text-left">
|
|
465
|
+
<span class="text-gray-400 shrink-0">📁</span>
|
|
466
|
+
<span class="text-gray-700 dark:text-gray-300 truncate">${wcEsc(dir.name)}</span>
|
|
467
|
+
</button>
|
|
468
|
+
<button onclick="wcAddExclude('${wcEsc(entry)}')" title="Add to exclusions"
|
|
469
|
+
class="shrink-0 text-red-400 hover:text-red-600 px-3 py-2 text-sm font-bold transition-colors">⊖</button>
|
|
470
|
+
</div>`;
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// Also show files in the current dir that match scanned extensions
|
|
474
|
+
const exts = [...document.querySelectorAll('.wc-ext:checked')].map((cb) => cb.value);
|
|
475
|
+
const fileRows = (data.files || [])
|
|
476
|
+
.filter((f) => exts.some((e) => f.name.endsWith('.' + e)))
|
|
477
|
+
.filter((f) => !excluded.has(exclusionEntry(f.path, f.name)))
|
|
478
|
+
.map((f) => {
|
|
479
|
+
const entry = exclusionEntry(f.path, f.name);
|
|
480
|
+
return `<div class="group flex items-center hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
|
|
481
|
+
<span class="flex-1 flex items-center gap-2 px-3 py-2 text-sm text-gray-500 dark:text-gray-400">
|
|
482
|
+
<span class="shrink-0 opacity-40">📄</span>
|
|
483
|
+
<span class="truncate font-mono">${wcEsc(f.name)}</span>
|
|
484
|
+
</span>
|
|
485
|
+
<button onclick="wcAddExclude('${wcEsc(entry)}')" title="Add to exclusions"
|
|
486
|
+
class="shrink-0 text-red-400 hover:text-red-600 px-3 py-2 text-sm font-bold transition-colors">⊖</button>
|
|
487
|
+
</div>`;
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
const allRows = [...rows, ...fileRows];
|
|
491
|
+
list.innerHTML = allRows.length
|
|
492
|
+
? allRows.join('')
|
|
493
|
+
: '<p class="px-3 py-3 text-xs text-gray-400 text-center">Empty directory</p>';
|
|
494
|
+
} catch {
|
|
495
|
+
list.innerHTML = '<p class="px-3 py-4 text-xs text-red-400 text-center">Cannot read directory</p>';
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function wcExclBrowseUp() {
|
|
500
|
+
if (_wcExclBrowseParent) wcExclLoadBrowse(_wcExclBrowseParent);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// ── Browser (folder picker) ───────────────────────────────────────────────────
|
|
504
|
+
|
|
505
|
+
let _wcBrowseParent = null;
|
|
506
|
+
let _wcBrowseCurrent = '';
|
|
507
|
+
|
|
508
|
+
function wcToggleBrowser() {
|
|
509
|
+
const browser = document.getElementById('wc-browser');
|
|
510
|
+
const isHidden = browser.classList.toggle('hidden');
|
|
511
|
+
if (!isHidden) wcLoadBrowse(_wcBrowseCurrent || document.getElementById('wc-root').value || '/');
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async function wcLoadBrowse(dirPath) {
|
|
515
|
+
const list = document.getElementById('wc-browse-list');
|
|
516
|
+
list.innerHTML = '<p class="px-3 py-4 text-xs text-gray-400 text-center">Loading…</p>';
|
|
517
|
+
try {
|
|
518
|
+
const data = await fetch('/api/browse?path=' + encodeURIComponent(dirPath)).then((r) => r.json());
|
|
519
|
+
_wcBrowseCurrent = data.current;
|
|
520
|
+
_wcBrowseParent = data.parent;
|
|
521
|
+
document.getElementById('wc-browse-path').textContent = data.current;
|
|
522
|
+
document.getElementById('wc-browse-up').disabled = !data.parent;
|
|
523
|
+
|
|
524
|
+
const rows = data.dirs.map((dir) =>
|
|
525
|
+
`<div class="group flex items-center hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
|
|
526
|
+
<button data-path="${wcEsc(dir.path)}" onclick="wcLoadBrowse(this.dataset.path)"
|
|
527
|
+
class="flex-1 flex items-center gap-2 px-3 py-2 text-sm text-left">
|
|
528
|
+
<span class="text-gray-400 shrink-0">📁</span>
|
|
529
|
+
<span class="text-gray-700 dark:text-gray-300 truncate">${wcEsc(dir.name)}</span>
|
|
530
|
+
</button>
|
|
531
|
+
<button onclick="wcAddExclude('${wcEsc(dir.name)}')" title="Exclude this folder"
|
|
532
|
+
class="shrink-0 opacity-0 group-hover:opacity-100 text-red-400 hover:text-red-600 px-3 py-2 text-sm transition-opacity">⊖</button>
|
|
533
|
+
</div>`,
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
const selectBtn = `<button onclick="wcSelectFolder()" class="w-full px-3 py-2 text-xs text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-950/30 text-left font-medium border-t border-gray-100 dark:border-gray-800">
|
|
537
|
+
✓ Select: <span class="font-mono">${wcEsc(data.current)}</span>
|
|
538
|
+
</button>`;
|
|
539
|
+
|
|
540
|
+
list.innerHTML = (rows.length
|
|
541
|
+
? rows.join('')
|
|
542
|
+
: '<p class="px-3 py-3 text-xs text-gray-400 text-center">No sub-folders</p>'
|
|
543
|
+
) + selectBtn;
|
|
544
|
+
} catch {
|
|
545
|
+
list.innerHTML = '<p class="px-3 py-4 text-xs text-red-400 text-center">Cannot read directory</p>';
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function wcBrowseUp() {
|
|
550
|
+
if (_wcBrowseParent) wcLoadBrowse(_wcBrowseParent);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function wcSelectFolder() {
|
|
554
|
+
document.getElementById('wc-root').value = _wcBrowseCurrent;
|
|
555
|
+
localStorage.setItem('wc-root', _wcBrowseCurrent);
|
|
556
|
+
document.getElementById('wc-browser').classList.add('hidden');
|
|
557
|
+
|
|
558
|
+
// Reset exclude field and sync exclude browser path to new root
|
|
559
|
+
const ta = document.getElementById('wc-exclude');
|
|
560
|
+
ta.value = '';
|
|
561
|
+
localStorage.removeItem('wc-exclude');
|
|
562
|
+
_wcExclBrowseCurrent = _wcBrowseCurrent;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// ── Persistence (localStorage) ────────────────────────────────────────────────
|
|
566
|
+
|
|
567
|
+
function wcToggleAllExts() {
|
|
568
|
+
const boxes = [...document.querySelectorAll('.wc-ext')];
|
|
569
|
+
const allChecked = boxes.every((cb) => cb.checked);
|
|
570
|
+
boxes.forEach((cb) => { cb.checked = !allChecked; });
|
|
571
|
+
document.getElementById('wcToggleAllBtn').textContent = allChecked ? 'All' : 'None';
|
|
572
|
+
wcSaveExts();
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function wcSaveExts() {
|
|
576
|
+
const exts = [...document.querySelectorAll('.wc-ext:checked')].map((cb) => cb.value);
|
|
577
|
+
localStorage.setItem('wc-exts', JSON.stringify(exts));
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function wcRestorePrefs() {
|
|
581
|
+
const savedRoot = localStorage.getItem('wc-root');
|
|
582
|
+
if (savedRoot) {
|
|
583
|
+
document.getElementById('wc-root').value = savedRoot;
|
|
584
|
+
_wcBrowseCurrent = savedRoot;
|
|
585
|
+
}
|
|
586
|
+
const savedExclude = localStorage.getItem('wc-exclude');
|
|
587
|
+
if (savedExclude) document.getElementById('wc-exclude').value = savedExclude;
|
|
588
|
+
const savedExts = localStorage.getItem('wc-exts');
|
|
589
|
+
if (savedExts) {
|
|
590
|
+
try {
|
|
591
|
+
const exts = JSON.parse(savedExts);
|
|
592
|
+
document.querySelectorAll('.wc-ext').forEach((cb) => {
|
|
593
|
+
cb.checked = exts.includes(cb.value);
|
|
594
|
+
});
|
|
595
|
+
} catch { /* ignore corrupt data */ }
|
|
596
|
+
}
|
|
597
|
+
document.querySelectorAll('.wc-ext').forEach((cb) => {
|
|
598
|
+
cb.addEventListener('change', wcSaveExts);
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// ── Open / Launch / Close ─────────────────────────────────────────────────────
|
|
603
|
+
|
|
604
|
+
async function openWordCloud() {
|
|
605
|
+
const overlay = document.getElementById('wc-overlay');
|
|
606
|
+
const status = document.getElementById('wc-status');
|
|
607
|
+
const canvas = document.getElementById('wc-canvas');
|
|
608
|
+
overlay.classList.remove('hidden');
|
|
609
|
+
status.textContent = 'Choose a root folder and click Launch.';
|
|
610
|
+
status.classList.remove('hidden');
|
|
611
|
+
canvas.classList.add('hidden');
|
|
612
|
+
|
|
613
|
+
const rootInput = document.getElementById('wc-root');
|
|
614
|
+
if (!rootInput.value) {
|
|
615
|
+
try {
|
|
616
|
+
const cfg = await fetch('/api/config').then((r) => r.json());
|
|
617
|
+
if (cfg.docsFolder) {
|
|
618
|
+
rootInput.value = cfg.docsFolder;
|
|
619
|
+
_wcBrowseCurrent = cfg.docsFolder;
|
|
620
|
+
}
|
|
621
|
+
} catch { /* ignore */ }
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
async function launchWordCloud() {
|
|
626
|
+
const status = document.getElementById('wc-status');
|
|
627
|
+
const canvas = document.getElementById('wc-canvas');
|
|
628
|
+
const root = document.getElementById('wc-root').value.trim();
|
|
629
|
+
|
|
630
|
+
if (!root) {
|
|
631
|
+
status.textContent = 'Please select a root folder first.';
|
|
632
|
+
status.classList.remove('hidden');
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const exts = [...document.querySelectorAll('.wc-ext:checked')].map((cb) => cb.value);
|
|
637
|
+
if (!exts.length) {
|
|
638
|
+
status.textContent = 'Please select at least one extension.';
|
|
639
|
+
status.classList.remove('hidden');
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
document.getElementById('wc-browser').classList.add('hidden');
|
|
644
|
+
document.getElementById('wc-sidebar').classList.add('hidden');
|
|
645
|
+
document.getElementById('wc-detail').classList.add('hidden');
|
|
646
|
+
status.textContent = 'Reading files…';
|
|
647
|
+
status.classList.remove('hidden');
|
|
648
|
+
canvas.classList.add('hidden');
|
|
649
|
+
|
|
650
|
+
try {
|
|
651
|
+
const excludeRaw = document.getElementById('wc-exclude').value.trim();
|
|
652
|
+
const excludeDirs = excludeRaw ? excludeRaw.split(/[\n,]/).map((s) => s.trim()).filter(Boolean) : [];
|
|
653
|
+
localStorage.setItem('wc-exclude', excludeRaw);
|
|
654
|
+
|
|
655
|
+
const params = new URLSearchParams({ path: root });
|
|
656
|
+
exts.forEach((e) => params.append('ext', e));
|
|
657
|
+
excludeDirs.forEach((d) => params.append('exclude', d));
|
|
658
|
+
const res = await fetch('/api/wordcloud?' + params);
|
|
659
|
+
if (!res.ok) {
|
|
660
|
+
const err = await res.json();
|
|
661
|
+
status.textContent = 'Error: ' + (err.error || res.statusText);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
const { files, fileTexts } = await res.json();
|
|
665
|
+
status.textContent = `Analyzing ${files} file(s)…`;
|
|
666
|
+
|
|
667
|
+
_wcWordFreq = new Map();
|
|
668
|
+
_wcWordFiles = new Map();
|
|
669
|
+
_wcTotalFiles = files;
|
|
670
|
+
|
|
671
|
+
for (const { path: filePath, text } of fileTexts) {
|
|
672
|
+
const ext = (filePath.split('.').pop() || '').toLowerCase();
|
|
673
|
+
const words = extractWordsFromFile(text, ext);
|
|
674
|
+
const seenInFile = new Set();
|
|
675
|
+
for (const w of words) {
|
|
676
|
+
_wcWordFreq.set(w, (_wcWordFreq.get(w) || 0) + 1);
|
|
677
|
+
if (!seenInFile.has(w)) {
|
|
678
|
+
seenInFile.add(w);
|
|
679
|
+
if (!_wcWordFiles.has(w)) _wcWordFiles.set(w, new Set());
|
|
680
|
+
_wcWordFiles.get(w).add(filePath);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
wcRender();
|
|
686
|
+
} catch (err) {
|
|
687
|
+
status.textContent = 'Error: ' + err.message;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function closeWordCloud() {
|
|
692
|
+
document.getElementById('wc-overlay').classList.add('hidden');
|
|
693
|
+
}
|