living-documentation 4.1.0 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of living-documentation might be problematic. Click here for more details.
- package/README.md +21 -10
- package/dist/src/frontend/diagram/image-name-modal.js +48 -0
- package/dist/src/frontend/diagram/image-upload.js +8 -6
- package/dist/src/frontend/diagram/main.js +31 -17
- package/dist/src/frontend/diagram/network.js +216 -27
- package/dist/src/frontend/diagram/node-rendering.js +440 -167
- package/dist/src/frontend/diagram/persistence.js +4 -2
- package/dist/src/frontend/diagram/selection-overlay.js +28 -6
- package/dist/src/frontend/diagram/state.js +1 -0
- package/dist/src/frontend/diagram.html +43 -0
- package/dist/src/frontend/index.html +261 -16
- package/dist/src/frontend/wordcloud.js +572 -209
- package/dist/src/routes/browse.d.ts.map +1 -1
- package/dist/src/routes/browse.js +2 -1
- package/dist/src/routes/browse.js.map +1 -1
- package/dist/src/routes/diagrams.d.ts.map +1 -1
- package/dist/src/routes/diagrams.js +2 -1
- package/dist/src/routes/diagrams.js.map +1 -1
- package/dist/src/routes/images.d.ts.map +1 -1
- package/dist/src/routes/images.js +14 -7
- package/dist/src/routes/images.js.map +1 -1
- package/dist/src/routes/wordcloud.d.ts.map +1 -1
- package/dist/src/routes/wordcloud.js +42 -20
- package/dist/src/routes/wordcloud.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,189 +1,533 @@
|
|
|
1
|
-
// ── Word Cloud —
|
|
1
|
+
// ── Word Cloud — domain language analyzer ────────────────────────────────────
|
|
2
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
|
+
]);
|
|
3
49
|
|
|
4
|
-
// ──
|
|
5
|
-
const
|
|
6
|
-
//
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
//
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
|
|
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",
|
|
47
166
|
]);
|
|
48
167
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
+
// Extract from line comments (// # --)
|
|
227
|
+
text.replace(/(?:\/\/|#(?!.*[{}[\]<>])| -- )\s*(.+)$/gm, (_, c) => {
|
|
228
|
+
words.push(...wcTokenize(c));
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Extract from block comments /* */
|
|
232
|
+
text.replace(/\/\*[\s\S]*?\*\//g, (c) => {
|
|
233
|
+
words.push(...wcTokenize(c));
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Extract from docstrings """ """ and ''' '''
|
|
237
|
+
text.replace(/"""[\s\S]*?"""|'''[\s\S]*?'''/g, (c) => {
|
|
238
|
+
words.push(...wcTokenize(c));
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Extract from string literals: only human-readable (starts with letter, min 4 chars)
|
|
242
|
+
text.replace(/["'`]([A-Za-zÀ-ÿ][A-Za-zÀ-ÿ\s,.'éàèùâêîôûç]{3,})["'`]/g, (_, s) => {
|
|
243
|
+
words.push(...wcTokenize(s));
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Split all identifiers (camelCase, PascalCase, snake_case, etc.)
|
|
247
|
+
text.replace(/\b([A-Za-z][A-Za-z0-9_]{2,})\b/g, (_, id) => {
|
|
248
|
+
words.push(...splitIdentifier(id));
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
} else if (WC_CONFIG_EXTS.has(ext)) {
|
|
252
|
+
// Config: extract string values and split identifiers
|
|
253
|
+
text.replace(/:\s*["']([^"']{3,})["']/g, (_, v) => {
|
|
254
|
+
words.push(...wcTokenize(v));
|
|
255
|
+
});
|
|
256
|
+
text.replace(/\b([A-Za-z][A-Za-z0-9_]{2,})\b/g, (_, id) => {
|
|
257
|
+
words.push(...splitIdentifier(id));
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return words.filter((w) => !wcIsStopWord(w));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ── State ─────────────────────────────────────────────────────────────────────
|
|
265
|
+
let _wcWordFreq = null; // Map<word, number> — total occurrence count
|
|
266
|
+
let _wcWordFiles = null; // Map<word, Set<path>> — which files contain the word
|
|
267
|
+
let _wcTotalFiles = 0;
|
|
93
268
|
|
|
94
269
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
270
|
+
function wcEsc(s) {
|
|
271
|
+
return String(s)
|
|
272
|
+
.replace(/&/g, '&').replace(/</g, '<')
|
|
273
|
+
.replace(/>/g, '>').replace(/"/g, '"');
|
|
274
|
+
}
|
|
95
275
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
276
|
+
// ── Render ────────────────────────────────────────────────────────────────────
|
|
277
|
+
function wcRender() {
|
|
278
|
+
const minFiles = Math.max(1, parseInt(document.getElementById('wc-min-files').value) || 1);
|
|
279
|
+
|
|
280
|
+
const allWords = [..._wcWordFreq.entries()].sort((a, b) => b[1] - a[1]);
|
|
281
|
+
const list = allWords
|
|
282
|
+
.filter(([w]) => (_wcWordFiles.get(w) || new Set()).size >= minFiles)
|
|
283
|
+
.slice(0, 150);
|
|
284
|
+
|
|
285
|
+
const status = document.getElementById('wc-status');
|
|
286
|
+
const canvas = document.getElementById('wc-canvas');
|
|
287
|
+
|
|
288
|
+
if (!list.length) {
|
|
289
|
+
const total = allWords.length;
|
|
290
|
+
status.textContent = total
|
|
291
|
+
? `${total} domain word(s) found but none appear in ${minFiles}+ file(s). Lower the "Min files" threshold to 1.`
|
|
292
|
+
: 'No domain words extracted. Try adding more file extensions or a broader folder.';
|
|
293
|
+
status.classList.remove('hidden');
|
|
294
|
+
canvas.classList.add('hidden');
|
|
295
|
+
document.getElementById('wc-sidebar').classList.add('hidden');
|
|
296
|
+
return;
|
|
101
297
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
298
|
+
|
|
299
|
+
// Stats
|
|
300
|
+
const uniqueDomain = [..._wcWordFreq.keys()]
|
|
301
|
+
.filter((w) => (_wcWordFiles.get(w) || new Set()).size >= minFiles).length;
|
|
302
|
+
document.getElementById('wc-stats').innerHTML =
|
|
303
|
+
`<div>${_wcTotalFiles} file(s) scanned</div>` +
|
|
304
|
+
`<div>${uniqueDomain} unique domain words</div>` +
|
|
305
|
+
`<div>${list.length} shown</div>`;
|
|
306
|
+
|
|
307
|
+
// Top-50 sidebar list
|
|
308
|
+
const topList = document.getElementById('wc-top-list');
|
|
309
|
+
const top50 = list.slice(0, 50);
|
|
310
|
+
const maxF = top50[0][1];
|
|
311
|
+
topList.innerHTML = top50.map(([w, n], i) => {
|
|
312
|
+
const pct = Math.round((n / maxF) * 100);
|
|
313
|
+
return `<div onclick="wcShowDetail('${wcEsc(w)}')"
|
|
314
|
+
class="flex items-center gap-2 px-3 py-1 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors">
|
|
315
|
+
<span class="text-gray-400 dark:text-gray-600 w-5 text-right shrink-0 tabular-nums text-xs">${i + 1}</span>
|
|
316
|
+
<div class="flex-1 min-w-0">
|
|
317
|
+
<div class="flex items-center justify-between gap-1">
|
|
318
|
+
<span class="text-gray-800 dark:text-gray-200 font-medium truncate text-xs">${wcEsc(w)}</span>
|
|
319
|
+
<span class="text-gray-400 dark:text-gray-600 tabular-nums shrink-0 text-xs">${n}</span>
|
|
320
|
+
</div>
|
|
321
|
+
<div class="h-1 bg-gray-100 dark:bg-gray-800 rounded-full mt-0.5">
|
|
322
|
+
<div class="h-1 bg-blue-400 dark:bg-blue-600 rounded-full" style="width:${pct}%"></div>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
</div>`;
|
|
326
|
+
}).join('');
|
|
327
|
+
|
|
328
|
+
// Show sidebar first so the layout settles before we measure the canvas area
|
|
329
|
+
document.getElementById('wc-sidebar').classList.remove('hidden');
|
|
330
|
+
status.classList.add('hidden');
|
|
331
|
+
|
|
332
|
+
const isDark = document.documentElement.classList.contains('dark');
|
|
333
|
+
const colors = isDark
|
|
334
|
+
? ['#60a5fa', '#34d399', '#f9a8d4', '#a78bfa', '#fbbf24', '#6ee7b7', '#93c5fd', '#fb923c', '#4ade80', '#f472b6']
|
|
335
|
+
: ['#1d4ed8', '#047857', '#7c3aed', '#b45309', '#be123c', '#0369a1', '#4338ca', '#c2410c', '#15803d', '#9333ea'];
|
|
336
|
+
|
|
337
|
+
// rAF: wait for the browser to reflow after showing the sidebar,
|
|
338
|
+
// then measure the actual canvas container size before rendering.
|
|
339
|
+
requestAnimationFrame(() => {
|
|
340
|
+
const wrap = document.getElementById('wc-canvas-wrap');
|
|
341
|
+
canvas.width = Math.max(400, wrap.offsetWidth);
|
|
342
|
+
canvas.height = Math.max(300, wrap.offsetHeight);
|
|
343
|
+
canvas.style.width = canvas.width + 'px';
|
|
344
|
+
canvas.style.height = canvas.height + 'px';
|
|
345
|
+
canvas.classList.remove('hidden');
|
|
346
|
+
|
|
347
|
+
// Scale font sizes to the canvas area so words fill the available space.
|
|
348
|
+
const fontScale = Math.sqrt((canvas.width * canvas.height) / (800 * 500));
|
|
349
|
+
const maxFont = Math.min(220, Math.round(72 * fontScale));
|
|
350
|
+
const wordList = top50.map(([w, n]) => [w, Math.max(12, Math.round((maxFont * n) / maxF))]);
|
|
351
|
+
|
|
352
|
+
WordCloud(canvas, {
|
|
353
|
+
list: wordList,
|
|
354
|
+
gridSize: Math.round((4 * canvas.width) / 1024),
|
|
355
|
+
fontFamily: 'ui-sans-serif, system-ui, sans-serif',
|
|
356
|
+
color: () => colors[Math.floor(Math.random() * colors.length)],
|
|
357
|
+
backgroundColor: isDark ? '#030712' : '#ffffff',
|
|
358
|
+
rotateRatio: 0.2,
|
|
359
|
+
minSize: 6,
|
|
360
|
+
shrinkToFit: true,
|
|
361
|
+
click: (item) => wcShowDetail(item[0]),
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function wcShowDetail(word) {
|
|
367
|
+
const files = [...(_wcWordFiles.get(word) || [])].sort();
|
|
368
|
+
const freq = _wcWordFreq.get(word) || 0;
|
|
369
|
+
document.getElementById('wc-detail-word').textContent = `${word} ×${freq}`;
|
|
370
|
+
document.getElementById('wc-detail-files').innerHTML = files.length
|
|
371
|
+
? files.map((f) => `
|
|
372
|
+
<div class="flex items-center gap-1 group py-0.5">
|
|
373
|
+
<span class="text-gray-600 dark:text-gray-400 break-all font-mono flex-1 text-xs">${wcEsc(f)}</span>
|
|
374
|
+
<button onclick="wcAddExclude('${wcEsc(f)}')" title="Exclude this file"
|
|
375
|
+
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>
|
|
376
|
+
</div>`).join('')
|
|
377
|
+
: '<div class="text-gray-400 text-center py-2">—</div>';
|
|
378
|
+
document.getElementById('wc-detail').classList.remove('hidden');
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function wcCloseDetail() {
|
|
382
|
+
document.getElementById('wc-detail').classList.add('hidden');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function wcGetExcludeSet() {
|
|
386
|
+
const ta = document.getElementById('wc-exclude');
|
|
387
|
+
return new Set(
|
|
388
|
+
(ta.value || '').split(/[\n,]/).map((s) => s.trim()).filter(Boolean)
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function wcAddExclude(item) {
|
|
393
|
+
const existing = wcGetExcludeSet();
|
|
394
|
+
if (existing.has(item)) return;
|
|
395
|
+
existing.add(item);
|
|
396
|
+
const ta = document.getElementById('wc-exclude');
|
|
397
|
+
ta.value = [...existing].join(', ');
|
|
398
|
+
localStorage.setItem('wc-exclude', ta.value);
|
|
399
|
+
// Refresh exclude browser and detail panel if open
|
|
400
|
+
if (!document.getElementById('wc-exclude-browser').classList.contains('hidden')) {
|
|
401
|
+
wcExclLoadBrowse(_wcExclBrowseCurrent);
|
|
105
402
|
}
|
|
106
|
-
|
|
403
|
+
wcRefreshDetailPanel();
|
|
107
404
|
}
|
|
108
405
|
|
|
109
|
-
function
|
|
110
|
-
|
|
406
|
+
function wcOnExcludeChange() {
|
|
407
|
+
const ta = document.getElementById('wc-exclude');
|
|
408
|
+
localStorage.setItem('wc-exclude', ta.value);
|
|
409
|
+
if (!document.getElementById('wc-exclude-browser').classList.contains('hidden')) {
|
|
410
|
+
wcExclLoadBrowse(_wcExclBrowseCurrent);
|
|
411
|
+
}
|
|
412
|
+
wcRefreshDetailPanel();
|
|
111
413
|
}
|
|
112
414
|
|
|
113
|
-
function
|
|
114
|
-
|
|
115
|
-
// Strip import/package/require/include/using/namespace declarations
|
|
116
|
-
.replace(/^\s*(import|export\s+\{[^}]*\}|export\s+\*|package|require|#include|#import|using|namespace|from\s+['"][^'"]+['"]\s*(import)?|@[A-Za-z]+)\b.*/gm, "")
|
|
117
|
-
// Strip markdown code blocks
|
|
118
|
-
.replace(/```[\s\S]*?```/g, "")
|
|
119
|
-
.replace(/`[^`\n]+`/g, "")
|
|
120
|
-
// Strip markdown links (keep label)
|
|
121
|
-
.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
|
|
122
|
-
// Strip URLs
|
|
123
|
-
.replace(/https?:\/\/\S+/g, "")
|
|
124
|
-
// Strip punctuation / operators
|
|
125
|
-
.replace(/[#*_~>`|!\[\](){}=\-+]/g, " ")
|
|
126
|
-
.toLowerCase()
|
|
127
|
-
.split(/[^a-zàâäéèêëïîôùûü']+/)
|
|
128
|
-
.map((w) => w.replace(/^'+|'+$/g, ""))
|
|
129
|
-
.filter((w) => w.length > 3 && !stopWords.has(w));
|
|
415
|
+
function wcApplyFilter() {
|
|
416
|
+
if (_wcWordFreq) wcRender();
|
|
130
417
|
}
|
|
131
418
|
|
|
132
|
-
|
|
133
|
-
const canvas = document.getElementById("wc-canvas");
|
|
134
|
-
const body = document.getElementById("wc-body");
|
|
135
|
-
canvas.width = body.clientWidth;
|
|
136
|
-
canvas.height = body.clientHeight;
|
|
419
|
+
// ── Exclude browser ───────────────────────────────────────────────────────────
|
|
137
420
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
421
|
+
let _wcExclBrowseParent = null;
|
|
422
|
+
let _wcExclBrowseCurrent = '';
|
|
423
|
+
|
|
424
|
+
function wcToggleExcludeBrowser() {
|
|
425
|
+
const browser = document.getElementById('wc-exclude-browser');
|
|
426
|
+
const isHidden = browser.classList.toggle('hidden');
|
|
427
|
+
if (!isHidden) {
|
|
428
|
+
const start = _wcExclBrowseCurrent || document.getElementById('wc-root').value || '/';
|
|
429
|
+
wcExclLoadBrowse(start);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async function wcExclLoadBrowse(dirPath) {
|
|
434
|
+
const list = document.getElementById('wc-excl-browse-list');
|
|
435
|
+
list.innerHTML = '<p class="px-3 py-4 text-xs text-gray-400 text-center">Loading…</p>';
|
|
436
|
+
try {
|
|
437
|
+
const data = await fetch('/api/browse?all=1&path=' + encodeURIComponent(dirPath)).then((r) => r.json());
|
|
438
|
+
_wcExclBrowseCurrent = data.current;
|
|
439
|
+
_wcExclBrowseParent = data.parent;
|
|
440
|
+
document.getElementById('wc-excl-browse-path').textContent = data.current;
|
|
441
|
+
document.getElementById('wc-excl-browse-up').disabled = !data.parent;
|
|
442
|
+
|
|
443
|
+
const root = document.getElementById('wc-root').value.trim();
|
|
444
|
+
|
|
445
|
+
// Helper: compute the entry to add — relative to root if possible, else basename
|
|
446
|
+
function exclusionEntry(absPath, name) {
|
|
447
|
+
if (root && absPath.startsWith(root + '/')) {
|
|
448
|
+
return absPath.slice(root.length + 1); // relative path
|
|
449
|
+
}
|
|
450
|
+
return name;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const excluded = wcGetExcludeSet();
|
|
454
|
+
|
|
455
|
+
const rows = data.dirs
|
|
456
|
+
.filter((dir) => !excluded.has(exclusionEntry(dir.path, dir.name)))
|
|
457
|
+
.map((dir) => {
|
|
458
|
+
const entry = exclusionEntry(dir.path, dir.name);
|
|
459
|
+
return `<div class="group flex items-center hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
|
|
460
|
+
<button data-path="${wcEsc(dir.path)}" onclick="wcExclLoadBrowse(this.dataset.path)"
|
|
461
|
+
class="flex-1 flex items-center gap-2 px-3 py-2 text-sm text-left">
|
|
462
|
+
<span class="text-gray-400 shrink-0">📁</span>
|
|
463
|
+
<span class="text-gray-700 dark:text-gray-300 truncate">${wcEsc(dir.name)}</span>
|
|
464
|
+
</button>
|
|
465
|
+
<button onclick="wcAddExclude('${wcEsc(entry)}')" title="Add to exclusions"
|
|
466
|
+
class="shrink-0 text-red-400 hover:text-red-600 px-3 py-2 text-sm font-bold transition-colors">⊖</button>
|
|
467
|
+
</div>`;
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// Also show files in the current dir that match scanned extensions
|
|
471
|
+
const exts = [...document.querySelectorAll('.wc-ext:checked')].map((cb) => cb.value);
|
|
472
|
+
const fileRows = (data.files || [])
|
|
473
|
+
.filter((f) => exts.some((e) => f.name.endsWith('.' + e)))
|
|
474
|
+
.filter((f) => !excluded.has(exclusionEntry(f.path, f.name)))
|
|
475
|
+
.map((f) => {
|
|
476
|
+
const entry = exclusionEntry(f.path, f.name);
|
|
477
|
+
return `<div class="group flex items-center hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
|
|
478
|
+
<span class="flex-1 flex items-center gap-2 px-3 py-2 text-sm text-gray-500 dark:text-gray-400">
|
|
479
|
+
<span class="shrink-0 opacity-40">📄</span>
|
|
480
|
+
<span class="truncate font-mono">${wcEsc(f.name)}</span>
|
|
481
|
+
</span>
|
|
482
|
+
<button onclick="wcAddExclude('${wcEsc(entry)}')" title="Add to exclusions"
|
|
483
|
+
class="shrink-0 text-red-400 hover:text-red-600 px-3 py-2 text-sm font-bold transition-colors">⊖</button>
|
|
484
|
+
</div>`;
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
const allRows = [...rows, ...fileRows];
|
|
488
|
+
list.innerHTML = allRows.length
|
|
489
|
+
? allRows.join('')
|
|
490
|
+
: '<p class="px-3 py-3 text-xs text-gray-400 text-center">Empty directory</p>';
|
|
491
|
+
} catch {
|
|
492
|
+
list.innerHTML = '<p class="px-3 py-4 text-xs text-red-400 text-center">Cannot read directory</p>';
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function wcExclBrowseUp() {
|
|
497
|
+
if (_wcExclBrowseParent) wcExclLoadBrowse(_wcExclBrowseParent);
|
|
158
498
|
}
|
|
159
499
|
|
|
160
500
|
// ── Browser (folder picker) ───────────────────────────────────────────────────
|
|
161
501
|
|
|
162
502
|
let _wcBrowseParent = null;
|
|
163
|
-
let _wcBrowseCurrent =
|
|
503
|
+
let _wcBrowseCurrent = '';
|
|
164
504
|
|
|
165
505
|
function wcToggleBrowser() {
|
|
166
|
-
const browser = document.getElementById(
|
|
167
|
-
const isHidden = browser.classList.toggle(
|
|
168
|
-
if (!isHidden) wcLoadBrowse(_wcBrowseCurrent || document.getElementById(
|
|
506
|
+
const browser = document.getElementById('wc-browser');
|
|
507
|
+
const isHidden = browser.classList.toggle('hidden');
|
|
508
|
+
if (!isHidden) wcLoadBrowse(_wcBrowseCurrent || document.getElementById('wc-root').value || '/');
|
|
169
509
|
}
|
|
170
510
|
|
|
171
511
|
async function wcLoadBrowse(dirPath) {
|
|
172
|
-
const list = document.getElementById(
|
|
512
|
+
const list = document.getElementById('wc-browse-list');
|
|
173
513
|
list.innerHTML = '<p class="px-3 py-4 text-xs text-gray-400 text-center">Loading…</p>';
|
|
174
514
|
try {
|
|
175
|
-
const data = await fetch(
|
|
515
|
+
const data = await fetch('/api/browse?path=' + encodeURIComponent(dirPath)).then((r) => r.json());
|
|
176
516
|
_wcBrowseCurrent = data.current;
|
|
177
517
|
_wcBrowseParent = data.parent;
|
|
178
|
-
document.getElementById(
|
|
179
|
-
document.getElementById(
|
|
518
|
+
document.getElementById('wc-browse-path').textContent = data.current;
|
|
519
|
+
document.getElementById('wc-browse-up').disabled = !data.parent;
|
|
180
520
|
|
|
181
521
|
const rows = data.dirs.map((dir) =>
|
|
182
|
-
`<
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
522
|
+
`<div class="group flex items-center hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
|
|
523
|
+
<button data-path="${wcEsc(dir.path)}" onclick="wcLoadBrowse(this.dataset.path)"
|
|
524
|
+
class="flex-1 flex items-center gap-2 px-3 py-2 text-sm text-left">
|
|
525
|
+
<span class="text-gray-400 shrink-0">📁</span>
|
|
526
|
+
<span class="text-gray-700 dark:text-gray-300 truncate">${wcEsc(dir.name)}</span>
|
|
527
|
+
</button>
|
|
528
|
+
<button onclick="wcAddExclude('${wcEsc(dir.name)}')" title="Exclude this folder"
|
|
529
|
+
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>
|
|
530
|
+
</div>`,
|
|
187
531
|
);
|
|
188
532
|
|
|
189
533
|
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">
|
|
@@ -191,7 +535,7 @@ async function wcLoadBrowse(dirPath) {
|
|
|
191
535
|
</button>`;
|
|
192
536
|
|
|
193
537
|
list.innerHTML = (rows.length
|
|
194
|
-
? rows.join(
|
|
538
|
+
? rows.join('')
|
|
195
539
|
: '<p class="px-3 py-3 text-xs text-gray-400 text-center">No sub-folders</p>'
|
|
196
540
|
) + selectBtn;
|
|
197
541
|
} catch {
|
|
@@ -204,53 +548,63 @@ function wcBrowseUp() {
|
|
|
204
548
|
}
|
|
205
549
|
|
|
206
550
|
function wcSelectFolder() {
|
|
207
|
-
document.getElementById(
|
|
208
|
-
localStorage.setItem(
|
|
209
|
-
document.getElementById(
|
|
551
|
+
document.getElementById('wc-root').value = _wcBrowseCurrent;
|
|
552
|
+
localStorage.setItem('wc-root', _wcBrowseCurrent);
|
|
553
|
+
document.getElementById('wc-browser').classList.add('hidden');
|
|
210
554
|
}
|
|
211
555
|
|
|
212
556
|
// ── Persistence (localStorage) ────────────────────────────────────────────────
|
|
213
557
|
|
|
558
|
+
function wcToggleAllExts() {
|
|
559
|
+
const boxes = [...document.querySelectorAll('.wc-ext')];
|
|
560
|
+
const allChecked = boxes.every((cb) => cb.checked);
|
|
561
|
+
boxes.forEach((cb) => { cb.checked = !allChecked; });
|
|
562
|
+
document.getElementById('wcToggleAllBtn').textContent = allChecked ? 'All' : 'None';
|
|
563
|
+
wcSaveExts();
|
|
564
|
+
}
|
|
565
|
+
|
|
214
566
|
function wcSaveExts() {
|
|
215
|
-
const exts = [...document.querySelectorAll(
|
|
216
|
-
localStorage.setItem(
|
|
567
|
+
const exts = [...document.querySelectorAll('.wc-ext:checked')].map((cb) => cb.value);
|
|
568
|
+
localStorage.setItem('wc-exts', JSON.stringify(exts));
|
|
217
569
|
}
|
|
218
570
|
|
|
219
571
|
function wcRestorePrefs() {
|
|
220
|
-
const savedRoot = localStorage.getItem(
|
|
572
|
+
const savedRoot = localStorage.getItem('wc-root');
|
|
221
573
|
if (savedRoot) {
|
|
222
|
-
document.getElementById(
|
|
574
|
+
document.getElementById('wc-root').value = savedRoot;
|
|
223
575
|
_wcBrowseCurrent = savedRoot;
|
|
224
576
|
}
|
|
225
|
-
const
|
|
577
|
+
const savedExclude = localStorage.getItem('wc-exclude');
|
|
578
|
+
if (savedExclude) document.getElementById('wc-exclude').value = savedExclude;
|
|
579
|
+
const savedExts = localStorage.getItem('wc-exts');
|
|
226
580
|
if (savedExts) {
|
|
227
581
|
try {
|
|
228
582
|
const exts = JSON.parse(savedExts);
|
|
229
|
-
document.querySelectorAll(
|
|
583
|
+
document.querySelectorAll('.wc-ext').forEach((cb) => {
|
|
230
584
|
cb.checked = exts.includes(cb.value);
|
|
231
585
|
});
|
|
232
586
|
} catch { /* ignore corrupt data */ }
|
|
233
587
|
}
|
|
234
|
-
document.querySelectorAll(
|
|
235
|
-
cb.addEventListener(
|
|
588
|
+
document.querySelectorAll('.wc-ext').forEach((cb) => {
|
|
589
|
+
cb.addEventListener('change', wcSaveExts);
|
|
236
590
|
});
|
|
237
591
|
}
|
|
238
592
|
|
|
239
593
|
// ── Open / Launch / Close ─────────────────────────────────────────────────────
|
|
240
594
|
|
|
241
595
|
async function openWordCloud() {
|
|
242
|
-
const overlay = document.getElementById(
|
|
243
|
-
const status = document.getElementById(
|
|
244
|
-
const canvas = document.getElementById(
|
|
245
|
-
overlay.classList.remove(
|
|
246
|
-
status.textContent =
|
|
247
|
-
status.classList.remove(
|
|
248
|
-
canvas.classList.add(
|
|
249
|
-
|
|
250
|
-
const rootInput = document.getElementById(
|
|
596
|
+
const overlay = document.getElementById('wc-overlay');
|
|
597
|
+
const status = document.getElementById('wc-status');
|
|
598
|
+
const canvas = document.getElementById('wc-canvas');
|
|
599
|
+
overlay.classList.remove('hidden');
|
|
600
|
+
status.textContent = 'Choose a root folder and click Launch.';
|
|
601
|
+
status.classList.remove('hidden');
|
|
602
|
+
canvas.classList.add('hidden');
|
|
603
|
+
|
|
604
|
+
const rootInput = document.getElementById('wc-root');
|
|
251
605
|
if (!rootInput.value) {
|
|
252
606
|
try {
|
|
253
|
-
const cfg = await fetch(
|
|
607
|
+
const cfg = await fetch('/api/config').then((r) => r.json());
|
|
254
608
|
if (cfg.docsFolder) {
|
|
255
609
|
rootInput.value = cfg.docsFolder;
|
|
256
610
|
_wcBrowseCurrent = cfg.docsFolder;
|
|
@@ -260,62 +614,71 @@ async function openWordCloud() {
|
|
|
260
614
|
}
|
|
261
615
|
|
|
262
616
|
async function launchWordCloud() {
|
|
263
|
-
const status = document.getElementById(
|
|
264
|
-
const canvas = document.getElementById(
|
|
265
|
-
const root = document.getElementById(
|
|
617
|
+
const status = document.getElementById('wc-status');
|
|
618
|
+
const canvas = document.getElementById('wc-canvas');
|
|
619
|
+
const root = document.getElementById('wc-root').value.trim();
|
|
266
620
|
|
|
267
621
|
if (!root) {
|
|
268
|
-
status.textContent =
|
|
269
|
-
status.classList.remove(
|
|
622
|
+
status.textContent = 'Please select a root folder first.';
|
|
623
|
+
status.classList.remove('hidden');
|
|
270
624
|
return;
|
|
271
625
|
}
|
|
272
626
|
|
|
273
|
-
const exts = [...document.querySelectorAll(
|
|
627
|
+
const exts = [...document.querySelectorAll('.wc-ext:checked')].map((cb) => cb.value);
|
|
274
628
|
if (!exts.length) {
|
|
275
|
-
status.textContent =
|
|
276
|
-
status.classList.remove(
|
|
629
|
+
status.textContent = 'Please select at least one extension.';
|
|
630
|
+
status.classList.remove('hidden');
|
|
277
631
|
return;
|
|
278
632
|
}
|
|
279
633
|
|
|
280
|
-
document.getElementById(
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
634
|
+
document.getElementById('wc-browser').classList.add('hidden');
|
|
635
|
+
document.getElementById('wc-sidebar').classList.add('hidden');
|
|
636
|
+
document.getElementById('wc-detail').classList.add('hidden');
|
|
637
|
+
status.textContent = 'Reading files…';
|
|
638
|
+
status.classList.remove('hidden');
|
|
639
|
+
canvas.classList.add('hidden');
|
|
284
640
|
|
|
285
641
|
try {
|
|
642
|
+
const excludeRaw = document.getElementById('wc-exclude').value.trim();
|
|
643
|
+
const excludeDirs = excludeRaw ? excludeRaw.split(/[\n,]/).map((s) => s.trim()).filter(Boolean) : [];
|
|
644
|
+
localStorage.setItem('wc-exclude', excludeRaw);
|
|
645
|
+
|
|
286
646
|
const params = new URLSearchParams({ path: root });
|
|
287
|
-
exts.forEach((e) => params.append(
|
|
288
|
-
|
|
647
|
+
exts.forEach((e) => params.append('ext', e));
|
|
648
|
+
excludeDirs.forEach((d) => params.append('exclude', d));
|
|
649
|
+
const res = await fetch('/api/wordcloud?' + params);
|
|
289
650
|
if (!res.ok) {
|
|
290
651
|
const err = await res.json();
|
|
291
|
-
status.textContent =
|
|
652
|
+
status.textContent = 'Error: ' + (err.error || res.statusText);
|
|
292
653
|
return;
|
|
293
654
|
}
|
|
294
|
-
const { files,
|
|
655
|
+
const { files, fileTexts } = await res.json();
|
|
295
656
|
status.textContent = `Analyzing ${files} file(s)…`;
|
|
296
657
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
658
|
+
_wcWordFreq = new Map();
|
|
659
|
+
_wcWordFiles = new Map();
|
|
660
|
+
_wcTotalFiles = files;
|
|
661
|
+
|
|
662
|
+
for (const { path: filePath, text } of fileTexts) {
|
|
663
|
+
const ext = (filePath.split('.').pop() || '').toLowerCase();
|
|
664
|
+
const words = extractWordsFromFile(text, ext);
|
|
665
|
+
const seenInFile = new Set();
|
|
666
|
+
for (const w of words) {
|
|
667
|
+
_wcWordFreq.set(w, (_wcWordFreq.get(w) || 0) + 1);
|
|
668
|
+
if (!seenInFile.has(w)) {
|
|
669
|
+
seenInFile.add(w);
|
|
670
|
+
if (!_wcWordFiles.has(w)) _wcWordFiles.set(w, new Set());
|
|
671
|
+
_wcWordFiles.get(w).add(filePath);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
311
674
|
}
|
|
312
675
|
|
|
313
|
-
|
|
676
|
+
wcRender();
|
|
314
677
|
} catch (err) {
|
|
315
|
-
status.textContent =
|
|
678
|
+
status.textContent = 'Error: ' + err.message;
|
|
316
679
|
}
|
|
317
680
|
}
|
|
318
681
|
|
|
319
682
|
function closeWordCloud() {
|
|
320
|
-
document.getElementById(
|
|
683
|
+
document.getElementById('wc-overlay').classList.add('hidden');
|
|
321
684
|
}
|