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.

@@ -1,189 +1,533 @@
1
- // ── Word Cloud — stop words & logic ───────────────────────────────────────────
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
- // ── Human stop words (English + French only) ──────────────────────────────────
5
- const WC_STOP_WORDS = new Set([
6
- // ── English ──
7
- "the","and","for","are","but","not","you","all","this","that",
8
- "with","have","from","they","will","one","been","can","has","was",
9
- "more","also","when","there","their","what","about","which","would",
10
- "into","than","then","each","just","over","after","such","here",
11
- "its","your","our","some","were","very","only","out","had",
12
- "she","his","her","him","who","how","any","other","these","those",
13
- "being","may","use","used","using","should","could","shall","must",
14
- "need","via","per","like","well","make","made","take","taken",
15
- "same","both","between","before","while","where","since","still",
16
- "even","able","back","come","down","does","done","good","much",
17
- "said","them","want","way","without","within","whether","though",
18
- "although","however","therefore","thus","hence","indeed","rather",
19
- "either","neither","yet","once","upon","during","against","among",
20
- "through","because","along","already","always","often","never",
21
- "again","around","another","every","most","many","least","less",
22
- "own","off","too","now","new","old","few","see","set",
23
- "put","got","let","tell","know","think","seem","look","keep",
24
- "give","show","hear","play","run","move","live","hold","turn",
25
- "help","start","might","really","actually","simply","directly",
26
- "basically","generally","normally","especially","currently",
27
- // ── French ──
28
- "les","des","une","pour","pas","sur","par","est","qui","que",
29
- "dans","avec","sont","plus","tout","aux","mais","comme","vous",
30
- "nous","leur","lui","elle","ils","elles","ces","ses","mon","ton",
31
- "son","mes","tes","ainsi","donc","alors","car","peut","fait",
32
- "encore","bien","aussi","très","même","entre","vers","dont","sans",
33
- "sous","cette","celui","celle","ceux","celles","cela","ceci",
34
- "avoir","être","faire","aller","voir","savoir","pouvoir","vouloir",
35
- "devoir","partir","venir","prendre","mettre","dire","donner","tenir",
36
- "tous","toutes","trop","peu","beaucoup","moins",
37
- "jamais","toujours","souvent","parfois","déjà","bientôt",
38
- "maintenant","après","avant","depuis","pendant","selon","afin",
39
- "quand","parce","puisque","lorsque","tandis","quoique",
40
- "cependant","néanmoins","pourtant","toutefois","ailleurs","ensuite",
41
- "enfin","surtout","notamment","seulement","simplement","vraiment",
42
- "lequel","laquelle","lesquels","lesquelles","duquel","auquel",
43
- "desquels","auxquels","chaque","plusieurs","certains","certaines",
44
- "quelques","autres","certaine","aucun","aucune","nul","nulle",
45
- "chacun","chacune","quoi","quels","quelles","quel","quelle",
46
- "votre","vos","notre","nos","leurs","puis","lors",
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
- // ── Language-specific stop words (keyed by file extension) ────────────────────
50
- const WC_LANG_STOP_WORDS = {
51
- // shared across all code files
52
- _code: new Set([
53
- "true","false","null","undefined","void","this","self","super",
54
- "return","import","export","from","class","interface","extends","implements",
55
- "public","private","protected","static","final","abstract","override",
56
- "function","const","let","var","type","enum","struct","trait","impl",
57
- "async","await","new","delete","typeof","instanceof","throw","throws",
58
- "catch","finally","break","continue","default","switch","case","while",
59
- "else","elif","pass","raise","yield","with","lambda","where","match",
60
- "package","module","namespace","using","include","require","define",
61
- "object","string","number","boolean","integer","float","double","long",
62
- "byte","char","array","list","dict","tuple","bool","none",
63
- "nil","print","console","stdout","stderr",
64
- "size","length","count","index","value","result","error","data",
65
- "args","argv","argc","opts","params","param","props","prop",
66
- "node","root","tree","left","right","next","prev","head","tail",
67
- "init","main","test","spec","mock","stub","util","helper","base",
68
- "read","write","open","close","send","recv","load","save","parse",
69
- "append","remove","insert","update","create","build",
70
- "get","set","has","add","push","pull","move","copy","sort","find","filter","reduce","map",
71
- ]),
72
- ts: new Set(["interface","namespace","readonly","keyof","infer","never","unknown","declare","satisfies","generic"]),
73
- tsx: new Set(["interface","namespace","readonly","keyof","infer","never","unknown","declare","satisfies","generic","react","props","children","component","render","state","effect","hook","usestate","useeffect","useref","usecontext","usememo","usecallback"]),
74
- js: new Set(["prototype","arguments","callee","getter","setter","proxy","promise","resolve","reject","then","catch"]),
75
- jsx: new Set(["react","props","children","component","render","state","effect","hook"]),
76
- java: new Set(["override","extends","implements","throws","final","synchronized","volatile","transient","strictfp","instanceof","assert","native","enum","annotation","autowired","bean","component","service","repository","controller","springframework","junit","lombok","getter","setter","builder","tostring","hashcode","equals","arraylist","hashmap","optional","stream","collectors","iterator","comparable","serializable","runnable","callable","exception","runtimeexception"]),
77
- kt: new Set(["override","extends","object","companion","data","sealed","inner","inline","reified","crossinline","noinline","lateinit","lazy","apply","also","let","with","run","when","vararg","init","constructor","primary","secondary","coroutine","suspend","flow","stateflow","sharedflow","viewmodel","livedata","hilt","inject","module","provides","binds","qualifier","scope","composable","remember","mutablestate","launchedeffect","lifecycle"]),
78
- py: new Set(["self","none","elif","lambda","yield","with","raise","assert","except","finally","pass","global","nonlocal","import","from","class","isinstance","issubclass","hasattr","getattr","setattr","delattr","super","property","staticmethod","classmethod","abstractmethod","dataclass","field","list","dict","tuple","bool","bytes","range","enumerate","print","input","open","close","append","extend","update","items","values","keys","format","strip","split","join","replace","lower","upper","startswith","endswith","type","len","int","str","float","repr","iter","next","zip","reversed","sorted","filter","reduce","partial","functools","itertools","collections","defaultdict","namedtuple","deque","heapq","bisect","contextlib","pathlib","datetime","argparse","logging","unittest","pytest","numpy","pandas","torch","tensorflow","flask","django","fastapi","sqlalchemy","pydantic","asyncio"]),
79
- go: new Set(["func","chan","goroutine","defer","panic","recover","make","append","copy","close","delete","complex","imag","real","iota","blank","rune","byte","error","interface","struct","select","range","fallthrough","goto","nil","main","init","println","printf","sprintf","fprintf","scanf","sscanf","fscanf","strings","bytes","errors","context","sync","atomic","mutex","waitgroup","channel","handler","middleware","router","request","response","writer","reader","buffer","scanner","encoder","decoder","marshal","unmarshal"]),
80
- rs: new Set(["let","mut","impl","trait","enum","struct","match","some","none","okay","unwrap","expect","clone","borrow","lifetime","ownership","move","copy","drop","result","option","vector","string","hashmap","hashset","btreemap","btreeset","refcell","mutex","rwlock","channel","sender","receiver","tokio","async","await","spawn","future","stream","iterator","closure","generic","where","derive","macro","println","format","panic","assert","eprintln","anyhow","thiserror","serde","warp","actix","axum","rocket","diesel","sqlx"]),
81
- cs: new Set(["using","namespace","sealed","readonly","partial","virtual","abstract","override","base","object","string","bool","int","double","float","decimal","char","byte","short","long","uint","ulong","ushort","sbyte","nullable","async","await","task","void","delegate","event","linq","lambda","expression","predicate","action","func","tuple","list","dictionary","hashset","queue","stack","array","ienumerable","ienumerator","icollection","ilist","idictionary","console","debug","trace","exception","argumentexception","nullreferenceexception","invalidoperationexception","ioexception","attribute","annotation","property","getter","setter","constructor","disposable","dispose","garbage","collector","threading","semaphore","monitor","interlocked","concurrent","entity","framework","controller","service","repository","dependency","injection","middleware","startup","program","appsettings","configuration","logging","dbcontext","dbset","migrations"]),
82
- swift: new Set(["guard","defer","where","some","opaque","associated","protocol","extension","mutating","nonmutating","lazy","weak","unowned","inout","subscript","operator","precedence","associativity","typealias","throw","rethrows","convenience","required","override","final","open","fileprivate","internal","public","private","static","class","struct","enum","actor","swiftui","view","body","state","binding","observedobject","stateobject","environmentobject","published","viewmodel","modifier","frame","padding","foregroundcolor","background","spacer","vstack","hstack","zstack","list","navigationview","navigationlink","sheet","alert","toolbar","button","text","image","textfield","toggle","picker","slider","stepper","scrollview","grid","path","shape","color","font","gesture","animation","transition","combine","publisher","subscriber","cancellable","sink","assign","flatmap","receive","debounce","throttle"]),
83
- rb: new Set(["begin","rescue","ensure","raise","yield","lambda","proc","block","method","module","require","include","extend","prepend","attr","accessor","reader","writer","protected","private","public","freeze","frozen","dup","clone","object","class","instance","variable","symbol","hash","array","string","integer","float","boolean","nil","each","map","select","reject","inject","reduce","collect","detect","find","sort","uniq","flatten","compact","zip","take","drop","first","last","push","pull","shift","unshift","pop","min","max","sum","count","size","length","empty","any","all","none","include","respond","send","define","missing","inherited","extended","prepended","included","hook","callback","before","after","around","action","controller","model","view","route","migration","schema","database","activerecord","activemodel","actioncontroller","actionview","rails","rack","gem","bundle","rake","rspec","minitest","capybara","factory","stub","mock","double","expect","allow","receive"]),
84
- html: new Set(["html","head","body","title","meta","link","script","style","div","span","section","article","header","footer","main","aside","nav","form","input","button","select","option","textarea","table","thead","tbody","tfoot","caption","colgroup","fieldset","legend","label","output","datalist","progress","details","summary","figure","figcaption","picture","source","video","audio","track","canvas","svg","path","circle","rect","line","polygon","polyline","defs","group","class","href","type","name","value","action","method","placeholder","required","checked","disabled","readonly","multiple","accept","enctype","autocomplete","autofocus","novalidate","pattern","minlength","maxlength","charset","content","lang","xmlns","viewport","initial","scale","description","keywords","author","refresh","equiv","property","role","aria","tabindex","draggable","contenteditable","spellcheck","translate","hidden","data","onclick","onchange","onsubmit","onload","onerror","onfocus","onblur","onkeydown","onkeyup","onmousedown","onmouseup","onmouseover","onmouseout","onmousemove","onscroll","onresize","tailwind"]),
85
- css: new Set(["display","flex","grid","block","inline","position","relative","absolute","fixed","sticky","float","clear","margin","padding","border","outline","width","height","overflow","visibility","opacity","transform","transition","animation","color","background","font","size","weight","family","decoration","align","justify","content","items","self","order","grow","shrink","basis","wrap","direction","columns","rows","template","repeat","auto","minmax","span","area","place","object","fit","cursor","pointer","events","none","user","select","resize","zindex","layer","shadow","radius","filter","backdrop","blur","brightness","contrast","grayscale","hue","invert","saturate","sepia","rotate","scale","translate","skew","matrix","perspective","clip","mask","shape","outside","inside","overflow","ellipsis","nowrap","word","break","letter","spacing","indent","capitalize","uppercase","lowercase","white","space","vertical","baseline","middle","super","sub","list","counter","before","after","hover","focus","active","visited","disabled","checked","valid","invalid","required","optional","first","last","nth","child","root","empty","target","lang","media","screen","print","query","supports","charset","keyframes","viewport","placeholder","selection","scrollbar"]),
86
- scss: new Set(["mixin","include","extend","each","while","else","error","warn","debug","forward","through","variable","interpolation","parent","selector","namespace","module","sass","scss","nesting","rule","declaration","property","value","default","global","important"]),
87
- yml: new Set(["true","false","null","name","uses","with","runs","steps","jobs","needs","outputs","inputs","secrets","vars","environment","strategy","matrix","services","container","image","volumes","ports","networks","healthcheck","command","entrypoint","working","directory","timeout","retry","concurrency","permissions","defaults","reusable","workflow","trigger","push","pull","request","release","schedule","cron","branches","tags","paths","types","artifacts","cache","restore","keys","path","upload","download","deployment","approvals","reviewers","pages","packages","registry","docker","build","test","deploy","lint","coverage","report","notify","slack","email","include","exclude"]),
88
- yaml: new Set(["true","false","null","name","uses","with","runs","steps","jobs","needs","outputs","inputs","secrets","vars","environment","strategy","matrix","services","container","image","volumes","ports","networks","healthcheck","command","entrypoint","working","directory","timeout","retry","concurrency","permissions","defaults","reusable","workflow","trigger","push","pull","request","release","schedule","cron","branches","tags","paths","types","artifacts","cache","restore","keys","path","upload","download","deployment","approvals","reviewers","pages","packages","registry","docker","build","test","deploy","lint","coverage","report","notify","slack","email","include","exclude"]),
89
- json: new Set(["true","false","null","name","version","description","keywords","license","author","main","module","types","files","scripts","dependencies","devdependencies","peerdependencies","optionaldependencies","engines","repository","bugs","homepage","private","workspaces","exports","imports","type","config","publishconfig","resolutions","overrides"]),
90
- xml: new Set(["version","encoding","xmlns","xsi","type","schema","location","element","attribute","complextype","simpletype","sequence","choice","restriction","extension","include","redefine","annotation","documentation","appinfo","notation","field","selector","unique","keyref","substitution","group","attributegroup","list","union","enumeration","pattern","minlength","maxlength","length","mininclusive","maxinclusive","minexclusive","maxexclusive","fractiondigits","totaldigits","whitespace","nillable","abstract","block","final","mixed","target","namespace","elementformdefault","attributeformdefault","blockdefault","finaldefault","lang","base","value","fixed","default","form","processcontents"]),
91
- toml: new Set(["true","false","name","version","description","edition","authors","license","repository","homepage","documentation","keywords","categories","workspace","members","dependencies","devdependencies","builddependencies","features","profile","release","debug","test","bench","default","path","optional","package","build","resolver","patch","replace","source","registry","target","compiler","linker","rustflags","incremental","overflow","checks","panic","codegen","units","lto","strip","debuginfo","splitdebuginfo","rpath","binaries","examples","tests","benches","library"]),
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, '&amp;').replace(/</g, '&lt;')
273
+ .replace(/>/g, '&gt;').replace(/"/g, '&quot;');
274
+ }
95
275
 
96
- function wcBuildStopWords(exts) {
97
- const combined = new Set(WC_STOP_WORDS);
98
- const hasCode = exts.some((e) => e !== "md" && e !== "txt");
99
- if (hasCode) {
100
- for (const w of WC_LANG_STOP_WORDS._code) combined.add(w);
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
- for (const ext of exts) {
103
- const s = WC_LANG_STOP_WORDS[ext];
104
- if (s) for (const w of s) combined.add(w);
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
- return combined;
403
+ wcRefreshDetailPanel();
107
404
  }
108
405
 
109
- function wcEsc(s) {
110
- return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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 extractWordsFromMarkdown(text, stopWords = WC_STOP_WORDS) {
114
- return text
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
- function renderWordCloud(list) {
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
- const isDark = document.documentElement.classList.contains("dark");
139
- const colors = isDark
140
- ? ["#60a5fa","#34d399","#f9a8d4","#a78bfa","#fbbf24","#6ee7b7","#93c5fd","#fb923c"]
141
- : ["#1d4ed8","#047857","#7c3aed","#b45309","#be123c","#0369a1","#4338ca","#c2410c"];
142
-
143
- const maxFreq = list[0][1];
144
- const wordList = list.map(([w, n]) => [w, Math.max(10, Math.round((72 * n) / maxFreq))]);
145
-
146
- document.getElementById("wc-status").classList.add("hidden");
147
- canvas.classList.remove("hidden");
148
-
149
- WordCloud(canvas, {
150
- list: wordList,
151
- gridSize: Math.round((8 * canvas.width) / 1024),
152
- fontFamily: "ui-sans-serif, system-ui, sans-serif",
153
- color: () => colors[Math.floor(Math.random() * colors.length)],
154
- backgroundColor: isDark ? "#030712" : "#ffffff",
155
- rotateRatio: 0.3,
156
- minSize: 10,
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">&#128193;</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">&#128196;</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("wc-browser");
167
- const isHidden = browser.classList.toggle("hidden");
168
- if (!isHidden) wcLoadBrowse(_wcBrowseCurrent || document.getElementById("wc-root").value || "/");
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("wc-browse-list");
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("/api/browse?path=" + encodeURIComponent(dirPath)).then((r) => r.json());
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("wc-browse-path").textContent = data.current;
179
- document.getElementById("wc-browse-up").disabled = !data.parent;
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
- `<button data-path="${wcEsc(dir.path)}" onclick="wcLoadBrowse(this.dataset.path)"
183
- class="w-full flex items-center gap-2 px-3 py-2 text-sm text-left hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
184
- <span class="text-gray-400 shrink-0">&#128193;</span>
185
- <span class="text-gray-700 dark:text-gray-300 truncate">${wcEsc(dir.name)}</span>
186
- </button>`
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">&#128193;</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("wc-root").value = _wcBrowseCurrent;
208
- localStorage.setItem("wc-root", _wcBrowseCurrent);
209
- document.getElementById("wc-browser").classList.add("hidden");
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(".wc-ext:checked")].map((cb) => cb.value);
216
- localStorage.setItem("wc-exts", JSON.stringify(exts));
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("wc-root");
572
+ const savedRoot = localStorage.getItem('wc-root');
221
573
  if (savedRoot) {
222
- document.getElementById("wc-root").value = savedRoot;
574
+ document.getElementById('wc-root').value = savedRoot;
223
575
  _wcBrowseCurrent = savedRoot;
224
576
  }
225
- const savedExts = localStorage.getItem("wc-exts");
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(".wc-ext").forEach((cb) => {
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(".wc-ext").forEach((cb) => {
235
- cb.addEventListener("change", wcSaveExts);
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("wc-overlay");
243
- const status = document.getElementById("wc-status");
244
- const canvas = document.getElementById("wc-canvas");
245
- overlay.classList.remove("hidden");
246
- status.textContent = "Choose a root folder and click Launch.";
247
- status.classList.remove("hidden");
248
- canvas.classList.add("hidden");
249
-
250
- const rootInput = document.getElementById("wc-root");
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("/api/config").then((r) => r.json());
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("wc-status");
264
- const canvas = document.getElementById("wc-canvas");
265
- const root = document.getElementById("wc-root").value.trim();
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 = "Please select a root folder first.";
269
- status.classList.remove("hidden");
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(".wc-ext:checked")].map((cb) => cb.value);
627
+ const exts = [...document.querySelectorAll('.wc-ext:checked')].map((cb) => cb.value);
274
628
  if (!exts.length) {
275
- status.textContent = "Please select at least one extension.";
276
- status.classList.remove("hidden");
629
+ status.textContent = 'Please select at least one extension.';
630
+ status.classList.remove('hidden');
277
631
  return;
278
632
  }
279
633
 
280
- document.getElementById("wc-browser").classList.add("hidden");
281
- status.textContent = "Reading files…";
282
- status.classList.remove("hidden");
283
- canvas.classList.add("hidden");
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("ext", e));
288
- const res = await fetch("/api/wordcloud?" + params);
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 = "Error: " + (err.error || res.statusText);
652
+ status.textContent = 'Error: ' + (err.error || res.statusText);
292
653
  return;
293
654
  }
294
- const { files, text } = await res.json();
655
+ const { files, fileTexts } = await res.json();
295
656
  status.textContent = `Analyzing ${files} file(s)…`;
296
657
 
297
- const stopWords = wcBuildStopWords(exts);
298
- const freq = {};
299
- for (const w of extractWordsFromMarkdown(text, stopWords)) {
300
- freq[w] = (freq[w] || 0) + 1;
301
- }
302
-
303
- const list = Object.entries(freq)
304
- .filter(([, n]) => n >= 2)
305
- .sort((a, b) => b[1] - a[1])
306
- .slice(0, 150);
307
-
308
- if (!list.length) {
309
- status.textContent = "Not enough words found.";
310
- return;
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
- renderWordCloud(list);
676
+ wcRender();
314
677
  } catch (err) {
315
- status.textContent = "Error: " + err.message;
678
+ status.textContent = 'Error: ' + err.message;
316
679
  }
317
680
  }
318
681
 
319
682
  function closeWordCloud() {
320
- document.getElementById("wc-overlay").classList.add("hidden");
683
+ document.getElementById('wc-overlay').classList.add('hidden');
321
684
  }