@dev-pi2pie/word-counter 0.1.4 → 0.1.5-canary.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -0
- package/dist/cjs/detector.cjs +427 -0
- package/dist/cjs/detector.cjs.map +1 -0
- package/dist/cjs/index.cjs +10 -1257
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/markdown.cjs +1318 -0
- package/dist/cjs/markdown.cjs.map +1 -0
- package/dist/esm/bin.mjs +966 -298
- package/dist/esm/bin.mjs.map +1 -1
- package/dist/esm/detector.d.mts +37 -0
- package/dist/esm/detector.mjs +412 -0
- package/dist/esm/detector.mjs.map +1 -0
- package/dist/esm/index.d.mts +1 -1
- package/dist/esm/index.mjs +2 -1248
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/index2.d.mts +2 -0
- package/dist/esm/markdown.mjs +1229 -0
- package/dist/esm/markdown.mjs.map +1 -0
- package/dist/esm/worker/count-worker.mjs +412 -47
- package/dist/esm/worker/count-worker.mjs.map +1 -1
- package/dist/esm/worker-pool.mjs +6 -3
- package/dist/esm/worker-pool.mjs.map +1 -1
- package/dist/wasm-language-detector/LICENSE +21 -0
- package/dist/wasm-language-detector/language_detector.d.ts +4 -0
- package/dist/wasm-language-detector/language_detector.js +132 -0
- package/dist/wasm-language-detector/language_detector_bg.wasm +0 -0
- package/dist/wasm-language-detector/language_detector_bg.wasm.d.ts +8 -0
- package/dist/wasm-language-detector/package.json +17 -0
- package/package.json +18 -10
package/README.md
CHANGED
|
@@ -37,6 +37,8 @@ For local development in this repository:
|
|
|
37
37
|
```bash
|
|
38
38
|
git clone https://github.com/dev-pi2pie/word-counter.git
|
|
39
39
|
cd word-counter
|
|
40
|
+
rustup target add wasm32-unknown-unknown
|
|
41
|
+
cargo install wasm-pack --locked
|
|
40
42
|
bun install
|
|
41
43
|
bun run build
|
|
42
44
|
npm link
|
|
@@ -94,6 +96,22 @@ word-counter --han-language zh-Hant "漢字測試"
|
|
|
94
96
|
word-counter --han-tag zh-Hans "汉字测试"
|
|
95
97
|
```
|
|
96
98
|
|
|
99
|
+
Enable the optional WASM detector for ambiguous Latin and Han routes:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
word-counter --detector wasm "This sentence should clearly be detected as English for the wasm detector path."
|
|
103
|
+
word-counter --detector wasm "漢字測試需要更多內容才能觸發偵測"
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Detector mode notes:
|
|
107
|
+
|
|
108
|
+
- `--detector regex` is the default behavior.
|
|
109
|
+
- `--detector wasm` only runs for ambiguous `und-Latn` and `und-Hani` chunks.
|
|
110
|
+
- `--detector regex` keeps the original script/regex chunk-first detection path.
|
|
111
|
+
- `--detector wasm` uses a detector-oriented ambiguous-window scoring pass before accepted tags are projected back onto the counting chunks.
|
|
112
|
+
- Very short chunks stay on the original `und-*` fallback.
|
|
113
|
+
- Low-confidence or unsupported detector results fall back to `und-*`.
|
|
114
|
+
|
|
97
115
|
Collect non-words (emoji/symbols/punctuation):
|
|
98
116
|
|
|
99
117
|
```bash
|
|
@@ -164,6 +182,33 @@ word-counter --print-jobs-limit
|
|
|
164
182
|
|
|
165
183
|
`--print-jobs-limit` must be used alone (no other inputs or runtime flags).
|
|
166
184
|
|
|
185
|
+
### Doctor (`doctor`)
|
|
186
|
+
|
|
187
|
+
Use `doctor` to verify whether the current host can run `word-counter` reliably:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
word-counter doctor
|
|
191
|
+
word-counter doctor --format json
|
|
192
|
+
word-counter doctor --format json --pretty
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Doctor scope in v1:
|
|
196
|
+
|
|
197
|
+
- checks runtime support policy against Node.js `>=20`
|
|
198
|
+
- verifies `Intl.Segmenter` availability plus word/grapheme constructor health
|
|
199
|
+
- reports batch jobs host limits using the same heuristics as `--print-jobs-limit`
|
|
200
|
+
- reports worker-route preflight signals and the worker-disable env toggle that affects worker availability
|
|
201
|
+
|
|
202
|
+
Doctor output contract:
|
|
203
|
+
|
|
204
|
+
- default output is human-readable text
|
|
205
|
+
- `--format json` prints compact machine-readable JSON
|
|
206
|
+
- `--format json --pretty` prints indented JSON
|
|
207
|
+
- doctor exits with code `0` for `ok` / `warn`, `1` for invalid doctor usage, and `2` for runtime `fail`
|
|
208
|
+
- doctor does not accept counting inputs, `--path`, `--jobs`, or other counting/debug flags
|
|
209
|
+
|
|
210
|
+
For a field-by-field explanation of doctor text and JSON output, see [`docs/doctor-usage-guide.md`](docs/doctor-usage-guide.md).
|
|
211
|
+
|
|
167
212
|
For full policy details, JSON parity expectations (`--misc`, `--total-of whitespace,words`), and benchmark standards, see [`docs/batch-jobs-usage-guide.md`](docs/batch-jobs-usage-guide.md).
|
|
168
213
|
|
|
169
214
|
### Stable Path Resolution Contract
|
|
@@ -266,6 +311,7 @@ Skip details stay debug-gated and can be suppressed with `--quiet-skips`.
|
|
|
266
311
|
- Adjacent characters that share the same locale tag are grouped into a chunk.
|
|
267
312
|
- Each chunk is counted with `Intl.Segmenter` at `granularity: "word"`, caching segmenters to avoid re-instantiation.
|
|
268
313
|
- Per-locale counts are summed into an overall total and printed to stdout.
|
|
314
|
+
- With `--detector wasm`, ambiguous `und-Latn` and `und-Hani` chunks can be relabeled through the optional WASM detector before counting.
|
|
269
315
|
|
|
270
316
|
## Locale vs Language Code
|
|
271
317
|
|
|
@@ -289,6 +335,10 @@ import wordCounter, {
|
|
|
289
335
|
segmentTextByLocale,
|
|
290
336
|
showSingularOrPluralWord,
|
|
291
337
|
} from "@dev-pi2pie/word-counter";
|
|
338
|
+
import {
|
|
339
|
+
wordCounterWithDetector,
|
|
340
|
+
segmentTextByLocaleWithDetector,
|
|
341
|
+
} from "@dev-pi2pie/word-counter/detector";
|
|
292
342
|
|
|
293
343
|
wordCounter("Hello world", { latinLanguageHint: "en" });
|
|
294
344
|
wordCounter("Hello world", { latinTagHint: "en" });
|
|
@@ -302,6 +352,11 @@ wordCounter("Hi 👋, world!", { mode: "char", nonWords: true });
|
|
|
302
352
|
wordCounter("飛鳥 bird 貓 cat", { mode: "char-collector" });
|
|
303
353
|
wordCounter("Hi\tthere\n", { nonWords: true, includeWhitespace: true });
|
|
304
354
|
countCharsForLocale("👋", "en");
|
|
355
|
+
await wordCounterWithDetector(
|
|
356
|
+
"This sentence should clearly be detected as English for the wasm detector path.",
|
|
357
|
+
{ detector: "wasm" },
|
|
358
|
+
);
|
|
359
|
+
await segmentTextByLocaleWithDetector("Hello 世界", { detector: "regex" });
|
|
305
360
|
```
|
|
306
361
|
|
|
307
362
|
Note: `includeWhitespace` only affects results when `nonWords: true` is enabled.
|
|
@@ -335,6 +390,7 @@ Sample output (with `nonWords: true` and `includeWhitespace: true`):
|
|
|
335
390
|
|
|
336
391
|
```js
|
|
337
392
|
const wordCounter = require("@dev-pi2pie/word-counter");
|
|
393
|
+
const detector = require("@dev-pi2pie/word-counter/detector");
|
|
338
394
|
const {
|
|
339
395
|
countCharsForLocale,
|
|
340
396
|
countWordsForLocale,
|
|
@@ -356,6 +412,10 @@ wordCounter("Hi 👋, world!", { mode: "char", nonWords: true });
|
|
|
356
412
|
wordCounter("飛鳥 bird 貓 cat", { mode: "char-collector" });
|
|
357
413
|
wordCounter("Hi\tthere\n", { nonWords: true, includeWhitespace: true });
|
|
358
414
|
countCharsForLocale("👋", "en");
|
|
415
|
+
await detector.wordCounterWithDetector(
|
|
416
|
+
"This sentence should clearly be detected as English for the wasm detector path.",
|
|
417
|
+
{ detector: "wasm" },
|
|
418
|
+
);
|
|
359
419
|
```
|
|
360
420
|
|
|
361
421
|
Note: `includeWhitespace` only affects results when `nonWords: true` is enabled.
|
|
@@ -410,6 +470,18 @@ Sample output (with `nonWords: true` and `includeWhitespace: true`):
|
|
|
410
470
|
| -------------------------- | -------- | ------------------------------ |
|
|
411
471
|
| `showSingularOrPluralWord` | function | Formats singular/plural words. |
|
|
412
472
|
|
|
473
|
+
#### Detector Subpath
|
|
474
|
+
|
|
475
|
+
Import from `@dev-pi2pie/word-counter/detector` for the explicit detector-enabled API.
|
|
476
|
+
|
|
477
|
+
| Export | Kind | Notes |
|
|
478
|
+
| ----------------------------- | -------- | ----------------------------------------------- |
|
|
479
|
+
| `wordCounterWithDetector` | function | Async detector-aware counting entrypoint. |
|
|
480
|
+
| `segmentTextByLocaleWithDetector` | function | Async detector-aware locale segmentation. |
|
|
481
|
+
| `countSectionsWithDetector` | function | Async detector-aware section counting. |
|
|
482
|
+
| `DEFAULT_DETECTOR_MODE` | value | Current default detector mode (`regex`). |
|
|
483
|
+
| `DETECTOR_MODES` | value | Supported detector modes. |
|
|
484
|
+
|
|
413
485
|
#### Types
|
|
414
486
|
|
|
415
487
|
| Export | Kind | Notes |
|
|
@@ -623,6 +695,9 @@ Example JSON (trimmed):
|
|
|
623
695
|
|
|
624
696
|
- Detection is regex/script based, not statistical language-ID.
|
|
625
697
|
- Ambiguous Latin defaults to `und-Latn`; Han fallback defaults to `und-Hani`.
|
|
698
|
+
- `--detector wasm` is optional and conservative; it only runs for ambiguous chunks that meet minimum script-bearing length thresholds.
|
|
699
|
+
- The current first WASM engine is `whatlang`, remapped into this package's public tags.
|
|
700
|
+
- The npm package ships one portable WASM artifact; users do not install per-OS detector packages.
|
|
626
701
|
- Use explicit tag and hint flags when you need deterministic tagging.
|
|
627
702
|
- Full notes (built-in heuristics, limitations, and override guidance) are tracked in `docs/locale-tag-detection-notes.md`.
|
|
628
703
|
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
const require_markdown = require("./markdown.cjs");
|
|
2
|
+
let node_fs = require("node:fs");
|
|
3
|
+
let node_path = require("node:path");
|
|
4
|
+
let node_module = require("node:module");
|
|
5
|
+
let node_url = require("node:url");
|
|
6
|
+
//#region src/detector/none.ts
|
|
7
|
+
async function segmentTextByLocaleWithRegexDetector(text, options = {}) {
|
|
8
|
+
return require_markdown.segmentTextByLocale(text, options);
|
|
9
|
+
}
|
|
10
|
+
async function wordCounterWithRegexDetector(text, options = {}) {
|
|
11
|
+
return require_markdown.wc_default(text, options);
|
|
12
|
+
}
|
|
13
|
+
async function countSectionsWithRegexDetector(input, section, options = {}) {
|
|
14
|
+
return require_markdown.countSections(input, section, options);
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/detector/result-builder.ts
|
|
18
|
+
function getNonWordTotal(nonWords) {
|
|
19
|
+
return nonWords.counts.emoji + nonWords.counts.symbols + nonWords.counts.punctuation + (nonWords.counts.whitespace ?? 0);
|
|
20
|
+
}
|
|
21
|
+
function collectNonWordsAggregate(analyzed, enabled) {
|
|
22
|
+
if (!enabled) return;
|
|
23
|
+
const collection = require_markdown.createNonWordCollection();
|
|
24
|
+
for (const chunk of analyzed) {
|
|
25
|
+
if (!chunk.nonWords) continue;
|
|
26
|
+
require_markdown.mergeNonWordCollections(collection, chunk.nonWords);
|
|
27
|
+
}
|
|
28
|
+
return collection;
|
|
29
|
+
}
|
|
30
|
+
function buildWordCounterResultFromChunks(chunks, options = {}) {
|
|
31
|
+
const mode = require_markdown.resolveMode(options.mode, "chunk");
|
|
32
|
+
const collectNonWords = Boolean(options.nonWords);
|
|
33
|
+
const includeWhitespace = Boolean(options.includeWhitespace);
|
|
34
|
+
if (mode === "char" || mode === "char-collector") {
|
|
35
|
+
const analyzed = chunks.map((chunk) => require_markdown.analyzeCharChunk(chunk, collectNonWords, includeWhitespace));
|
|
36
|
+
const total = analyzed.reduce((sum, chunk) => sum + chunk.chars, 0);
|
|
37
|
+
const counts = collectNonWords ? {
|
|
38
|
+
words: analyzed.reduce((sum, chunk) => sum + chunk.wordChars, 0),
|
|
39
|
+
nonWords: analyzed.reduce((sum, chunk) => sum + chunk.nonWordChars, 0),
|
|
40
|
+
total
|
|
41
|
+
} : void 0;
|
|
42
|
+
if (mode === "char") return {
|
|
43
|
+
total,
|
|
44
|
+
counts,
|
|
45
|
+
breakdown: {
|
|
46
|
+
mode,
|
|
47
|
+
items: analyzed.map((chunk) => ({
|
|
48
|
+
locale: chunk.locale,
|
|
49
|
+
text: chunk.text,
|
|
50
|
+
chars: chunk.chars,
|
|
51
|
+
nonWords: chunk.nonWords
|
|
52
|
+
}))
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
total,
|
|
57
|
+
counts,
|
|
58
|
+
breakdown: {
|
|
59
|
+
mode,
|
|
60
|
+
items: require_markdown.aggregateCharsByLocale(analyzed).map((chunk) => ({
|
|
61
|
+
locale: chunk.locale,
|
|
62
|
+
chars: chunk.chars,
|
|
63
|
+
nonWords: chunk.nonWords
|
|
64
|
+
}))
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const analyzed = chunks.map((chunk) => require_markdown.analyzeChunk(chunk, collectNonWords, includeWhitespace));
|
|
69
|
+
const wordsTotal = analyzed.reduce((sum, chunk) => sum + chunk.words, 0);
|
|
70
|
+
const nonWordsTotal = collectNonWords ? analyzed.reduce((sum, chunk) => {
|
|
71
|
+
if (!chunk.nonWords) return sum;
|
|
72
|
+
return sum + getNonWordTotal(chunk.nonWords);
|
|
73
|
+
}, 0) : 0;
|
|
74
|
+
const total = analyzed.reduce((sum, chunk) => {
|
|
75
|
+
let chunkTotal = chunk.words;
|
|
76
|
+
if (collectNonWords && chunk.nonWords) chunkTotal += getNonWordTotal(chunk.nonWords);
|
|
77
|
+
return sum + chunkTotal;
|
|
78
|
+
}, 0);
|
|
79
|
+
const counts = collectNonWords ? {
|
|
80
|
+
words: wordsTotal,
|
|
81
|
+
nonWords: nonWordsTotal,
|
|
82
|
+
total
|
|
83
|
+
} : void 0;
|
|
84
|
+
if (mode === "segments") return {
|
|
85
|
+
total,
|
|
86
|
+
counts,
|
|
87
|
+
breakdown: {
|
|
88
|
+
mode,
|
|
89
|
+
items: analyzed.map((chunk) => ({
|
|
90
|
+
locale: chunk.locale,
|
|
91
|
+
text: chunk.text,
|
|
92
|
+
words: chunk.words,
|
|
93
|
+
segments: chunk.segments,
|
|
94
|
+
nonWords: chunk.nonWords
|
|
95
|
+
}))
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
if (mode === "collector") return {
|
|
99
|
+
total,
|
|
100
|
+
counts,
|
|
101
|
+
breakdown: {
|
|
102
|
+
mode,
|
|
103
|
+
items: require_markdown.aggregateByLocale(analyzed),
|
|
104
|
+
nonWords: collectNonWordsAggregate(analyzed, collectNonWords)
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
return {
|
|
108
|
+
total,
|
|
109
|
+
counts,
|
|
110
|
+
breakdown: {
|
|
111
|
+
mode,
|
|
112
|
+
items: analyzed.map((chunk) => ({
|
|
113
|
+
locale: chunk.locale,
|
|
114
|
+
text: chunk.text,
|
|
115
|
+
words: chunk.words,
|
|
116
|
+
nonWords: chunk.nonWords
|
|
117
|
+
}))
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
//#endregion
|
|
122
|
+
//#region src/detector/sections.ts
|
|
123
|
+
function normalizeText(value) {
|
|
124
|
+
if (value == null) return "";
|
|
125
|
+
if (typeof value === "string") return value;
|
|
126
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
127
|
+
try {
|
|
128
|
+
return JSON.stringify(value);
|
|
129
|
+
} catch {
|
|
130
|
+
return String(value);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function buildPerKeyItems(data, options) {
|
|
134
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return [];
|
|
135
|
+
return Promise.all(Object.entries(data).map(async ([key, value]) => {
|
|
136
|
+
const valueText = normalizeText(value);
|
|
137
|
+
return {
|
|
138
|
+
name: key,
|
|
139
|
+
source: "frontmatter",
|
|
140
|
+
result: await wordCounterWithDetector(valueText ? `${key}: ${valueText}` : key, options)
|
|
141
|
+
};
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
async function buildSingleItem(name, text, options, source) {
|
|
145
|
+
return [{
|
|
146
|
+
name,
|
|
147
|
+
source,
|
|
148
|
+
result: await wordCounterWithDetector(text, options)
|
|
149
|
+
}];
|
|
150
|
+
}
|
|
151
|
+
function sumTotals(items) {
|
|
152
|
+
return items.reduce((sum, item) => sum + item.result.total, 0);
|
|
153
|
+
}
|
|
154
|
+
async function countSectionsWithResolvedDetector(input, section, options = {}) {
|
|
155
|
+
options.mode;
|
|
156
|
+
if (section === "all") {
|
|
157
|
+
const result = await wordCounterWithDetector(input, options);
|
|
158
|
+
return {
|
|
159
|
+
section,
|
|
160
|
+
total: result.total,
|
|
161
|
+
frontmatterType: null,
|
|
162
|
+
items: [{
|
|
163
|
+
name: "all",
|
|
164
|
+
source: "content",
|
|
165
|
+
result
|
|
166
|
+
}]
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const parsed = require_markdown.parseMarkdown(input);
|
|
170
|
+
const frontmatterText = parsed.frontmatter ?? "";
|
|
171
|
+
const contentText = parsed.content ?? "";
|
|
172
|
+
let items = [];
|
|
173
|
+
if (section === "frontmatter") items = await buildSingleItem("frontmatter", frontmatterText, options, "frontmatter");
|
|
174
|
+
else if (section === "content") items = await buildSingleItem("content", contentText, options, "content");
|
|
175
|
+
else if (section === "split") items = [...await buildSingleItem("frontmatter", frontmatterText, options, "frontmatter"), ...await buildSingleItem("content", contentText, options, "content")];
|
|
176
|
+
else if (section === "per-key") items = await buildPerKeyItems(parsed.data, options);
|
|
177
|
+
else if (section === "split-per-key") items = [...await buildPerKeyItems(parsed.data, options), ...await buildSingleItem("content", contentText, options, "content")];
|
|
178
|
+
return {
|
|
179
|
+
section,
|
|
180
|
+
total: sumTotals(items),
|
|
181
|
+
frontmatterType: parsed.frontmatterType,
|
|
182
|
+
items
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const LATIN_WASM_MIN_CONFIDENCE = .75;
|
|
186
|
+
const HANI_WASM_MIN_CONFIDENCE = .9;
|
|
187
|
+
const LATIN_SCRIPT_REGEX = /\p{Script=Latin}/u;
|
|
188
|
+
const HAN_SCRIPT_REGEX = /\p{Script=Han}/u;
|
|
189
|
+
const DETECTOR_ROUTE_POLICIES = {
|
|
190
|
+
[require_markdown.DEFAULT_LOCALE]: {
|
|
191
|
+
routeTag: require_markdown.DEFAULT_LOCALE,
|
|
192
|
+
minScriptChars: 24,
|
|
193
|
+
minConfidence: LATIN_WASM_MIN_CONFIDENCE,
|
|
194
|
+
requireReliable: true
|
|
195
|
+
},
|
|
196
|
+
[require_markdown.DEFAULT_HAN_TAG]: {
|
|
197
|
+
routeTag: require_markdown.DEFAULT_HAN_TAG,
|
|
198
|
+
minScriptChars: 12,
|
|
199
|
+
minConfidence: HANI_WASM_MIN_CONFIDENCE,
|
|
200
|
+
requireReliable: true
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
function isAmbiguousDetectorRoute(locale) {
|
|
204
|
+
return locale === "und-Latn" || locale === "und-Hani";
|
|
205
|
+
}
|
|
206
|
+
function countScriptBearingCharsForRoute(text, routeTag) {
|
|
207
|
+
const matcher = routeTag === "und-Hani" ? HAN_SCRIPT_REGEX : LATIN_SCRIPT_REGEX;
|
|
208
|
+
let count = 0;
|
|
209
|
+
for (const char of text) if (matcher.test(char)) count += 1;
|
|
210
|
+
return count;
|
|
211
|
+
}
|
|
212
|
+
function shouldRunWasmDetector(text, routeTag) {
|
|
213
|
+
const policy = DETECTOR_ROUTE_POLICIES[routeTag];
|
|
214
|
+
return countScriptBearingCharsForRoute(text, routeTag) >= policy.minScriptChars;
|
|
215
|
+
}
|
|
216
|
+
function normalizeDetectorSampleForRoute(text, routeTag) {
|
|
217
|
+
const matcher = routeTag === "und-Hani" ? HAN_SCRIPT_REGEX : LATIN_SCRIPT_REGEX;
|
|
218
|
+
return [...text].map((char) => {
|
|
219
|
+
if (matcher.test(char)) return char;
|
|
220
|
+
if (/\s/u.test(char)) return " ";
|
|
221
|
+
return " ";
|
|
222
|
+
}).join("").replace(/\s+/g, " ").trim();
|
|
223
|
+
}
|
|
224
|
+
//#endregion
|
|
225
|
+
//#region src/detector/whatlang-wasm.ts
|
|
226
|
+
const GENERATED_FOLDER_NAME = "wasm-language-detector";
|
|
227
|
+
const GENERATED_MODULE_FILE = "language_detector.js";
|
|
228
|
+
const MAX_SEARCH_DEPTH = 8;
|
|
229
|
+
const requireFromHere = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
|
|
230
|
+
const WASM_DETECTOR_RUNTIME_UNAVAILABLE_MESSAGE = "WASM detector runtime is unavailable. Run `bun run build:wasm` to generate it.";
|
|
231
|
+
let modulePromise = null;
|
|
232
|
+
function resolveCandidateModulePaths() {
|
|
233
|
+
const moduleDir = (0, node_path.dirname)((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
|
|
234
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
235
|
+
let currentDir = moduleDir;
|
|
236
|
+
for (let depth = 0; depth < MAX_SEARCH_DEPTH; depth += 1) {
|
|
237
|
+
candidates.add((0, node_path.join)(currentDir, GENERATED_FOLDER_NAME, GENERATED_MODULE_FILE));
|
|
238
|
+
candidates.add((0, node_path.join)(currentDir, "generated", GENERATED_FOLDER_NAME, GENERATED_MODULE_FILE));
|
|
239
|
+
const parentDir = (0, node_path.dirname)(currentDir);
|
|
240
|
+
if (parentDir === currentDir) break;
|
|
241
|
+
currentDir = parentDir;
|
|
242
|
+
}
|
|
243
|
+
return [...candidates];
|
|
244
|
+
}
|
|
245
|
+
function resolveWhatlangWasmModulePath() {
|
|
246
|
+
for (const candidate of resolveCandidateModulePaths()) if ((0, node_fs.existsSync)(candidate)) return candidate;
|
|
247
|
+
throw new Error(WASM_DETECTOR_RUNTIME_UNAVAILABLE_MESSAGE);
|
|
248
|
+
}
|
|
249
|
+
async function loadWhatlangWasmModule() {
|
|
250
|
+
if (!modulePromise) modulePromise = (async () => {
|
|
251
|
+
return requireFromHere(resolveWhatlangWasmModulePath());
|
|
252
|
+
})();
|
|
253
|
+
return modulePromise;
|
|
254
|
+
}
|
|
255
|
+
async function detectWithWhatlangWasm(text, routeTag) {
|
|
256
|
+
return (await loadWhatlangWasmModule()).detect_language(text, routeTag);
|
|
257
|
+
}
|
|
258
|
+
//#endregion
|
|
259
|
+
//#region src/detector/whatlang-map.ts
|
|
260
|
+
const LATIN_LANGUAGE_TAGS = {
|
|
261
|
+
cat: "ca",
|
|
262
|
+
ces: "cs",
|
|
263
|
+
dan: "da",
|
|
264
|
+
deu: "de",
|
|
265
|
+
eng: "en",
|
|
266
|
+
fin: "fi",
|
|
267
|
+
fra: "fr",
|
|
268
|
+
hun: "hu",
|
|
269
|
+
ita: "it",
|
|
270
|
+
lat: "la",
|
|
271
|
+
nld: "nl",
|
|
272
|
+
pol: "pl",
|
|
273
|
+
por: "pt",
|
|
274
|
+
ron: "ro",
|
|
275
|
+
spa: "es",
|
|
276
|
+
swe: "sv",
|
|
277
|
+
tur: "tr"
|
|
278
|
+
};
|
|
279
|
+
const HANI_LANGUAGE_TAGS = {
|
|
280
|
+
cmn: "zh",
|
|
281
|
+
jpn: "ja"
|
|
282
|
+
};
|
|
283
|
+
function hasSupportedScript(result, routeTag) {
|
|
284
|
+
if (routeTag === "und-Latn") return result.script === "Latin";
|
|
285
|
+
return result.script === "Mandarin";
|
|
286
|
+
}
|
|
287
|
+
function remapLanguageTag(lang, routeTag) {
|
|
288
|
+
if (routeTag === "und-Latn") return LATIN_LANGUAGE_TAGS[lang];
|
|
289
|
+
return HANI_LANGUAGE_TAGS[lang];
|
|
290
|
+
}
|
|
291
|
+
function remapWhatlangResult(result, routeTag) {
|
|
292
|
+
if (!hasSupportedScript(result, routeTag)) return null;
|
|
293
|
+
const tag = remapLanguageTag(result.lang, routeTag);
|
|
294
|
+
if (!tag) return null;
|
|
295
|
+
return {
|
|
296
|
+
tag,
|
|
297
|
+
confidence: result.confidence,
|
|
298
|
+
reliable: result.reliable,
|
|
299
|
+
source: "wasm"
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function getDetectorFallbackTag(routeTag) {
|
|
303
|
+
return routeTag === "und-Hani" ? require_markdown.DEFAULT_HAN_TAG : require_markdown.DEFAULT_LOCALE;
|
|
304
|
+
}
|
|
305
|
+
//#endregion
|
|
306
|
+
//#region src/detector/wasm.ts
|
|
307
|
+
function shouldAcceptDetectorTag(routeTag, confidence, reliable) {
|
|
308
|
+
const policy = DETECTOR_ROUTE_POLICIES[routeTag];
|
|
309
|
+
if (policy.requireReliable && reliable !== true) return false;
|
|
310
|
+
if (confidence === void 0) return false;
|
|
311
|
+
return confidence >= policy.minConfidence;
|
|
312
|
+
}
|
|
313
|
+
function buildDetectorWindows(chunks) {
|
|
314
|
+
const windows = [];
|
|
315
|
+
for (let index = 0; index < chunks.length; index += 1) {
|
|
316
|
+
const chunk = chunks[index];
|
|
317
|
+
if (!chunk || !isAmbiguousDetectorRoute(chunk.locale)) continue;
|
|
318
|
+
const previousWindow = windows[windows.length - 1];
|
|
319
|
+
if (previousWindow && previousWindow.routeTag === chunk.locale && previousWindow.endIndex === index - 1) {
|
|
320
|
+
previousWindow.endIndex = index;
|
|
321
|
+
previousWindow.text += chunk.text;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
windows.push({
|
|
325
|
+
routeTag: chunk.locale,
|
|
326
|
+
startIndex: index,
|
|
327
|
+
endIndex: index,
|
|
328
|
+
text: chunk.text
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
return windows;
|
|
332
|
+
}
|
|
333
|
+
async function resolveWindowLocale(window) {
|
|
334
|
+
if (!shouldRunWasmDetector(window.text, window.routeTag)) return window.routeTag;
|
|
335
|
+
const rawResult = await detectWithWhatlangWasm(window.text, window.routeTag);
|
|
336
|
+
const rawRemapped = rawResult ? remapWhatlangResult(rawResult, window.routeTag) : null;
|
|
337
|
+
const normalizedSample = normalizeDetectorSampleForRoute(window.text, window.routeTag);
|
|
338
|
+
const normalizedResult = normalizedSample.length > 0 && normalizedSample !== window.text ? await detectWithWhatlangWasm(normalizedSample, window.routeTag) : null;
|
|
339
|
+
const normalizedRemapped = normalizedResult ? remapWhatlangResult(normalizedResult, window.routeTag) : null;
|
|
340
|
+
const candidates = [rawRemapped, normalizedRemapped].filter((value) => value !== null);
|
|
341
|
+
if (candidates.length === 0) return getDetectorFallbackTag(window.routeTag);
|
|
342
|
+
const strongestCandidate = candidates.reduce((best, current) => {
|
|
343
|
+
if (!best) return current;
|
|
344
|
+
return (current.confidence ?? 0) > (best.confidence ?? 0) ? current : best;
|
|
345
|
+
}, candidates[0]);
|
|
346
|
+
if (strongestCandidate && shouldAcceptDetectorTag(window.routeTag, strongestCandidate.confidence, strongestCandidate.reliable)) return strongestCandidate.tag;
|
|
347
|
+
if (window.routeTag === "und-Latn" && rawRemapped && normalizedRemapped && rawRemapped.tag === normalizedRemapped.tag) {
|
|
348
|
+
if (Math.max(rawRemapped.confidence ?? 0, normalizedRemapped.confidence ?? 0) >= .7) return rawRemapped.tag;
|
|
349
|
+
}
|
|
350
|
+
return getDetectorFallbackTag(window.routeTag);
|
|
351
|
+
}
|
|
352
|
+
async function segmentTextByLocaleWithWasmDetector(text, options = {}) {
|
|
353
|
+
const chunks = require_markdown.segmentTextByLocale(text, options);
|
|
354
|
+
const resolved = [...chunks];
|
|
355
|
+
const windows = buildDetectorWindows(chunks);
|
|
356
|
+
for (const window of windows) {
|
|
357
|
+
const resolvedLocale = await resolveWindowLocale(window);
|
|
358
|
+
for (let index = window.startIndex; index <= window.endIndex; index += 1) {
|
|
359
|
+
const chunk = resolved[index];
|
|
360
|
+
if (!chunk) continue;
|
|
361
|
+
resolved[index] = {
|
|
362
|
+
...chunk,
|
|
363
|
+
locale: resolvedLocale
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return resolved;
|
|
368
|
+
}
|
|
369
|
+
async function wordCounterWithWasmDetector(text, options = {}) {
|
|
370
|
+
return buildWordCounterResultFromChunks(await segmentTextByLocaleWithWasmDetector(text, options), options);
|
|
371
|
+
}
|
|
372
|
+
async function countSectionsWithWasmDetector(input, section, options = {}) {
|
|
373
|
+
return countSectionsWithResolvedDetector(input, section, options);
|
|
374
|
+
}
|
|
375
|
+
//#endregion
|
|
376
|
+
//#region src/detector/index.ts
|
|
377
|
+
const DETECTOR_MODES = ["regex", "wasm"];
|
|
378
|
+
const DEFAULT_DETECTOR_MODE = "regex";
|
|
379
|
+
function resolveDetectorMode(mode) {
|
|
380
|
+
return mode ?? "regex";
|
|
381
|
+
}
|
|
382
|
+
function assertDetectorModeImplemented(mode) {}
|
|
383
|
+
async function segmentTextByLocaleWithDetector(text, options = {}) {
|
|
384
|
+
if (resolveDetectorMode(options.detector) === "wasm") return segmentTextByLocaleWithWasmDetector(text, options);
|
|
385
|
+
return segmentTextByLocaleWithRegexDetector(text, options);
|
|
386
|
+
}
|
|
387
|
+
async function wordCounterWithDetector(text, options = {}) {
|
|
388
|
+
if (resolveDetectorMode(options.detector) === "wasm") return wordCounterWithWasmDetector(text, options);
|
|
389
|
+
return wordCounterWithRegexDetector(text, options);
|
|
390
|
+
}
|
|
391
|
+
async function countSectionsWithDetector(input, section, options = {}) {
|
|
392
|
+
if (resolveDetectorMode(options.detector) === "wasm") return countSectionsWithWasmDetector(input, section, options);
|
|
393
|
+
return countSectionsWithRegexDetector(input, section, options);
|
|
394
|
+
}
|
|
395
|
+
const DETECTOR_SOURCES = [
|
|
396
|
+
"script",
|
|
397
|
+
"hint",
|
|
398
|
+
"wasm"
|
|
399
|
+
];
|
|
400
|
+
const DEFAULT_DETECTOR_RESULT_SOURCE = "script";
|
|
401
|
+
function createDetectorResult(tag, source = DEFAULT_DETECTOR_RESULT_SOURCE, confidence, reliable) {
|
|
402
|
+
return {
|
|
403
|
+
tag,
|
|
404
|
+
source,
|
|
405
|
+
...confidence === void 0 ? {} : { confidence },
|
|
406
|
+
...reliable === void 0 ? {} : { reliable }
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
//#endregion
|
|
410
|
+
//#region src/detector/index.cjs.ts
|
|
411
|
+
const cjsExports = {
|
|
412
|
+
assertDetectorModeImplemented,
|
|
413
|
+
countSectionsWithDetector,
|
|
414
|
+
createDetectorResult,
|
|
415
|
+
DEFAULT_DETECTOR_MODE,
|
|
416
|
+
DEFAULT_DETECTOR_RESULT_SOURCE,
|
|
417
|
+
DETECTOR_MODES,
|
|
418
|
+
DETECTOR_SOURCES,
|
|
419
|
+
resolveDetectorMode,
|
|
420
|
+
segmentTextByLocaleWithDetector,
|
|
421
|
+
WASM_DETECTOR_RUNTIME_UNAVAILABLE_MESSAGE,
|
|
422
|
+
wordCounterWithDetector
|
|
423
|
+
};
|
|
424
|
+
module.exports = cjsExports;
|
|
425
|
+
//#endregion
|
|
426
|
+
|
|
427
|
+
//# sourceMappingURL=detector.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detector.cjs","names":["segmentTextByLocale","wordCounter","countSections","createNonWordCollection","resolveMode","analyzeCharChunk","aggregateCharsByLocale","analyzeChunk","aggregateByLocale","parseMarkdown","DEFAULT_LOCALE","DEFAULT_HAN_TAG","DEFAULT_HAN_TAG","DEFAULT_LOCALE","segmentTextByLocale"],"sources":["../../src/detector/none.ts","../../src/detector/result-builder.ts","../../src/detector/sections.ts","../../src/detector/policy.ts","../../src/detector/whatlang-wasm.ts","../../src/detector/whatlang-map.ts","../../src/detector/wasm.ts","../../src/detector/index.ts","../../src/detector/index.cjs.ts"],"sourcesContent":["import { countSections } from \"../markdown\";\nimport wordCounter, { segmentTextByLocale } from \"../wc\";\nimport type {\n DetectorCountSectionsOptions,\n DetectorLocaleOptions,\n DetectorWordCounterOptions,\n} from \"./types\";\n\nexport async function segmentTextByLocaleWithRegexDetector(\n text: string,\n options: DetectorLocaleOptions = {},\n) {\n return segmentTextByLocale(text, options);\n}\n\nexport async function wordCounterWithRegexDetector(\n text: string,\n options: DetectorWordCounterOptions = {},\n) {\n return wordCounter(text, options);\n}\n\nexport async function countSectionsWithRegexDetector(\n input: string,\n section: Parameters<typeof countSections>[1],\n options: DetectorCountSectionsOptions = {},\n) {\n return countSections(input, section, options);\n}\n","import {\n analyzeCharChunk,\n analyzeChunk,\n aggregateByLocale,\n aggregateCharsByLocale,\n} from \"../wc/analyze\";\nimport { resolveMode } from \"../wc/mode\";\nimport { createNonWordCollection, mergeNonWordCollections } from \"../wc/non-words\";\nimport type {\n CharBreakdown,\n CharCollectorBreakdown,\n ChunkBreakdown,\n ChunkWithSegments,\n LocaleChunk,\n NonWordCollection,\n WordCounterMode,\n WordCounterOptions,\n WordCounterResult,\n} from \"../wc/types\";\n\nfunction getNonWordTotal(nonWords: NonWordCollection): number {\n return (\n nonWords.counts.emoji +\n nonWords.counts.symbols +\n nonWords.counts.punctuation +\n (nonWords.counts.whitespace ?? 0)\n );\n}\n\nfunction collectNonWordsAggregate(\n analyzed: Array<{ nonWords?: NonWordCollection }>,\n enabled: boolean,\n): NonWordCollection | undefined {\n if (!enabled) {\n return undefined;\n }\n const collection = createNonWordCollection();\n for (const chunk of analyzed) {\n if (!chunk.nonWords) {\n continue;\n }\n mergeNonWordCollections(collection, chunk.nonWords);\n }\n return collection;\n}\n\nexport function buildWordCounterResultFromChunks(\n chunks: LocaleChunk[],\n options: WordCounterOptions = {},\n): WordCounterResult {\n const mode: WordCounterMode = resolveMode(options.mode, \"chunk\");\n const collectNonWords = Boolean(options.nonWords);\n const includeWhitespace = Boolean(options.includeWhitespace);\n\n if (mode === \"char\" || mode === \"char-collector\") {\n const analyzed = chunks.map((chunk) =>\n analyzeCharChunk(chunk, collectNonWords, includeWhitespace),\n );\n const total = analyzed.reduce((sum, chunk) => sum + chunk.chars, 0);\n const counts = collectNonWords\n ? {\n words: analyzed.reduce((sum, chunk) => sum + chunk.wordChars, 0),\n nonWords: analyzed.reduce((sum, chunk) => sum + chunk.nonWordChars, 0),\n total,\n }\n : undefined;\n\n if (mode === \"char\") {\n const items: CharBreakdown[] = analyzed.map((chunk) => ({\n locale: chunk.locale,\n text: chunk.text,\n chars: chunk.chars,\n nonWords: chunk.nonWords,\n }));\n return {\n total,\n counts,\n breakdown: {\n mode,\n items,\n },\n };\n }\n\n const aggregated = aggregateCharsByLocale(analyzed);\n const items: CharCollectorBreakdown[] = aggregated.map((chunk) => ({\n locale: chunk.locale,\n chars: chunk.chars,\n nonWords: chunk.nonWords,\n }));\n return {\n total,\n counts,\n breakdown: {\n mode,\n items,\n },\n };\n }\n\n const analyzed = chunks.map((chunk) =>\n analyzeChunk(chunk, collectNonWords, includeWhitespace),\n );\n const wordsTotal = analyzed.reduce((sum, chunk) => sum + chunk.words, 0);\n const nonWordsTotal = collectNonWords\n ? analyzed.reduce((sum, chunk) => {\n if (!chunk.nonWords) {\n return sum;\n }\n return sum + getNonWordTotal(chunk.nonWords);\n }, 0)\n : 0;\n const total = analyzed.reduce((sum, chunk) => {\n let chunkTotal = chunk.words;\n if (collectNonWords && chunk.nonWords) {\n chunkTotal += getNonWordTotal(chunk.nonWords);\n }\n return sum + chunkTotal;\n }, 0);\n\n const counts = collectNonWords ? { words: wordsTotal, nonWords: nonWordsTotal, total } : undefined;\n\n if (mode === \"segments\") {\n const items: ChunkWithSegments[] = analyzed.map((chunk) => ({\n locale: chunk.locale,\n text: chunk.text,\n words: chunk.words,\n segments: chunk.segments,\n nonWords: chunk.nonWords,\n }));\n return {\n total,\n counts,\n breakdown: {\n mode,\n items,\n },\n };\n }\n\n if (mode === \"collector\") {\n const items = aggregateByLocale(analyzed);\n const nonWords = collectNonWordsAggregate(analyzed, collectNonWords);\n return {\n total,\n counts,\n breakdown: {\n mode,\n items,\n nonWords,\n },\n };\n }\n\n const items: ChunkBreakdown[] = analyzed.map((chunk) => ({\n locale: chunk.locale,\n text: chunk.text,\n words: chunk.words,\n nonWords: chunk.nonWords,\n }));\n\n return {\n total,\n counts,\n breakdown: {\n mode,\n items,\n },\n };\n}\n","import { parseMarkdown } from \"../markdown\";\nimport type { SectionMode, SectionedResult } from \"../markdown\";\nimport type { WordCounterMode, WordCounterResult } from \"../wc/types\";\nimport type { DetectorCountSectionsOptions } from \"./types\";\nimport { wordCounterWithDetector } from \"./index\";\n\nfunction normalizeText(value: unknown): string {\n if (value == null) {\n return \"\";\n }\n if (typeof value === \"string\") {\n return value;\n }\n if (typeof value === \"number\" || typeof value === \"boolean\") {\n return String(value);\n }\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n}\n\nasync function buildPerKeyItems(\n data: Record<string, unknown> | null,\n options: DetectorCountSectionsOptions,\n): Promise<Array<{ name: string; source: \"frontmatter\"; result: WordCounterResult }>> {\n if (!data || typeof data !== \"object\" || Array.isArray(data)) {\n return [];\n }\n\n return Promise.all(\n Object.entries(data).map(async ([key, value]) => {\n const valueText = normalizeText(value);\n const text = valueText ? `${key}: ${valueText}` : key;\n return {\n name: key,\n source: \"frontmatter\" as const,\n result: await wordCounterWithDetector(text, options),\n };\n }),\n );\n}\n\nasync function buildSingleItem(\n name: string,\n text: string,\n options: DetectorCountSectionsOptions,\n source: \"frontmatter\" | \"content\",\n): Promise<Array<{ name: string; source: \"frontmatter\" | \"content\"; result: WordCounterResult }>> {\n return [{ name, source, result: await wordCounterWithDetector(text, options) }];\n}\n\nfunction sumTotals(items: Array<{ result: WordCounterResult }>): number {\n return items.reduce((sum, item) => sum + item.result.total, 0);\n}\n\nexport async function countSectionsWithResolvedDetector(\n input: string,\n section: SectionMode,\n options: DetectorCountSectionsOptions = {},\n): Promise<SectionedResult> {\n const mode: WordCounterMode = options.mode ?? \"chunk\";\n if (section === \"all\") {\n const result = await wordCounterWithDetector(input, options);\n return {\n section,\n total: result.total,\n frontmatterType: null,\n items: [{ name: \"all\", source: \"content\", result }],\n };\n }\n\n const parsed = parseMarkdown(input);\n const frontmatterText = parsed.frontmatter ?? \"\";\n const contentText = parsed.content ?? \"\";\n\n let items: Array<{ name: string; source: \"frontmatter\" | \"content\"; result: WordCounterResult }> = [];\n\n if (section === \"frontmatter\") {\n items = await buildSingleItem(\"frontmatter\", frontmatterText, options, \"frontmatter\");\n } else if (section === \"content\") {\n items = await buildSingleItem(\"content\", contentText, options, \"content\");\n } else if (section === \"split\") {\n items = [\n ...(await buildSingleItem(\"frontmatter\", frontmatterText, options, \"frontmatter\")),\n ...(await buildSingleItem(\"content\", contentText, options, \"content\")),\n ];\n } else if (section === \"per-key\") {\n items = await buildPerKeyItems(parsed.data, options);\n } else if (section === \"split-per-key\") {\n items = [\n ...(await buildPerKeyItems(parsed.data, options)),\n ...(await buildSingleItem(\"content\", contentText, options, \"content\")),\n ];\n }\n\n return {\n section,\n total: sumTotals(items),\n frontmatterType: parsed.frontmatterType,\n items,\n };\n}\n","import { DEFAULT_HAN_TAG, DEFAULT_LOCALE } from \"../wc/locale-detect\";\n\nexport const LATIN_WASM_MIN_SCRIPT_CHARS = 24;\nexport const HANI_WASM_MIN_SCRIPT_CHARS = 12;\nexport const LATIN_WASM_MIN_CONFIDENCE = 0.75;\nexport const HANI_WASM_MIN_CONFIDENCE = 0.9;\nexport const LATIN_WASM_CORROBORATED_MIN_CONFIDENCE = 0.7;\n\nconst LATIN_SCRIPT_REGEX = /\\p{Script=Latin}/u;\nconst HAN_SCRIPT_REGEX = /\\p{Script=Han}/u;\n\nexport type DetectorRouteTag = typeof DEFAULT_LOCALE | typeof DEFAULT_HAN_TAG;\n\nexport type DetectorRoutePolicy = {\n routeTag: DetectorRouteTag;\n minScriptChars: number;\n minConfidence: number;\n requireReliable: boolean;\n};\n\nexport const DETECTOR_ROUTE_POLICIES: Record<DetectorRouteTag, DetectorRoutePolicy> = {\n [DEFAULT_LOCALE]: {\n routeTag: DEFAULT_LOCALE,\n minScriptChars: LATIN_WASM_MIN_SCRIPT_CHARS,\n minConfidence: LATIN_WASM_MIN_CONFIDENCE,\n requireReliable: true,\n },\n [DEFAULT_HAN_TAG]: {\n routeTag: DEFAULT_HAN_TAG,\n minScriptChars: HANI_WASM_MIN_SCRIPT_CHARS,\n minConfidence: HANI_WASM_MIN_CONFIDENCE,\n requireReliable: true,\n },\n};\n\nexport function isAmbiguousDetectorRoute(locale: string): locale is DetectorRouteTag {\n return locale === DEFAULT_LOCALE || locale === DEFAULT_HAN_TAG;\n}\n\nexport function countScriptBearingCharsForRoute(\n text: string,\n routeTag: DetectorRouteTag,\n): number {\n const matcher = routeTag === DEFAULT_HAN_TAG ? HAN_SCRIPT_REGEX : LATIN_SCRIPT_REGEX;\n let count = 0;\n for (const char of text) {\n if (matcher.test(char)) {\n count += 1;\n }\n }\n return count;\n}\n\nexport function shouldRunWasmDetector(text: string, routeTag: DetectorRouteTag): boolean {\n const policy = DETECTOR_ROUTE_POLICIES[routeTag];\n return countScriptBearingCharsForRoute(text, routeTag) >= policy.minScriptChars;\n}\n\nexport function normalizeDetectorSampleForRoute(\n text: string,\n routeTag: DetectorRouteTag,\n): string {\n const matcher = routeTag === DEFAULT_HAN_TAG ? HAN_SCRIPT_REGEX : LATIN_SCRIPT_REGEX;\n return [...text]\n .map((char) => {\n if (matcher.test(char)) {\n return char;\n }\n if (/\\s/u.test(char)) {\n return \" \";\n }\n return \" \";\n })\n .join(\"\")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n","import { existsSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { createRequire } from \"node:module\";\nimport { fileURLToPath } from \"node:url\";\nimport type { DetectorRouteTag } from \"./policy\";\nimport type { WhatlangWasmResult } from \"./whatlang-map\";\n\nconst GENERATED_FOLDER_NAME = \"wasm-language-detector\";\nconst GENERATED_MODULE_FILE = \"language_detector.js\";\nconst MAX_SEARCH_DEPTH = 8;\nconst requireFromHere = createRequire(import.meta.url);\n\nexport const WASM_DETECTOR_RUNTIME_UNAVAILABLE_MESSAGE =\n \"WASM detector runtime is unavailable. Run `bun run build:wasm` to generate it.\";\n\ntype WhatlangWasmModule = {\n detect_language: (text: string, routeTag: string) => WhatlangWasmResult | null;\n};\n\nlet modulePromise: Promise<WhatlangWasmModule> | null = null;\n\nfunction resolveCandidateModulePaths(): string[] {\n const moduleDir = dirname(fileURLToPath(import.meta.url));\n const candidates = new Set<string>();\n let currentDir = moduleDir;\n\n for (let depth = 0; depth < MAX_SEARCH_DEPTH; depth += 1) {\n candidates.add(join(currentDir, GENERATED_FOLDER_NAME, GENERATED_MODULE_FILE));\n candidates.add(join(currentDir, \"generated\", GENERATED_FOLDER_NAME, GENERATED_MODULE_FILE));\n\n const parentDir = dirname(currentDir);\n if (parentDir === currentDir) {\n break;\n }\n currentDir = parentDir;\n }\n\n return [...candidates];\n}\n\nfunction resolveWhatlangWasmModulePath(): string {\n for (const candidate of resolveCandidateModulePaths()) {\n if (existsSync(candidate)) {\n return candidate;\n }\n }\n\n throw new Error(WASM_DETECTOR_RUNTIME_UNAVAILABLE_MESSAGE);\n}\n\nasync function loadWhatlangWasmModule(): Promise<WhatlangWasmModule> {\n if (!modulePromise) {\n modulePromise = (async () => {\n const modulePath = resolveWhatlangWasmModulePath();\n return requireFromHere(modulePath) as WhatlangWasmModule;\n })();\n }\n\n return modulePromise;\n}\n\nexport async function detectWithWhatlangWasm(\n text: string,\n routeTag: DetectorRouteTag,\n): Promise<WhatlangWasmResult | null> {\n const wasmModule = await loadWhatlangWasmModule();\n return wasmModule.detect_language(text, routeTag);\n}\n","import { DEFAULT_HAN_TAG, DEFAULT_LOCALE } from \"../wc/locale-detect\";\nimport type { DetectorRouteTag } from \"./policy\";\nimport type { DetectorResult } from \"./types\";\n\nexport interface WhatlangWasmResult {\n lang: string;\n script: string;\n confidence: number;\n reliable: boolean;\n}\n\nconst LATIN_LANGUAGE_TAGS: Record<string, string> = {\n cat: \"ca\",\n ces: \"cs\",\n dan: \"da\",\n deu: \"de\",\n eng: \"en\",\n fin: \"fi\",\n fra: \"fr\",\n hun: \"hu\",\n ita: \"it\",\n lat: \"la\",\n nld: \"nl\",\n pol: \"pl\",\n por: \"pt\",\n ron: \"ro\",\n spa: \"es\",\n swe: \"sv\",\n tur: \"tr\",\n};\n\nconst HANI_LANGUAGE_TAGS: Record<string, string> = {\n cmn: \"zh\",\n jpn: \"ja\",\n};\n\nfunction hasSupportedScript(result: WhatlangWasmResult, routeTag: DetectorRouteTag): boolean {\n if (routeTag === DEFAULT_LOCALE) {\n return result.script === \"Latin\";\n }\n\n return result.script === \"Mandarin\";\n}\n\nfunction remapLanguageTag(\n lang: string,\n routeTag: DetectorRouteTag,\n): string | undefined {\n if (routeTag === DEFAULT_LOCALE) {\n return LATIN_LANGUAGE_TAGS[lang];\n }\n\n return HANI_LANGUAGE_TAGS[lang];\n}\n\nexport function remapWhatlangResult(\n result: WhatlangWasmResult,\n routeTag: DetectorRouteTag,\n): DetectorResult | null {\n if (!hasSupportedScript(result, routeTag)) {\n return null;\n }\n\n const tag = remapLanguageTag(result.lang, routeTag);\n if (!tag) {\n return null;\n }\n\n return {\n tag,\n confidence: result.confidence,\n reliable: result.reliable,\n source: \"wasm\",\n };\n}\n\nexport function getDetectorFallbackTag(routeTag: DetectorRouteTag): string {\n return routeTag === DEFAULT_HAN_TAG ? DEFAULT_HAN_TAG : DEFAULT_LOCALE;\n}\n","import { DEFAULT_HAN_TAG, DEFAULT_LOCALE } from \"../wc/locale-detect\";\nimport { segmentTextByLocale } from \"../wc\";\nimport type { LocaleChunk } from \"../wc/types\";\nimport { buildWordCounterResultFromChunks } from \"./result-builder\";\nimport { countSectionsWithResolvedDetector } from \"./sections\";\nimport {\n DETECTOR_ROUTE_POLICIES,\n LATIN_WASM_CORROBORATED_MIN_CONFIDENCE,\n isAmbiguousDetectorRoute,\n normalizeDetectorSampleForRoute,\n shouldRunWasmDetector,\n type DetectorRouteTag,\n} from \"./policy\";\nimport { detectWithWhatlangWasm, WASM_DETECTOR_RUNTIME_UNAVAILABLE_MESSAGE } from \"./whatlang-wasm\";\nimport { getDetectorFallbackTag, remapWhatlangResult } from \"./whatlang-map\";\nimport type {\n DetectorCountSectionsOptions,\n DetectorLocaleOptions,\n DetectorWordCounterOptions,\n} from \"./types\";\n\nfunction shouldAcceptDetectorTag(\n routeTag: DetectorRouteTag,\n confidence: number | undefined,\n reliable: boolean | undefined,\n): boolean {\n const policy = DETECTOR_ROUTE_POLICIES[routeTag];\n if (policy.requireReliable && reliable !== true) {\n return false;\n }\n\n if (confidence === undefined) {\n return false;\n }\n\n return confidence >= policy.minConfidence;\n}\n\ntype DetectorWindow = {\n routeTag: DetectorRouteTag;\n startIndex: number;\n endIndex: number;\n text: string;\n};\n\nfunction buildDetectorWindows(chunks: LocaleChunk[]): DetectorWindow[] {\n const windows: DetectorWindow[] = [];\n\n for (let index = 0; index < chunks.length; index += 1) {\n const chunk = chunks[index];\n if (!chunk || !isAmbiguousDetectorRoute(chunk.locale)) {\n continue;\n }\n\n const previousWindow = windows[windows.length - 1];\n if (\n previousWindow &&\n previousWindow.routeTag === chunk.locale &&\n previousWindow.endIndex === index - 1\n ) {\n previousWindow.endIndex = index;\n previousWindow.text += chunk.text;\n continue;\n }\n\n windows.push({\n routeTag: chunk.locale,\n startIndex: index,\n endIndex: index,\n text: chunk.text,\n });\n }\n\n return windows;\n}\n\nasync function resolveWindowLocale(window: DetectorWindow): Promise<string> {\n if (!shouldRunWasmDetector(window.text, window.routeTag)) {\n return window.routeTag;\n }\n\n const rawResult = await detectWithWhatlangWasm(window.text, window.routeTag);\n const rawRemapped = rawResult ? remapWhatlangResult(rawResult, window.routeTag) : null;\n\n const normalizedSample = normalizeDetectorSampleForRoute(window.text, window.routeTag);\n const normalizedResult =\n normalizedSample.length > 0 && normalizedSample !== window.text\n ? await detectWithWhatlangWasm(normalizedSample, window.routeTag)\n : null;\n const normalizedRemapped = normalizedResult\n ? remapWhatlangResult(normalizedResult, window.routeTag)\n : null;\n\n const candidates = [rawRemapped, normalizedRemapped].filter((value) => value !== null);\n if (candidates.length === 0) {\n return getDetectorFallbackTag(window.routeTag);\n }\n\n const strongestCandidate = candidates.reduce((best, current) => {\n if (!best) {\n return current;\n }\n return (current.confidence ?? 0) > (best.confidence ?? 0) ? current : best;\n }, candidates[0]);\n\n if (\n strongestCandidate &&\n shouldAcceptDetectorTag(\n window.routeTag,\n strongestCandidate.confidence,\n strongestCandidate.reliable,\n )\n ) {\n return strongestCandidate.tag;\n }\n\n if (\n window.routeTag === DEFAULT_LOCALE &&\n rawRemapped &&\n normalizedRemapped &&\n rawRemapped.tag === normalizedRemapped.tag\n ) {\n const corroboratedConfidence = Math.max(\n rawRemapped.confidence ?? 0,\n normalizedRemapped.confidence ?? 0,\n );\n if (corroboratedConfidence >= LATIN_WASM_CORROBORATED_MIN_CONFIDENCE) {\n return rawRemapped.tag;\n }\n }\n\n return getDetectorFallbackTag(window.routeTag);\n}\n\nexport { WASM_DETECTOR_RUNTIME_UNAVAILABLE_MESSAGE };\n\nexport async function segmentTextByLocaleWithWasmDetector(\n text: string,\n options: DetectorLocaleOptions = {},\n) {\n const chunks = segmentTextByLocale(text, options);\n const resolved = [...chunks];\n const windows = buildDetectorWindows(chunks);\n\n for (const window of windows) {\n const resolvedLocale = await resolveWindowLocale(window);\n for (let index = window.startIndex; index <= window.endIndex; index += 1) {\n const chunk = resolved[index];\n if (!chunk) {\n continue;\n }\n resolved[index] = {\n ...chunk,\n locale: resolvedLocale,\n };\n }\n }\n\n return resolved;\n}\n\nexport async function wordCounterWithWasmDetector(\n text: string,\n options: DetectorWordCounterOptions = {},\n) {\n const chunks = await segmentTextByLocaleWithWasmDetector(text, options);\n return buildWordCounterResultFromChunks(chunks, options);\n}\n\nexport async function countSectionsWithWasmDetector(\n input: string,\n section: Parameters<typeof countSectionsWithResolvedDetector>[1],\n options: DetectorCountSectionsOptions = {},\n) {\n return countSectionsWithResolvedDetector(input, section, options);\n}\n","import type { SectionMode } from \"../markdown\";\nimport type { LocaleChunk } from \"../wc/types\";\nimport {\n countSectionsWithRegexDetector,\n segmentTextByLocaleWithRegexDetector,\n wordCounterWithRegexDetector,\n} from \"./none\";\nimport {\n countSectionsWithWasmDetector,\n segmentTextByLocaleWithWasmDetector,\n WASM_DETECTOR_RUNTIME_UNAVAILABLE_MESSAGE,\n wordCounterWithWasmDetector,\n} from \"./wasm\";\nimport type {\n DetectorCountSectionsOptions,\n DetectorLocaleOptions,\n DetectorMode,\n DetectorResult,\n DetectorSource,\n DetectorWordCounterOptions,\n} from \"./types\";\n\nexport type {\n DetectorCountSections,\n DetectorCountSectionsOptions,\n DetectorCountResult,\n DetectorLocaleOptions,\n DetectorMode,\n DetectorResult,\n DetectorRuntimeOptions,\n DetectorSource,\n DetectorWordCounterOptions,\n} from \"./types\";\n\nexport const DETECTOR_MODES: DetectorMode[] = [\"regex\", \"wasm\"];\nexport const DEFAULT_DETECTOR_MODE: DetectorMode = \"regex\";\n\nexport function resolveDetectorMode(mode?: DetectorMode): DetectorMode {\n return mode ?? DEFAULT_DETECTOR_MODE;\n}\n\nexport function assertDetectorModeImplemented(mode?: DetectorMode): void {\n void mode;\n}\n\nexport async function segmentTextByLocaleWithDetector(\n text: string,\n options: DetectorLocaleOptions = {},\n): Promise<LocaleChunk[]> {\n const mode = resolveDetectorMode(options.detector);\n if (mode === \"wasm\") {\n return segmentTextByLocaleWithWasmDetector(text, options);\n }\n return segmentTextByLocaleWithRegexDetector(text, options);\n}\n\nexport async function wordCounterWithDetector(\n text: string,\n options: DetectorWordCounterOptions = {},\n) {\n const mode = resolveDetectorMode(options.detector);\n if (mode === \"wasm\") {\n return wordCounterWithWasmDetector(text, options);\n }\n return wordCounterWithRegexDetector(text, options);\n}\n\nexport async function countSectionsWithDetector(\n input: string,\n section: SectionMode,\n options: DetectorCountSectionsOptions = {},\n) {\n const mode = resolveDetectorMode(options.detector);\n if (mode === \"wasm\") {\n return countSectionsWithWasmDetector(input, section, options);\n }\n return countSectionsWithRegexDetector(input, section, options);\n}\n\nexport const DETECTOR_SOURCES: DetectorSource[] = [\"script\", \"hint\", \"wasm\"];\nexport const DEFAULT_DETECTOR_RESULT_SOURCE: DetectorSource = \"script\";\nexport { WASM_DETECTOR_RUNTIME_UNAVAILABLE_MESSAGE };\n\nexport function createDetectorResult(\n tag: string,\n source: DetectorSource = DEFAULT_DETECTOR_RESULT_SOURCE,\n confidence?: number,\n reliable?: boolean,\n): DetectorResult {\n return {\n tag,\n source,\n ...(confidence === undefined ? {} : { confidence }),\n ...(reliable === undefined ? {} : { reliable }),\n };\n}\n","import {\n assertDetectorModeImplemented,\n countSectionsWithDetector,\n createDetectorResult,\n DEFAULT_DETECTOR_MODE,\n DEFAULT_DETECTOR_RESULT_SOURCE,\n DETECTOR_MODES,\n DETECTOR_SOURCES,\n resolveDetectorMode,\n segmentTextByLocaleWithDetector,\n WASM_DETECTOR_RUNTIME_UNAVAILABLE_MESSAGE,\n wordCounterWithDetector,\n} from \"./index\";\n\nconst cjsExports = {\n assertDetectorModeImplemented,\n countSectionsWithDetector,\n createDetectorResult,\n DEFAULT_DETECTOR_MODE,\n DEFAULT_DETECTOR_RESULT_SOURCE,\n DETECTOR_MODES,\n DETECTOR_SOURCES,\n resolveDetectorMode,\n segmentTextByLocaleWithDetector,\n WASM_DETECTOR_RUNTIME_UNAVAILABLE_MESSAGE,\n wordCounterWithDetector,\n};\n\nexport = cjsExports;\n"],"mappings":"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"}
|