@dev-pi2pie/word-counter 0.1.0-canary.5 → 0.1.1
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 +42 -5
- package/dist/cjs/index.cjs +97 -15
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/bin.mjs +2317 -2009
- package/dist/esm/bin.mjs.map +1 -1
- package/dist/esm/index.d.mts +9 -1
- package/dist/esm/index.mjs +97 -15
- package/dist/esm/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/esm/bin.mjs
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import { Command, Option } from "commander";
|
|
4
4
|
import { closeSync, createWriteStream, existsSync, mkdirSync, openSync, readFileSync, statSync } from "node:fs";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { readFile, readdir, stat } from "node:fs/promises";
|
|
8
|
+
import { parseDocument } from "yaml";
|
|
8
9
|
|
|
9
10
|
//#region \0rolldown/runtime.js
|
|
10
11
|
var __create = Object.create;
|
|
@@ -34,1961 +35,2229 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
34
35
|
}) : target, mod));
|
|
35
36
|
|
|
36
37
|
//#endregion
|
|
37
|
-
//#region src/
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const list = [];
|
|
42
|
-
result[key] = list;
|
|
43
|
-
return list;
|
|
38
|
+
//#region src/cli/debug/channel.ts
|
|
39
|
+
const NOOP_CLOSE = async () => {};
|
|
40
|
+
function shouldEmitAtVerbosity(channelVerbosity, eventVerbosity) {
|
|
41
|
+
return channelVerbosity === "verbose" || eventVerbosity === "compact";
|
|
44
42
|
}
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
function formatTimestampPart(value) {
|
|
44
|
+
return String(value).padStart(2, "0");
|
|
45
|
+
}
|
|
46
|
+
function formatDebugReportTimestamp(now) {
|
|
47
|
+
return [`${now.getFullYear()}${formatTimestampPart(now.getMonth() + 1)}${formatTimestampPart(now.getDate())}`, `${formatTimestampPart(now.getHours())}${formatTimestampPart(now.getMinutes())}${formatTimestampPart(now.getSeconds())}`].join("-");
|
|
48
|
+
}
|
|
49
|
+
function withCollisionSuffix(pathValue, sequence) {
|
|
50
|
+
if (sequence <= 0) return pathValue;
|
|
51
|
+
const extension = extname(pathValue);
|
|
52
|
+
const baseName = basename(pathValue, extension);
|
|
53
|
+
return join(dirname(pathValue), `${baseName}-${sequence}${extension}`);
|
|
54
|
+
}
|
|
55
|
+
function resolveReportPath(report, now, pid) {
|
|
56
|
+
const cwd = report.cwd ?? process.cwd();
|
|
57
|
+
const defaultName = `wc-debug-${formatDebugReportTimestamp(now)}-${pid}.jsonl`;
|
|
58
|
+
const explicitPath = typeof report.path === "string";
|
|
59
|
+
const basePath = explicitPath ? resolve(cwd, report.path) : resolve(cwd, defaultName);
|
|
60
|
+
mkdirSync(dirname(basePath), { recursive: true });
|
|
61
|
+
if (explicitPath) {
|
|
62
|
+
if (existsSync(basePath) && statSync(basePath).isDirectory()) throw new Error(`debug report path must be a file: ${basePath}`);
|
|
63
|
+
return basePath;
|
|
64
|
+
}
|
|
65
|
+
let candidate = basePath;
|
|
66
|
+
let sequence = 0;
|
|
67
|
+
while (existsSync(candidate)) {
|
|
68
|
+
sequence += 1;
|
|
69
|
+
candidate = withCollisionSuffix(basePath, sequence);
|
|
70
|
+
}
|
|
71
|
+
return candidate;
|
|
72
|
+
}
|
|
73
|
+
function createTerminalSink() {
|
|
74
|
+
return {
|
|
75
|
+
write(line) {
|
|
76
|
+
console.error(`[debug] ${line}`);
|
|
77
|
+
},
|
|
78
|
+
close: NOOP_CLOSE
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function createFileSink(pathValue) {
|
|
82
|
+
try {
|
|
83
|
+
closeSync(openSync(pathValue, "a"));
|
|
84
|
+
} catch (error) {
|
|
85
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
86
|
+
throw new Error(`debug report path is not writable: ${pathValue} (${message})`);
|
|
49
87
|
}
|
|
88
|
+
const stream = createWriteStream(pathValue, {
|
|
89
|
+
flags: "a",
|
|
90
|
+
encoding: "utf8"
|
|
91
|
+
});
|
|
92
|
+
let streamError;
|
|
93
|
+
stream.on("error", (error) => {
|
|
94
|
+
streamError = error;
|
|
95
|
+
});
|
|
96
|
+
return {
|
|
97
|
+
write(line) {
|
|
98
|
+
if (streamError || stream.destroyed) return;
|
|
99
|
+
stream.write(`${line}\n`);
|
|
100
|
+
},
|
|
101
|
+
close() {
|
|
102
|
+
if (streamError || stream.destroyed || stream.writableEnded) return Promise.resolve();
|
|
103
|
+
return new Promise((resolve) => {
|
|
104
|
+
stream.end(() => {
|
|
105
|
+
resolve();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function createDebugChannel(options) {
|
|
112
|
+
if (!options.enabled) return {
|
|
113
|
+
enabled: false,
|
|
114
|
+
verbosity: options.verbosity ?? "compact",
|
|
115
|
+
emit() {},
|
|
116
|
+
close: NOOP_CLOSE
|
|
117
|
+
};
|
|
118
|
+
const verbosity = options.verbosity ?? "compact";
|
|
119
|
+
const sinks = [];
|
|
120
|
+
let reportPath;
|
|
121
|
+
if (options.report) {
|
|
122
|
+
const now = options.now?.() ?? /* @__PURE__ */ new Date();
|
|
123
|
+
const pid = options.pid ?? process.pid;
|
|
124
|
+
reportPath = resolveReportPath(options.report, now, pid);
|
|
125
|
+
sinks.push(createFileSink(reportPath));
|
|
126
|
+
if (options.report.tee) sinks.push(createTerminalSink());
|
|
127
|
+
} else sinks.push(createTerminalSink());
|
|
128
|
+
return {
|
|
129
|
+
enabled: true,
|
|
130
|
+
verbosity,
|
|
131
|
+
reportPath,
|
|
132
|
+
emit(event, details = {}, eventOptions = {}) {
|
|
133
|
+
if (!shouldEmitAtVerbosity(verbosity, eventOptions.verbosity ?? "compact")) return;
|
|
134
|
+
const payload = JSON.stringify({
|
|
135
|
+
event,
|
|
136
|
+
...details
|
|
137
|
+
});
|
|
138
|
+
for (const sink of sinks) sink.write(payload);
|
|
139
|
+
},
|
|
140
|
+
async close() {
|
|
141
|
+
for (const sink of sinks) await sink.close();
|
|
142
|
+
}
|
|
143
|
+
};
|
|
50
144
|
}
|
|
51
145
|
|
|
52
146
|
//#endregion
|
|
53
|
-
//#region src/
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
147
|
+
//#region src/cli/path/filter.ts
|
|
148
|
+
const DEFAULT_INCLUDE_EXTENSIONS = new Set([
|
|
149
|
+
".md",
|
|
150
|
+
".markdown",
|
|
151
|
+
".mdx",
|
|
152
|
+
".mdc",
|
|
153
|
+
".txt"
|
|
154
|
+
]);
|
|
155
|
+
function collectExtensionOption(value, previous = []) {
|
|
156
|
+
return [...previous, value];
|
|
58
157
|
}
|
|
59
|
-
function
|
|
60
|
-
const trimmed =
|
|
158
|
+
function normalizeExtensionToken(value) {
|
|
159
|
+
const trimmed = value.trim().toLowerCase();
|
|
61
160
|
if (!trimmed) return null;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
161
|
+
const normalized = trimmed.startsWith(".") ? trimmed : `.${trimmed}`;
|
|
162
|
+
if (normalized === ".") return null;
|
|
163
|
+
return normalized;
|
|
164
|
+
}
|
|
165
|
+
function parseExtensionValues(values) {
|
|
166
|
+
const parsed = /* @__PURE__ */ new Set();
|
|
167
|
+
if (!values || values.length === 0) return parsed;
|
|
168
|
+
for (const value of values) for (const token of value.split(",")) {
|
|
169
|
+
const normalized = normalizeExtensionToken(token);
|
|
170
|
+
if (!normalized) continue;
|
|
171
|
+
parsed.add(normalized);
|
|
65
172
|
}
|
|
66
|
-
|
|
67
|
-
if (segments.some((segment) => !segment)) return null;
|
|
68
|
-
return segments.join(".");
|
|
173
|
+
return parsed;
|
|
69
174
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (inString) {
|
|
79
|
-
if (escaped) {
|
|
80
|
-
escaped = false;
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
if (char === "\\" && inString === "double") {
|
|
84
|
-
escaped = true;
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
if (inString === "double" && char === "\"") {
|
|
88
|
-
inString = null;
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
if (inString === "single" && char === "'") {
|
|
92
|
-
inString = null;
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
if (char === "\"") {
|
|
98
|
-
inString = "double";
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
if (char === "'") {
|
|
102
|
-
inString = "single";
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
if (char === "#") return line.slice(0, i).trimEnd();
|
|
175
|
+
function buildDirectoryExtensionFilter(includeValues, excludeValues) {
|
|
176
|
+
const includeFromFlags = parseExtensionValues(includeValues);
|
|
177
|
+
const excludeExtensions = parseExtensionValues(excludeValues);
|
|
178
|
+
const includeExtensions = includeFromFlags.size > 0 ? includeFromFlags : new Set(DEFAULT_INCLUDE_EXTENSIONS);
|
|
179
|
+
const effectiveIncludeExtensions = /* @__PURE__ */ new Set();
|
|
180
|
+
for (const extension of includeExtensions) {
|
|
181
|
+
if (excludeExtensions.has(extension)) continue;
|
|
182
|
+
effectiveIncludeExtensions.add(extension);
|
|
106
183
|
}
|
|
107
|
-
return
|
|
184
|
+
return {
|
|
185
|
+
includeExtensions,
|
|
186
|
+
excludeExtensions,
|
|
187
|
+
effectiveIncludeExtensions
|
|
188
|
+
};
|
|
108
189
|
}
|
|
109
|
-
function
|
|
110
|
-
|
|
190
|
+
function shouldIncludeFromDirectory(filePath, filter) {
|
|
191
|
+
const extension = extname(filePath).toLowerCase();
|
|
192
|
+
return filter.effectiveIncludeExtensions.has(extension);
|
|
111
193
|
}
|
|
112
|
-
function
|
|
113
|
-
if (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return
|
|
194
|
+
function buildDirectoryRegexFilter(pattern) {
|
|
195
|
+
if (pattern === void 0) return {
|
|
196
|
+
sourcePattern: void 0,
|
|
197
|
+
regex: void 0
|
|
198
|
+
};
|
|
199
|
+
if (pattern.trim().length === 0) return {
|
|
200
|
+
sourcePattern: pattern,
|
|
201
|
+
regex: void 0
|
|
202
|
+
};
|
|
203
|
+
try {
|
|
204
|
+
return {
|
|
205
|
+
sourcePattern: pattern,
|
|
206
|
+
regex: new RegExp(pattern)
|
|
207
|
+
};
|
|
208
|
+
} catch (error) {
|
|
209
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
210
|
+
throw new Error(`Invalid --regex pattern: ${message}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function toDirectoryRelativePath(rootPath, filePath) {
|
|
214
|
+
const relativePath = relative(rootPath, filePath);
|
|
215
|
+
if (sep === "/") return relativePath;
|
|
216
|
+
return relativePath.split(sep).join("/");
|
|
217
|
+
}
|
|
218
|
+
function shouldIncludeFromDirectoryRegex(relativePath, filter) {
|
|
219
|
+
if (!filter.regex) return true;
|
|
220
|
+
return filter.regex.test(relativePath);
|
|
118
221
|
}
|
|
119
222
|
|
|
120
223
|
//#endregion
|
|
121
|
-
//#region src/
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
let current = "";
|
|
140
|
-
let inString = null;
|
|
141
|
-
let escaped = false;
|
|
142
|
-
for (let i = 0; i < inner.length; i += 1) {
|
|
143
|
-
const char = inner[i] ?? "";
|
|
144
|
-
if (inString) {
|
|
145
|
-
current += char;
|
|
146
|
-
if (escaped) {
|
|
147
|
-
escaped = false;
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
if (char === "\\" && inString === "double") {
|
|
151
|
-
escaped = true;
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
if (inString === "double" && char === "\"") inString = null;
|
|
155
|
-
else if (inString === "single" && char === "'") inString = null;
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
|
-
if (char === "\"") {
|
|
159
|
-
inString = "double";
|
|
160
|
-
current += char;
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
if (char === "'") {
|
|
164
|
-
inString = "single";
|
|
165
|
-
current += char;
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
if (char === ",") {
|
|
169
|
-
const item = parsePrimitive(current);
|
|
170
|
-
if (item === null) return null;
|
|
171
|
-
items.push(item);
|
|
172
|
-
current = "";
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
current += char;
|
|
176
|
-
}
|
|
177
|
-
const finalItem = parsePrimitive(current);
|
|
178
|
-
if (finalItem === null) return null;
|
|
179
|
-
items.push(finalItem);
|
|
180
|
-
return items;
|
|
181
|
-
}
|
|
182
|
-
function parseInlineTable(raw) {
|
|
183
|
-
const trimmed = raw.trim();
|
|
184
|
-
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return null;
|
|
185
|
-
const inner = trimmed.slice(1, -1).trim();
|
|
186
|
-
if (!inner) return {};
|
|
187
|
-
const pairs = [];
|
|
188
|
-
let current = "";
|
|
189
|
-
let inString = null;
|
|
190
|
-
let escaped = false;
|
|
191
|
-
let bracketDepth = 0;
|
|
192
|
-
let braceDepth = 0;
|
|
193
|
-
for (let i = 0; i < inner.length; i += 1) {
|
|
194
|
-
const char = inner[i] ?? "";
|
|
195
|
-
if (inString) {
|
|
196
|
-
current += char;
|
|
197
|
-
if (escaped) {
|
|
198
|
-
escaped = false;
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
if (char === "\\" && inString === "double") {
|
|
202
|
-
escaped = true;
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
if (inString === "double" && char === "\"") inString = null;
|
|
206
|
-
else if (inString === "single" && char === "'") inString = null;
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
if (char === "\"") {
|
|
210
|
-
inString = "double";
|
|
211
|
-
current += char;
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
if (char === "'") {
|
|
215
|
-
inString = "single";
|
|
216
|
-
current += char;
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
if (char === "[") {
|
|
220
|
-
bracketDepth += 1;
|
|
221
|
-
current += char;
|
|
222
|
-
continue;
|
|
223
|
-
}
|
|
224
|
-
if (char === "]") {
|
|
225
|
-
if (bracketDepth > 0) bracketDepth -= 1;
|
|
226
|
-
current += char;
|
|
227
|
-
continue;
|
|
228
|
-
}
|
|
229
|
-
if (char === "{") {
|
|
230
|
-
braceDepth += 1;
|
|
231
|
-
current += char;
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
234
|
-
if (char === "}") {
|
|
235
|
-
if (braceDepth > 0) braceDepth -= 1;
|
|
236
|
-
current += char;
|
|
237
|
-
continue;
|
|
238
|
-
}
|
|
239
|
-
if (char === "," && bracketDepth === 0 && braceDepth === 0) {
|
|
240
|
-
pairs.push(current);
|
|
241
|
-
current = "";
|
|
242
|
-
continue;
|
|
243
|
-
}
|
|
244
|
-
current += char;
|
|
245
|
-
}
|
|
246
|
-
if (current.trim()) pairs.push(current);
|
|
247
|
-
const output = {};
|
|
248
|
-
for (const pair of pairs) {
|
|
249
|
-
const separatorIndex = pair.indexOf("=");
|
|
250
|
-
if (separatorIndex === -1) return null;
|
|
251
|
-
const key = normalizeKeyPath(pair.slice(0, separatorIndex));
|
|
252
|
-
if (!key) return null;
|
|
253
|
-
const valueRaw = pair.slice(separatorIndex + 1).trim();
|
|
254
|
-
if (!valueRaw) return null;
|
|
255
|
-
if (valueRaw.startsWith("{")) return null;
|
|
256
|
-
const normalized = normalizeValue(valueRaw);
|
|
257
|
-
if (normalized === null) return null;
|
|
258
|
-
if (typeof normalized === "object" && !Array.isArray(normalized)) return null;
|
|
259
|
-
output[key] = normalized;
|
|
260
|
-
}
|
|
261
|
-
return output;
|
|
262
|
-
}
|
|
263
|
-
function normalizeValue(value) {
|
|
264
|
-
if (!value) return null;
|
|
265
|
-
const trimmed = value.trim();
|
|
266
|
-
if (trimmed.startsWith("{") && trimmed.endsWith("}")) return parseInlineTable(trimmed);
|
|
267
|
-
const array = parseArray(trimmed);
|
|
268
|
-
if (array) return array;
|
|
269
|
-
if (trimmed.startsWith("[") && trimmed.endsWith("]")) return null;
|
|
270
|
-
return parsePrimitive(trimmed);
|
|
271
|
-
}
|
|
272
|
-
function toPlainText(value) {
|
|
273
|
-
if (value == null) return "";
|
|
274
|
-
if (Array.isArray(value)) return value.map((item) => String(item)).join(", ");
|
|
275
|
-
return String(value);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
//#endregion
|
|
279
|
-
//#region src/markdown/toml/parse-frontmatter.ts
|
|
280
|
-
function parseTomlFrontmatter(frontmatter) {
|
|
281
|
-
const result = {};
|
|
282
|
-
const lines = frontmatter.split("\n");
|
|
283
|
-
let tablePrefix = "";
|
|
284
|
-
let tableTarget = null;
|
|
285
|
-
let tablePrefixInList = false;
|
|
286
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
287
|
-
const rawLine = lines[index] ?? "";
|
|
288
|
-
const trimmedLine = rawLine.trim();
|
|
289
|
-
if (!trimmedLine || trimmedLine.startsWith("#")) continue;
|
|
290
|
-
if (trimmedLine.startsWith("[[")) {
|
|
291
|
-
const match = trimmedLine.match(/^\[\[([^\]]+)]]$/);
|
|
292
|
-
if (!match) return null;
|
|
293
|
-
const normalizedTable = normalizeKeyPath(match[1] ?? "");
|
|
294
|
-
if (!normalizedTable) return null;
|
|
295
|
-
const list = ensureArrayContainer(result, normalizedTable);
|
|
296
|
-
const newEntry = {};
|
|
297
|
-
list.push(newEntry);
|
|
298
|
-
tableTarget = newEntry;
|
|
299
|
-
tablePrefix = normalizedTable;
|
|
300
|
-
tablePrefixInList = true;
|
|
301
|
-
continue;
|
|
302
|
-
}
|
|
303
|
-
const tableMatch = trimmedLine.match(/^\[([^\]]+)]$/);
|
|
304
|
-
if (tableMatch) {
|
|
305
|
-
const normalizedTable = normalizeKeyPath(tableMatch[1] ?? "");
|
|
306
|
-
if (!normalizedTable) return null;
|
|
307
|
-
tablePrefix = normalizedTable;
|
|
308
|
-
tablePrefixInList = false;
|
|
309
|
-
tableTarget = null;
|
|
310
|
-
continue;
|
|
311
|
-
}
|
|
312
|
-
const lineForParsing = /("""|''')/.test(rawLine) ? rawLine : stripInlineComment(rawLine);
|
|
313
|
-
const separatorIndex = lineForParsing.indexOf("=");
|
|
314
|
-
if (separatorIndex === -1) return null;
|
|
315
|
-
const key = normalizeKeyPath(lineForParsing.slice(0, separatorIndex));
|
|
316
|
-
let valueRaw = lineForParsing.slice(separatorIndex + 1).trim();
|
|
317
|
-
if (!key) return null;
|
|
318
|
-
const tripleDelimiter = valueRaw.startsWith("\"\"\"") ? "\"\"\"" : valueRaw.startsWith("'''") ? "'''" : null;
|
|
319
|
-
if (tripleDelimiter) {
|
|
320
|
-
const closingIndex = valueRaw.indexOf(tripleDelimiter, tripleDelimiter.length);
|
|
321
|
-
if (closingIndex !== -1) {
|
|
322
|
-
const strippedAfter = stripInlineComment(valueRaw.slice(closingIndex + tripleDelimiter.length));
|
|
323
|
-
valueRaw = `${valueRaw.slice(0, closingIndex + tripleDelimiter.length)}${strippedAfter}`;
|
|
324
|
-
} else {
|
|
325
|
-
const delimiter = tripleDelimiter;
|
|
326
|
-
let combined = valueRaw;
|
|
327
|
-
let closed = false;
|
|
328
|
-
while (index + 1 < lines.length) {
|
|
329
|
-
index += 1;
|
|
330
|
-
const nextLine = lines[index] ?? "";
|
|
331
|
-
combined += `\n${nextLine}`;
|
|
332
|
-
if (new RegExp(`${delimiter}\\s*$`).test(nextLine)) {
|
|
333
|
-
closed = true;
|
|
334
|
-
break;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
if (!closed) return null;
|
|
338
|
-
valueRaw = combined;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
const normalized = normalizeValue(valueRaw);
|
|
342
|
-
if (normalized === null) return null;
|
|
343
|
-
const fullKey = tablePrefix ? `${tablePrefix}.${key}` : key;
|
|
344
|
-
if (typeof normalized === "object" && !Array.isArray(normalized)) {
|
|
345
|
-
for (const [inlineKey, inlineValue] of Object.entries(normalized)) {
|
|
346
|
-
const entryKey = tablePrefixInList ? `${key}.${inlineKey}` : `${fullKey}.${inlineKey}`;
|
|
347
|
-
if (tablePrefixInList && tableTarget) tableTarget[entryKey] = toPlainText(inlineValue);
|
|
348
|
-
else result[entryKey] = toPlainText(inlineValue);
|
|
349
|
-
}
|
|
350
|
-
continue;
|
|
351
|
-
}
|
|
352
|
-
if (tablePrefixInList && tableTarget) {
|
|
353
|
-
tableTarget[key] = toPlainText(normalized);
|
|
354
|
-
continue;
|
|
355
|
-
}
|
|
356
|
-
result[fullKey] = toPlainText(normalized);
|
|
357
|
-
}
|
|
358
|
-
flattenArrayTables(result);
|
|
359
|
-
return result;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
//#endregion
|
|
363
|
-
//#region src/markdown/parse-markdown.ts
|
|
364
|
-
const FENCE_TO_TYPE = {
|
|
365
|
-
"---": "yaml",
|
|
366
|
-
"+++": "toml",
|
|
367
|
-
";;;": "json"
|
|
224
|
+
//#region src/cli/total-of.ts
|
|
225
|
+
const TOTAL_OF_PARTS = [
|
|
226
|
+
"words",
|
|
227
|
+
"emoji",
|
|
228
|
+
"symbols",
|
|
229
|
+
"punctuation",
|
|
230
|
+
"whitespace"
|
|
231
|
+
];
|
|
232
|
+
const TOTAL_OF_PART_ALIASES = {
|
|
233
|
+
word: "words",
|
|
234
|
+
words: "words",
|
|
235
|
+
emoji: "emoji",
|
|
236
|
+
emojis: "emoji",
|
|
237
|
+
symbol: "symbols",
|
|
238
|
+
symbols: "symbols",
|
|
239
|
+
punction: "punctuation",
|
|
240
|
+
punctuation: "punctuation",
|
|
241
|
+
whitespace: "whitespace"
|
|
368
242
|
};
|
|
369
|
-
function
|
|
370
|
-
return input.replace(/\r\n/g, "\n");
|
|
371
|
-
}
|
|
372
|
-
function stripBom(line) {
|
|
373
|
-
return line.startsWith("") ? line.slice(1) : line;
|
|
374
|
-
}
|
|
375
|
-
function getFenceType(line) {
|
|
376
|
-
const match = line.match(/^[\t ]*(---|\+\+\+|;;;)[\t ]*$/);
|
|
377
|
-
if (!match) return null;
|
|
378
|
-
return FENCE_TO_TYPE[match[1] ?? ""] ?? null;
|
|
379
|
-
}
|
|
380
|
-
function parseFrontmatter(frontmatter, type) {
|
|
381
|
-
if (!type) return null;
|
|
382
|
-
if (type === "json") try {
|
|
383
|
-
return JSON.parse(frontmatter);
|
|
384
|
-
} catch {
|
|
385
|
-
return null;
|
|
386
|
-
}
|
|
387
|
-
if (type === "yaml") {
|
|
388
|
-
const doc = parseDocument(frontmatter, { prettyErrors: false });
|
|
389
|
-
if (doc.errors.length > 0) return null;
|
|
390
|
-
const data = doc.toJSON();
|
|
391
|
-
if (!data || typeof data !== "object" || Array.isArray(data)) return null;
|
|
392
|
-
return data;
|
|
393
|
-
}
|
|
394
|
-
if (type === "toml") return parseTomlFrontmatter(frontmatter);
|
|
395
|
-
return null;
|
|
396
|
-
}
|
|
397
|
-
function extractJsonBlock(text, startIndex) {
|
|
398
|
-
let depth = 0;
|
|
399
|
-
let inString = false;
|
|
400
|
-
let escaped = false;
|
|
401
|
-
for (let i = startIndex; i < text.length; i += 1) {
|
|
402
|
-
const char = text[i] ?? "";
|
|
403
|
-
if (inString) {
|
|
404
|
-
if (escaped) {
|
|
405
|
-
escaped = false;
|
|
406
|
-
continue;
|
|
407
|
-
}
|
|
408
|
-
if (char === "\\") {
|
|
409
|
-
escaped = true;
|
|
410
|
-
continue;
|
|
411
|
-
}
|
|
412
|
-
if (char === "\"") inString = false;
|
|
413
|
-
continue;
|
|
414
|
-
}
|
|
415
|
-
if (char === "\"") {
|
|
416
|
-
inString = true;
|
|
417
|
-
continue;
|
|
418
|
-
}
|
|
419
|
-
if (char === "{") {
|
|
420
|
-
depth += 1;
|
|
421
|
-
continue;
|
|
422
|
-
}
|
|
423
|
-
if (char === "}") {
|
|
424
|
-
depth -= 1;
|
|
425
|
-
if (depth === 0) return {
|
|
426
|
-
jsonText: text.slice(startIndex, i + 1),
|
|
427
|
-
endIndex: i
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
return null;
|
|
432
|
-
}
|
|
433
|
-
function parseMarkdown(input) {
|
|
434
|
-
const normalized = normalizeNewlines(input);
|
|
435
|
-
const lines = normalized.split("\n");
|
|
436
|
-
if (lines.length === 0) return {
|
|
437
|
-
frontmatter: null,
|
|
438
|
-
content: normalized,
|
|
439
|
-
data: null,
|
|
440
|
-
frontmatterType: null
|
|
441
|
-
};
|
|
442
|
-
lines[0] = stripBom(lines[0] ?? "");
|
|
443
|
-
const normalizedWithoutBom = lines.join("\n");
|
|
444
|
-
const openingType = getFenceType(lines[0] ?? "");
|
|
445
|
-
if (!openingType) {
|
|
446
|
-
const jsonStart = (normalizedWithoutBom.match(/^[\t \n]*/)?.[0] ?? "").length;
|
|
447
|
-
if (normalizedWithoutBom[jsonStart] !== "{") return {
|
|
448
|
-
frontmatter: null,
|
|
449
|
-
content: normalizedWithoutBom,
|
|
450
|
-
data: null,
|
|
451
|
-
frontmatterType: null
|
|
452
|
-
};
|
|
453
|
-
const jsonBlock = extractJsonBlock(normalizedWithoutBom, jsonStart);
|
|
454
|
-
if (!jsonBlock) return {
|
|
455
|
-
frontmatter: null,
|
|
456
|
-
content: normalizedWithoutBom,
|
|
457
|
-
data: null,
|
|
458
|
-
frontmatterType: null
|
|
459
|
-
};
|
|
460
|
-
const frontmatter = jsonBlock.jsonText;
|
|
461
|
-
let content = normalizedWithoutBom.slice(jsonBlock.endIndex + 1);
|
|
462
|
-
if (content.startsWith("\n")) content = content.slice(1);
|
|
463
|
-
const data = parseFrontmatter(frontmatter, "json");
|
|
464
|
-
if (!data) return {
|
|
465
|
-
frontmatter: null,
|
|
466
|
-
content: normalizedWithoutBom,
|
|
467
|
-
data: null,
|
|
468
|
-
frontmatterType: null
|
|
469
|
-
};
|
|
470
|
-
return {
|
|
471
|
-
frontmatter,
|
|
472
|
-
content,
|
|
473
|
-
data,
|
|
474
|
-
frontmatterType: "json"
|
|
475
|
-
};
|
|
476
|
-
}
|
|
477
|
-
let closingIndex = -1;
|
|
478
|
-
for (let i = 1; i < lines.length; i += 1) if (getFenceType(lines[i] ?? "") === openingType) {
|
|
479
|
-
closingIndex = i;
|
|
480
|
-
break;
|
|
481
|
-
}
|
|
482
|
-
if (closingIndex === -1) return {
|
|
483
|
-
frontmatter: null,
|
|
484
|
-
content: normalizedWithoutBom,
|
|
485
|
-
data: null,
|
|
486
|
-
frontmatterType: null
|
|
487
|
-
};
|
|
488
|
-
const frontmatter = lines.slice(1, closingIndex).join("\n");
|
|
243
|
+
function createTotalOfCounts() {
|
|
489
244
|
return {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
245
|
+
words: 0,
|
|
246
|
+
emoji: 0,
|
|
247
|
+
symbols: 0,
|
|
248
|
+
punctuation: 0,
|
|
249
|
+
whitespace: 0
|
|
494
250
|
};
|
|
495
251
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
const cached = segmenterCache.get(locale);
|
|
503
|
-
if (cached) return cached;
|
|
504
|
-
const segmenter = new Intl.Segmenter(locale, { granularity: "word" });
|
|
505
|
-
segmenterCache.set(locale, segmenter);
|
|
506
|
-
return segmenter;
|
|
507
|
-
}
|
|
508
|
-
function getGraphemeSegmenter(locale) {
|
|
509
|
-
const cached = graphemeSegmenterCache.get(locale);
|
|
510
|
-
if (cached) return cached;
|
|
511
|
-
const segmenter = new Intl.Segmenter(locale, { granularity: "grapheme" });
|
|
512
|
-
graphemeSegmenterCache.set(locale, segmenter);
|
|
513
|
-
return segmenter;
|
|
514
|
-
}
|
|
515
|
-
function supportsSegmenter() {
|
|
516
|
-
return typeof Intl !== "undefined" && typeof Intl.Segmenter === "function";
|
|
517
|
-
}
|
|
518
|
-
function countCharsForLocale(text, locale) {
|
|
519
|
-
if (!supportsSegmenter()) return Array.from(text).length;
|
|
520
|
-
const segmenter = getGraphemeSegmenter(locale);
|
|
521
|
-
let count = 0;
|
|
522
|
-
for (const _segment of segmenter.segment(text)) count++;
|
|
523
|
-
return count;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
//#endregion
|
|
527
|
-
//#region src/utils/append-all.ts
|
|
528
|
-
function appendAll(target, source) {
|
|
529
|
-
for (const item of source) target.push(item);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
//#endregion
|
|
533
|
-
//#region src/wc/non-words.ts
|
|
534
|
-
const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation})/u;
|
|
535
|
-
const emojiPresentationRegex = /\p{Emoji_Presentation}/u;
|
|
536
|
-
const keycapEmojiRegex = /[0-9#*]\uFE0F?\u20E3/u;
|
|
537
|
-
const symbolRegex = /\p{S}/u;
|
|
538
|
-
const punctuationRegex = /\p{P}/u;
|
|
539
|
-
const whitespaceRegex = /\s/u;
|
|
540
|
-
const newlineChars = new Set([
|
|
541
|
-
"\n",
|
|
542
|
-
"\r",
|
|
543
|
-
"\u2028",
|
|
544
|
-
"\u2029"
|
|
545
|
-
]);
|
|
546
|
-
function createNonWordCollection() {
|
|
547
|
-
return {
|
|
548
|
-
emoji: [],
|
|
549
|
-
symbols: [],
|
|
550
|
-
punctuation: [],
|
|
551
|
-
counts: {
|
|
552
|
-
emoji: 0,
|
|
553
|
-
symbols: 0,
|
|
554
|
-
punctuation: 0
|
|
555
|
-
}
|
|
556
|
-
};
|
|
252
|
+
function collectNonWordCounts(target, nonWords) {
|
|
253
|
+
if (!nonWords) return;
|
|
254
|
+
target.emoji += nonWords.counts.emoji;
|
|
255
|
+
target.symbols += nonWords.counts.symbols;
|
|
256
|
+
target.punctuation += nonWords.counts.punctuation;
|
|
257
|
+
target.whitespace += nonWords.counts.whitespace ?? 0;
|
|
557
258
|
}
|
|
558
|
-
function
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
if (category === "symbol") {
|
|
565
|
-
collection.symbols.push(segment);
|
|
566
|
-
collection.counts.symbols += 1;
|
|
567
|
-
return;
|
|
259
|
+
function collectFromWordCounterResult(result) {
|
|
260
|
+
const counts = createTotalOfCounts();
|
|
261
|
+
counts.words += result.counts?.words ?? result.total;
|
|
262
|
+
if (result.breakdown.mode === "collector") {
|
|
263
|
+
collectNonWordCounts(counts, result.breakdown.nonWords);
|
|
264
|
+
return counts;
|
|
568
265
|
}
|
|
569
|
-
|
|
570
|
-
|
|
266
|
+
for (const item of result.breakdown.items) collectNonWordCounts(counts, item.nonWords);
|
|
267
|
+
return counts;
|
|
571
268
|
}
|
|
572
|
-
function
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
for (const
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
whitespace.spaces += 1;
|
|
579
|
-
count += 1;
|
|
580
|
-
continue;
|
|
581
|
-
}
|
|
582
|
-
if (char === " ") {
|
|
583
|
-
whitespace = whitespace ?? createWhitespaceCounts();
|
|
584
|
-
whitespace.tabs += 1;
|
|
585
|
-
count += 1;
|
|
586
|
-
continue;
|
|
587
|
-
}
|
|
588
|
-
if (newlineChars.has(char)) {
|
|
589
|
-
whitespace = whitespace ?? createWhitespaceCounts();
|
|
590
|
-
whitespace.newlines += 1;
|
|
591
|
-
count += 1;
|
|
592
|
-
continue;
|
|
593
|
-
}
|
|
594
|
-
if (whitespaceRegex.test(char)) {
|
|
595
|
-
whitespace = whitespace ?? createWhitespaceCounts();
|
|
596
|
-
whitespace.other += 1;
|
|
597
|
-
count += 1;
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
if (count > 0) {
|
|
601
|
-
collection.whitespace = whitespace ?? createWhitespaceCounts();
|
|
602
|
-
collection.counts.whitespace = (collection.counts.whitespace ?? 0) + count;
|
|
269
|
+
function collectTotalOfCounts(result) {
|
|
270
|
+
if (!("section" in result)) return collectFromWordCounterResult(result);
|
|
271
|
+
const counts = createTotalOfCounts();
|
|
272
|
+
for (const item of result.items) {
|
|
273
|
+
const itemCounts = collectFromWordCounterResult(item.result);
|
|
274
|
+
for (const part of TOTAL_OF_PARTS) counts[part] += itemCounts[part];
|
|
603
275
|
}
|
|
604
|
-
return
|
|
276
|
+
return counts;
|
|
605
277
|
}
|
|
606
|
-
function
|
|
607
|
-
const
|
|
608
|
-
if (
|
|
609
|
-
|
|
610
|
-
if (punctuationRegex.test(segment)) return "punctuation";
|
|
611
|
-
return null;
|
|
278
|
+
function parseTotalOfToken(token) {
|
|
279
|
+
const canonical = TOTAL_OF_PART_ALIASES[token.trim().toLowerCase()];
|
|
280
|
+
if (canonical) return canonical;
|
|
281
|
+
throw new Error(`Invalid --total-of part: ${token}. Allowed: ${TOTAL_OF_PARTS.join(", ")}.`);
|
|
612
282
|
}
|
|
613
|
-
function
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
appendAll(target.punctuation, source.punctuation);
|
|
624
|
-
target.counts.punctuation += source.counts.punctuation;
|
|
625
|
-
}
|
|
626
|
-
if (source.counts.whitespace && source.counts.whitespace > 0 && source.whitespace) {
|
|
627
|
-
const whitespace = target.whitespace ?? createWhitespaceCounts();
|
|
628
|
-
whitespace.spaces += source.whitespace.spaces;
|
|
629
|
-
whitespace.tabs += source.whitespace.tabs;
|
|
630
|
-
whitespace.newlines += source.whitespace.newlines;
|
|
631
|
-
whitespace.other += source.whitespace.other;
|
|
632
|
-
target.whitespace = whitespace;
|
|
633
|
-
target.counts.whitespace = (target.counts.whitespace ?? 0) + source.counts.whitespace;
|
|
283
|
+
function parseTotalOfOption(value) {
|
|
284
|
+
const rawTokens = value.split(",").map((token) => token.trim()).filter((token) => token.length > 0);
|
|
285
|
+
if (rawTokens.length === 0) throw new Error(`Invalid --total-of value: "${value}". Use comma-separated parts from: ${TOTAL_OF_PARTS.join(", ")}.`);
|
|
286
|
+
const parts = [];
|
|
287
|
+
const seen = /* @__PURE__ */ new Set();
|
|
288
|
+
for (const token of rawTokens) {
|
|
289
|
+
const parsed = parseTotalOfToken(token);
|
|
290
|
+
if (seen.has(parsed)) continue;
|
|
291
|
+
seen.add(parsed);
|
|
292
|
+
parts.push(parsed);
|
|
634
293
|
}
|
|
635
|
-
return
|
|
294
|
+
return parts;
|
|
636
295
|
}
|
|
637
|
-
function
|
|
296
|
+
function requiresNonWordCollection(parts) {
|
|
297
|
+
if (!parts || parts.length === 0) return false;
|
|
298
|
+
return parts.some((part) => part !== "words");
|
|
299
|
+
}
|
|
300
|
+
function requiresWhitespaceCollection(parts) {
|
|
301
|
+
if (!parts || parts.length === 0) return false;
|
|
302
|
+
return parts.includes("whitespace");
|
|
303
|
+
}
|
|
304
|
+
function resolveTotalOfOverride(result, parts) {
|
|
305
|
+
if (!parts || parts.length === 0) return;
|
|
306
|
+
const counts = collectTotalOfCounts(result);
|
|
307
|
+
let total = 0;
|
|
308
|
+
for (const part of parts) total += counts[part];
|
|
638
309
|
return {
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
newlines: 0,
|
|
642
|
-
other: 0
|
|
310
|
+
parts: [...parts],
|
|
311
|
+
total
|
|
643
312
|
};
|
|
644
313
|
}
|
|
314
|
+
function formatTotalOfParts(parts) {
|
|
315
|
+
return parts.join(", ");
|
|
316
|
+
}
|
|
645
317
|
|
|
646
318
|
//#endregion
|
|
647
|
-
//#region src/
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
let nonWordChars = 0;
|
|
672
|
-
for (const part of segmenter.segment(chunk.text)) {
|
|
673
|
-
if (part.isWordLike) {
|
|
674
|
-
const count = countCharsForLocale(part.segment, chunk.locale);
|
|
675
|
-
chars += count;
|
|
676
|
-
wordChars += count;
|
|
677
|
-
continue;
|
|
678
|
-
}
|
|
679
|
-
if (collectNonWords && nonWords) {
|
|
680
|
-
let whitespaceCount = 0;
|
|
681
|
-
if (includeWhitespace) whitespaceCount = addWhitespace(nonWords, part.segment);
|
|
682
|
-
const category = classifyNonWordSegment(part.segment);
|
|
683
|
-
if (category) addNonWord(nonWords, category, part.segment);
|
|
684
|
-
if (category || whitespaceCount > 0) {
|
|
685
|
-
const count = countCharsForLocale(part.segment, chunk.locale);
|
|
686
|
-
chars += count;
|
|
687
|
-
nonWordChars += count;
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
return {
|
|
692
|
-
locale: chunk.locale,
|
|
693
|
-
text: chunk.text,
|
|
694
|
-
chars,
|
|
695
|
-
wordChars,
|
|
696
|
-
nonWordChars,
|
|
697
|
-
nonWords: nonWords ?? void 0
|
|
698
|
-
};
|
|
319
|
+
//#region src/cli/program/options.ts
|
|
320
|
+
const MODE_CHOICES = [
|
|
321
|
+
"chunk",
|
|
322
|
+
"segments",
|
|
323
|
+
"collector",
|
|
324
|
+
"char",
|
|
325
|
+
"char-collector"
|
|
326
|
+
];
|
|
327
|
+
const FORMAT_CHOICES = [
|
|
328
|
+
"standard",
|
|
329
|
+
"raw",
|
|
330
|
+
"json"
|
|
331
|
+
];
|
|
332
|
+
const SECTION_CHOICES = [
|
|
333
|
+
"all",
|
|
334
|
+
"split",
|
|
335
|
+
"frontmatter",
|
|
336
|
+
"content",
|
|
337
|
+
"per-key",
|
|
338
|
+
"split-per-key"
|
|
339
|
+
];
|
|
340
|
+
const PATH_MODE_CHOICES = ["auto", "manual"];
|
|
341
|
+
function collectPathValue(value, previous = []) {
|
|
342
|
+
return [...previous, value];
|
|
699
343
|
}
|
|
700
|
-
function
|
|
701
|
-
|
|
702
|
-
const map = /* @__PURE__ */ new Map();
|
|
703
|
-
for (const chunk of chunks) {
|
|
704
|
-
const existing = map.get(chunk.locale);
|
|
705
|
-
if (existing) {
|
|
706
|
-
existing.words += chunk.words;
|
|
707
|
-
appendAll(existing.segments, chunk.segments);
|
|
708
|
-
continue;
|
|
709
|
-
}
|
|
710
|
-
order.push(chunk.locale);
|
|
711
|
-
map.set(chunk.locale, {
|
|
712
|
-
locale: chunk.locale,
|
|
713
|
-
words: chunk.words,
|
|
714
|
-
segments: [...chunk.segments]
|
|
715
|
-
});
|
|
716
|
-
}
|
|
717
|
-
return order.map((locale) => map.get(locale));
|
|
344
|
+
function configureProgramOptions(program, parseMode) {
|
|
345
|
+
program.addOption(new Option("-m, --mode <mode>", "breakdown mode").choices(MODE_CHOICES).argParser(parseMode).default("chunk")).addOption(new Option("-f, --format <format>", "output format").choices(FORMAT_CHOICES).default("standard")).addOption(new Option("--section <section>", "document section mode").choices(SECTION_CHOICES).default("all")).addOption(new Option("--path-mode <mode>", "path resolution mode: auto (default) expands directories; manual treats --path values as literal files").choices(PATH_MODE_CHOICES).default("auto")).option("--latin-language <language>", "hint a language tag for Latin script text").option("--latin-tag <tag>", "hint a BCP 47 tag for Latin script text").option("--latin-locale <locale>", "legacy alias of --latin-language").option("--han-language <language>", "hint a language tag for Han script text").option("--han-tag <tag>", "hint a BCP 47 tag for Han script text").option("--non-words", "collect emoji, symbols, and punctuation (excludes whitespace)").option("--include-whitespace", "include whitespace counts (implies with --non-words; same as --misc)").option("--misc", "collect non-words plus whitespace (alias for --include-whitespace)").option("--total-of <parts>", "override total composition (comma-separated): words,emoji,symbols,punctuation,whitespace", parseTotalOfOption).option("--pretty", "pretty print JSON output", false).option("--debug", "enable debug diagnostics on stderr").option("--verbose", "emit verbose per-file debug diagnostics (requires --debug)").option("--debug-report [path]", "write debug diagnostics to a report file").option("--debug-report-tee", "mirror debug diagnostics to both report file and stderr").option("--debug-tee", "alias of --debug-report-tee").option("--merged", "show merged aggregate output (default)").option("--per-file", "show per-file output plus merged summary").option("--no-progress", "disable batch progress indicator").option("--keep-progress", "keep final batch progress line visible in standard mode").option("--no-recursive", "disable recursive directory traversal").option("--quiet-skips", "hide skip diagnostics (applies when --debug is enabled)").option("--include-ext <exts>", "comma-separated extensions to include during directory scanning", collectExtensionOption, []).option("--exclude-ext <exts>", "comma-separated extensions to exclude during directory scanning", collectExtensionOption, []).option("--regex <pattern>", "regex filter for directory-scanned paths (applies to --path directories only)").option("-p, --path <path>", "read input from file or directory (directories expand in auto mode by default)", collectPathValue, []).argument("[text...]", "text to count").showHelpAfterError();
|
|
718
346
|
}
|
|
719
347
|
|
|
720
348
|
//#endregion
|
|
721
|
-
//#region
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
};
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
349
|
+
//#region node_modules/picocolors/picocolors.js
|
|
350
|
+
var require_picocolors = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
351
|
+
let p = process || {}, argv = p.argv || [], env = p.env || {};
|
|
352
|
+
let isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
|
|
353
|
+
let formatter = (open, close, replace = open) => (input) => {
|
|
354
|
+
let string = "" + input, index = string.indexOf(close, open.length);
|
|
355
|
+
return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
|
|
356
|
+
};
|
|
357
|
+
let replaceClose = (string, close, replace, index) => {
|
|
358
|
+
let result = "", cursor = 0;
|
|
359
|
+
do {
|
|
360
|
+
result += string.substring(cursor, index) + replace;
|
|
361
|
+
cursor = index + close.length;
|
|
362
|
+
index = string.indexOf(close, cursor);
|
|
363
|
+
} while (~index);
|
|
364
|
+
return result + string.substring(cursor);
|
|
365
|
+
};
|
|
366
|
+
let createColors = (enabled = isColorSupported) => {
|
|
367
|
+
let f = enabled ? formatter : () => String;
|
|
368
|
+
return {
|
|
369
|
+
isColorSupported: enabled,
|
|
370
|
+
reset: f("\x1B[0m", "\x1B[0m"),
|
|
371
|
+
bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"),
|
|
372
|
+
dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"),
|
|
373
|
+
italic: f("\x1B[3m", "\x1B[23m"),
|
|
374
|
+
underline: f("\x1B[4m", "\x1B[24m"),
|
|
375
|
+
inverse: f("\x1B[7m", "\x1B[27m"),
|
|
376
|
+
hidden: f("\x1B[8m", "\x1B[28m"),
|
|
377
|
+
strikethrough: f("\x1B[9m", "\x1B[29m"),
|
|
378
|
+
black: f("\x1B[30m", "\x1B[39m"),
|
|
379
|
+
red: f("\x1B[31m", "\x1B[39m"),
|
|
380
|
+
green: f("\x1B[32m", "\x1B[39m"),
|
|
381
|
+
yellow: f("\x1B[33m", "\x1B[39m"),
|
|
382
|
+
blue: f("\x1B[34m", "\x1B[39m"),
|
|
383
|
+
magenta: f("\x1B[35m", "\x1B[39m"),
|
|
384
|
+
cyan: f("\x1B[36m", "\x1B[39m"),
|
|
385
|
+
white: f("\x1B[37m", "\x1B[39m"),
|
|
386
|
+
gray: f("\x1B[90m", "\x1B[39m"),
|
|
387
|
+
bgBlack: f("\x1B[40m", "\x1B[49m"),
|
|
388
|
+
bgRed: f("\x1B[41m", "\x1B[49m"),
|
|
389
|
+
bgGreen: f("\x1B[42m", "\x1B[49m"),
|
|
390
|
+
bgYellow: f("\x1B[43m", "\x1B[49m"),
|
|
391
|
+
bgBlue: f("\x1B[44m", "\x1B[49m"),
|
|
392
|
+
bgMagenta: f("\x1B[45m", "\x1B[49m"),
|
|
393
|
+
bgCyan: f("\x1B[46m", "\x1B[49m"),
|
|
394
|
+
bgWhite: f("\x1B[47m", "\x1B[49m"),
|
|
395
|
+
blackBright: f("\x1B[90m", "\x1B[39m"),
|
|
396
|
+
redBright: f("\x1B[91m", "\x1B[39m"),
|
|
397
|
+
greenBright: f("\x1B[92m", "\x1B[39m"),
|
|
398
|
+
yellowBright: f("\x1B[93m", "\x1B[39m"),
|
|
399
|
+
blueBright: f("\x1B[94m", "\x1B[39m"),
|
|
400
|
+
magentaBright: f("\x1B[95m", "\x1B[39m"),
|
|
401
|
+
cyanBright: f("\x1B[96m", "\x1B[39m"),
|
|
402
|
+
whiteBright: f("\x1B[97m", "\x1B[39m"),
|
|
403
|
+
bgBlackBright: f("\x1B[100m", "\x1B[49m"),
|
|
404
|
+
bgRedBright: f("\x1B[101m", "\x1B[49m"),
|
|
405
|
+
bgGreenBright: f("\x1B[102m", "\x1B[49m"),
|
|
406
|
+
bgYellowBright: f("\x1B[103m", "\x1B[49m"),
|
|
407
|
+
bgBlueBright: f("\x1B[104m", "\x1B[49m"),
|
|
408
|
+
bgMagentaBright: f("\x1B[105m", "\x1B[49m"),
|
|
409
|
+
bgCyanBright: f("\x1B[106m", "\x1B[49m"),
|
|
410
|
+
bgWhiteBright: f("\x1B[107m", "\x1B[49m")
|
|
411
|
+
};
|
|
412
|
+
};
|
|
413
|
+
module.exports = createColors();
|
|
414
|
+
module.exports.createColors = createColors;
|
|
415
|
+
}));
|
|
416
|
+
|
|
417
|
+
//#endregion
|
|
418
|
+
//#region src/cli/program/version-embedded.ts
|
|
419
|
+
var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
|
|
420
|
+
const EMBEDDED_PACKAGE_VERSION = "0.1.1";
|
|
421
|
+
|
|
422
|
+
//#endregion
|
|
423
|
+
//#region src/cli/program/version.ts
|
|
424
|
+
function* candidateSearchRoots() {
|
|
425
|
+
yield dirname(fileURLToPath(import.meta.url));
|
|
426
|
+
const argvPath = process.argv[1];
|
|
427
|
+
if (typeof argvPath === "string" && argvPath.length > 0) yield dirname(resolve(argvPath));
|
|
428
|
+
yield process.cwd();
|
|
429
|
+
}
|
|
430
|
+
function* walkUpDirectories(start, maxLevels) {
|
|
431
|
+
let current = start;
|
|
432
|
+
for (let depth = 0; depth < maxLevels; depth += 1) {
|
|
433
|
+
yield current;
|
|
434
|
+
const parent = dirname(current);
|
|
435
|
+
if (parent === current) break;
|
|
436
|
+
current = parent;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
function resolveVersionFromPath(start, maxLevels) {
|
|
440
|
+
for (const directory of walkUpDirectories(start, maxLevels)) try {
|
|
441
|
+
const raw = readFileSync(join(directory, "package.json"), "utf8");
|
|
442
|
+
const data = JSON.parse(raw);
|
|
443
|
+
if (data.version) return data.version;
|
|
444
|
+
} catch {}
|
|
445
|
+
return null;
|
|
739
446
|
}
|
|
740
|
-
function
|
|
741
|
-
|
|
447
|
+
function normalizeVersion(value) {
|
|
448
|
+
if (typeof value !== "string") return null;
|
|
449
|
+
const trimmed = value.trim();
|
|
450
|
+
if (!trimmed) return null;
|
|
451
|
+
return trimmed;
|
|
742
452
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
const
|
|
747
|
-
const
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
cyrillic: /\p{Script=Cyrillic}/u,
|
|
756
|
-
devanagari: /\p{Script=Devanagari}/u,
|
|
757
|
-
thai: /\p{Script=Thai}/u
|
|
758
|
-
};
|
|
759
|
-
const latinLocaleHints = [
|
|
760
|
-
{
|
|
761
|
-
locale: "de",
|
|
762
|
-
regex: /[äöüÄÖÜß]/
|
|
763
|
-
},
|
|
764
|
-
{
|
|
765
|
-
locale: "es",
|
|
766
|
-
regex: /[ñÑ¿¡]/
|
|
767
|
-
},
|
|
768
|
-
{
|
|
769
|
-
locale: "pt",
|
|
770
|
-
regex: /[ãõÃÕ]/
|
|
771
|
-
},
|
|
772
|
-
{
|
|
773
|
-
locale: "fr",
|
|
774
|
-
regex: /[œŒæÆ]/
|
|
453
|
+
function resolvePackageVersion(options = {}) {
|
|
454
|
+
const embeddedVersion = normalizeVersion(options.embeddedVersion ?? EMBEDDED_PACKAGE_VERSION);
|
|
455
|
+
if (embeddedVersion) return embeddedVersion;
|
|
456
|
+
const maxLevels = options.maxLevels ?? 8;
|
|
457
|
+
const resolveFromPath = options.resolveFromPath ?? resolveVersionFromPath;
|
|
458
|
+
const roots = options.candidateRoots ?? [...candidateSearchRoots()];
|
|
459
|
+
const seen = /* @__PURE__ */ new Set();
|
|
460
|
+
for (const root of roots) {
|
|
461
|
+
if (seen.has(root)) continue;
|
|
462
|
+
seen.add(root);
|
|
463
|
+
const version = normalizeVersion(resolveFromPath(root, maxLevels));
|
|
464
|
+
if (version) return version;
|
|
775
465
|
}
|
|
776
|
-
|
|
777
|
-
const latinLocales = new Set([DEFAULT_LOCALE, ...latinLocaleHints.map((hint) => hint.locale)]);
|
|
778
|
-
function isLatinLocale(locale) {
|
|
779
|
-
return latinLocales.has(locale);
|
|
780
|
-
}
|
|
781
|
-
function detectLatinLocale(char) {
|
|
782
|
-
for (const hint of latinLocaleHints) if (hint.regex.test(char)) return hint.locale;
|
|
783
|
-
return DEFAULT_LOCALE;
|
|
466
|
+
return "0.0.0";
|
|
784
467
|
}
|
|
785
|
-
function
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
const latinLanguageHint = options.latinLanguageHint?.trim();
|
|
789
|
-
if (latinLanguageHint) return latinLanguageHint;
|
|
790
|
-
const latinLocaleHint = options.latinLocaleHint?.trim();
|
|
791
|
-
if (latinLocaleHint) return latinLocaleHint;
|
|
468
|
+
function getFormattedVersionLabel() {
|
|
469
|
+
const version = resolvePackageVersion();
|
|
470
|
+
return import_picocolors.default.bgBlack(import_picocolors.default.bold(import_picocolors.default.italic(` word-counter ${import_picocolors.default.cyanBright(`ver.${version}`)} `)));
|
|
792
471
|
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
472
|
+
|
|
473
|
+
//#endregion
|
|
474
|
+
//#region src/utils/append-all.ts
|
|
475
|
+
function appendAll(target, source) {
|
|
476
|
+
for (const item of source) target.push(item);
|
|
798
477
|
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
if (
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
478
|
+
|
|
479
|
+
//#endregion
|
|
480
|
+
//#region src/cli/path/load.ts
|
|
481
|
+
function isProbablyBinary(buffer) {
|
|
482
|
+
if (buffer.length === 0) return false;
|
|
483
|
+
const sampleSize = Math.min(buffer.length, 1024);
|
|
484
|
+
let suspicious = 0;
|
|
485
|
+
for (let index = 0; index < sampleSize; index += 1) {
|
|
486
|
+
const byte = buffer[index] ?? 0;
|
|
487
|
+
if (byte === 0) return true;
|
|
488
|
+
if (byte === 9 || byte === 10 || byte === 13) continue;
|
|
489
|
+
if (byte >= 32 && byte <= 126) continue;
|
|
490
|
+
if (byte >= 128) continue;
|
|
491
|
+
suspicious += 1;
|
|
809
492
|
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
493
|
+
return suspicious / sampleSize > .3;
|
|
494
|
+
}
|
|
495
|
+
async function loadBatchInputs(filePaths) {
|
|
496
|
+
const files = [];
|
|
497
|
+
const skipped = [];
|
|
498
|
+
for (const filePath of filePaths) {
|
|
499
|
+
let buffer;
|
|
500
|
+
try {
|
|
501
|
+
buffer = await readFile(filePath);
|
|
502
|
+
} catch (error) {
|
|
503
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
504
|
+
skipped.push({
|
|
505
|
+
path: filePath,
|
|
506
|
+
reason: `not readable: ${message}`
|
|
507
|
+
});
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
if (isProbablyBinary(buffer)) {
|
|
511
|
+
skipped.push({
|
|
512
|
+
path: filePath,
|
|
513
|
+
reason: "binary file"
|
|
514
|
+
});
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
files.push({
|
|
518
|
+
path: filePath,
|
|
519
|
+
content: buffer.toString("utf8")
|
|
520
|
+
});
|
|
817
521
|
}
|
|
818
|
-
return
|
|
522
|
+
return {
|
|
523
|
+
files,
|
|
524
|
+
skipped
|
|
525
|
+
};
|
|
819
526
|
}
|
|
820
527
|
|
|
821
528
|
//#endregion
|
|
822
|
-
//#region src/
|
|
823
|
-
function
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
529
|
+
//#region src/cli/path/resolve.ts
|
|
530
|
+
async function expandDirectory(rootPath, directoryPath, recursive, extensionFilter, regexFilter, skipped, recordRegexExcluded, debug, stats) {
|
|
531
|
+
let entries;
|
|
532
|
+
try {
|
|
533
|
+
entries = await readdir(directoryPath, {
|
|
534
|
+
withFileTypes: true,
|
|
535
|
+
encoding: "utf8"
|
|
536
|
+
});
|
|
537
|
+
} catch (error) {
|
|
538
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
539
|
+
skipped.push({
|
|
540
|
+
path: directoryPath,
|
|
541
|
+
reason: `directory read failed: ${message}`
|
|
542
|
+
});
|
|
543
|
+
debug.emit("path.resolve.expand.read_failed", {
|
|
544
|
+
directory: directoryPath,
|
|
545
|
+
reason: `directory read failed: ${message}`
|
|
546
|
+
});
|
|
547
|
+
return [];
|
|
548
|
+
}
|
|
549
|
+
const sortedEntries = entries.slice().sort((left, right) => left.name.localeCompare(right.name));
|
|
550
|
+
const files = [];
|
|
551
|
+
debug.emit("path.resolve.expand.start", {
|
|
552
|
+
directory: directoryPath,
|
|
553
|
+
entries: sortedEntries.length,
|
|
554
|
+
recursive
|
|
555
|
+
});
|
|
556
|
+
for (const entry of sortedEntries) {
|
|
557
|
+
const entryPath = resolve(directoryPath, entry.name);
|
|
558
|
+
if (entry.isFile()) {
|
|
559
|
+
if (!shouldIncludeFromDirectory(entryPath, extensionFilter)) {
|
|
560
|
+
skipped.push({
|
|
561
|
+
path: entryPath,
|
|
562
|
+
reason: "extension excluded"
|
|
563
|
+
});
|
|
564
|
+
debug.emit("path.resolve.filter.excluded", {
|
|
565
|
+
path: entryPath,
|
|
566
|
+
reason: "extension excluded"
|
|
567
|
+
}, { verbosity: "verbose" });
|
|
568
|
+
stats.filterExcluded += 1;
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
const relativePath = toDirectoryRelativePath(rootPath, entryPath);
|
|
572
|
+
if (!shouldIncludeFromDirectoryRegex(relativePath, regexFilter)) {
|
|
573
|
+
if (recordRegexExcluded(entryPath)) {
|
|
574
|
+
debug.emit("path.resolve.regex.excluded", {
|
|
575
|
+
path: entryPath,
|
|
576
|
+
relativePath,
|
|
577
|
+
pattern: regexFilter.sourcePattern,
|
|
578
|
+
reason: "regex excluded"
|
|
579
|
+
}, { verbosity: "verbose" });
|
|
580
|
+
stats.regexExcluded += 1;
|
|
581
|
+
}
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
files.push(entryPath);
|
|
585
|
+
stats.directoryIncluded += 1;
|
|
586
|
+
debug.emit("path.resolve.expand.include", {
|
|
587
|
+
path: entryPath,
|
|
588
|
+
source: "directory"
|
|
589
|
+
}, { verbosity: "verbose" });
|
|
835
590
|
continue;
|
|
836
591
|
}
|
|
837
|
-
if (
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
592
|
+
if (!entry.isDirectory() || !recursive) continue;
|
|
593
|
+
appendAll(files, await expandDirectory(rootPath, entryPath, recursive, extensionFilter, regexFilter, skipped, recordRegexExcluded, debug, stats));
|
|
594
|
+
}
|
|
595
|
+
debug.emit("path.resolve.expand.complete", {
|
|
596
|
+
directory: directoryPath,
|
|
597
|
+
files: files.length
|
|
598
|
+
});
|
|
599
|
+
return files;
|
|
600
|
+
}
|
|
601
|
+
async function resolveBatchFilePaths(pathInputs, options) {
|
|
602
|
+
const skipped = [];
|
|
603
|
+
const regexExcludedPaths = /* @__PURE__ */ new Set();
|
|
604
|
+
const resolvedFiles = /* @__PURE__ */ new Set();
|
|
605
|
+
const stats = {
|
|
606
|
+
dedupeAccepted: 0,
|
|
607
|
+
dedupeDuplicates: 0,
|
|
608
|
+
filterExcluded: 0,
|
|
609
|
+
regexExcluded: 0,
|
|
610
|
+
directoryIncluded: 0
|
|
611
|
+
};
|
|
612
|
+
const extensionFilter = options.extensionFilter ?? buildDirectoryExtensionFilter(void 0, void 0);
|
|
613
|
+
let regexFilter;
|
|
614
|
+
const debug = options.debug ?? {
|
|
615
|
+
enabled: false,
|
|
616
|
+
verbosity: "compact",
|
|
617
|
+
emit() {},
|
|
618
|
+
close: async () => {}
|
|
619
|
+
};
|
|
620
|
+
debug.emit("path.resolve.inputs", {
|
|
621
|
+
inputs: pathInputs.length,
|
|
622
|
+
pathMode: options.pathMode,
|
|
623
|
+
recursive: options.recursive,
|
|
624
|
+
hasRegex: Boolean(options.directoryRegexPattern)
|
|
625
|
+
});
|
|
626
|
+
const addResolvedFile = (filePath, details) => {
|
|
627
|
+
regexExcludedPaths.delete(filePath);
|
|
628
|
+
if (resolvedFiles.has(filePath)) {
|
|
629
|
+
stats.dedupeDuplicates += 1;
|
|
630
|
+
debug.emit("path.resolve.dedupe.duplicate", {
|
|
631
|
+
path: filePath,
|
|
632
|
+
source: details.source,
|
|
633
|
+
input: details.input
|
|
634
|
+
}, { verbosity: "verbose" });
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
resolvedFiles.add(filePath);
|
|
638
|
+
stats.dedupeAccepted += 1;
|
|
639
|
+
debug.emit("path.resolve.dedupe.accept", {
|
|
640
|
+
path: filePath,
|
|
641
|
+
source: details.source,
|
|
642
|
+
input: details.input
|
|
643
|
+
}, { verbosity: "verbose" });
|
|
644
|
+
};
|
|
645
|
+
const getRegexFilter = () => {
|
|
646
|
+
if (!regexFilter) regexFilter = buildDirectoryRegexFilter(options.directoryRegexPattern);
|
|
647
|
+
return regexFilter;
|
|
648
|
+
};
|
|
649
|
+
const recordRegexExcluded = (filePath) => {
|
|
650
|
+
if (resolvedFiles.has(filePath)) return false;
|
|
651
|
+
regexExcludedPaths.add(filePath);
|
|
652
|
+
return true;
|
|
653
|
+
};
|
|
654
|
+
for (const rawPath of pathInputs) {
|
|
655
|
+
const targetPath = resolve(rawPath);
|
|
656
|
+
debug.emit("path.resolve.input", {
|
|
657
|
+
rawPath,
|
|
658
|
+
resolvedPath: targetPath
|
|
659
|
+
});
|
|
660
|
+
let metadata;
|
|
661
|
+
try {
|
|
662
|
+
metadata = await stat(targetPath);
|
|
663
|
+
} catch (error) {
|
|
664
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
665
|
+
skipped.push({
|
|
666
|
+
path: targetPath,
|
|
667
|
+
reason: `not readable: ${message}`
|
|
668
|
+
});
|
|
669
|
+
debug.emit("path.resolve.skip", {
|
|
670
|
+
path: targetPath,
|
|
671
|
+
reason: `not readable: ${message}`
|
|
672
|
+
});
|
|
841
673
|
continue;
|
|
842
674
|
}
|
|
843
|
-
if (
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
675
|
+
if (metadata.isDirectory() && options.pathMode === "auto") {
|
|
676
|
+
const effectiveRegexFilter = getRegexFilter();
|
|
677
|
+
debug.emit("path.resolve.root.expand", {
|
|
678
|
+
root: targetPath,
|
|
679
|
+
recursive: options.recursive,
|
|
680
|
+
regex: effectiveRegexFilter.sourcePattern ?? null
|
|
681
|
+
});
|
|
682
|
+
const files = await expandDirectory(targetPath, targetPath, options.recursive, extensionFilter, effectiveRegexFilter, skipped, recordRegexExcluded, debug, stats);
|
|
683
|
+
for (const file of files) addResolvedFile(file, {
|
|
684
|
+
source: "directory",
|
|
685
|
+
input: targetPath
|
|
853
686
|
});
|
|
854
|
-
currentLocale = targetLocale;
|
|
855
|
-
buffer = char;
|
|
856
|
-
bufferHasScript = true;
|
|
857
687
|
continue;
|
|
858
688
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
if (chunks.length === 0) return chunks;
|
|
870
|
-
const merged = [];
|
|
871
|
-
let last = chunks[0];
|
|
872
|
-
for (let i = 1; i < chunks.length; i++) {
|
|
873
|
-
const chunk = chunks[i];
|
|
874
|
-
if (chunk.locale === last.locale) last = {
|
|
875
|
-
locale: last.locale,
|
|
876
|
-
text: last.text + chunk.text
|
|
877
|
-
};
|
|
878
|
-
else {
|
|
879
|
-
merged.push(last);
|
|
880
|
-
last = chunk;
|
|
689
|
+
if (!metadata.isFile()) {
|
|
690
|
+
skipped.push({
|
|
691
|
+
path: targetPath,
|
|
692
|
+
reason: "not a regular file"
|
|
693
|
+
});
|
|
694
|
+
debug.emit("path.resolve.skip", {
|
|
695
|
+
path: targetPath,
|
|
696
|
+
reason: "not a regular file"
|
|
697
|
+
});
|
|
698
|
+
continue;
|
|
881
699
|
}
|
|
700
|
+
addResolvedFile(targetPath, {
|
|
701
|
+
source: "direct",
|
|
702
|
+
input: targetPath
|
|
703
|
+
});
|
|
882
704
|
}
|
|
883
|
-
|
|
884
|
-
|
|
705
|
+
for (const path of regexExcludedPaths) skipped.push({
|
|
706
|
+
path,
|
|
707
|
+
reason: "regex excluded"
|
|
708
|
+
});
|
|
709
|
+
const files = [...resolvedFiles].sort((left, right) => left.localeCompare(right));
|
|
710
|
+
debug.emit("path.resolve.filter.summary", {
|
|
711
|
+
excluded: stats.filterExcluded + stats.regexExcluded,
|
|
712
|
+
extensionExcluded: stats.filterExcluded,
|
|
713
|
+
regexExcluded: stats.regexExcluded,
|
|
714
|
+
included: stats.directoryIncluded
|
|
715
|
+
});
|
|
716
|
+
debug.emit("path.resolve.dedupe.summary", {
|
|
717
|
+
accepted: stats.dedupeAccepted,
|
|
718
|
+
duplicates: stats.dedupeDuplicates
|
|
719
|
+
});
|
|
720
|
+
debug.emit("path.resolve.complete", {
|
|
721
|
+
files: files.length,
|
|
722
|
+
skipped: skipped.length,
|
|
723
|
+
ordering: "absolute-path-ascending"
|
|
724
|
+
});
|
|
725
|
+
return {
|
|
726
|
+
files,
|
|
727
|
+
skipped
|
|
728
|
+
};
|
|
885
729
|
}
|
|
886
730
|
|
|
887
731
|
//#endregion
|
|
888
|
-
//#region src/
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
const
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
}, 0);
|
|
933
|
-
const counts = collectNonWords ? {
|
|
934
|
-
words: wordsTotal,
|
|
935
|
-
nonWords: nonWordsTotal,
|
|
936
|
-
total
|
|
937
|
-
} : void 0;
|
|
938
|
-
if (mode === "segments") return {
|
|
939
|
-
total,
|
|
940
|
-
counts,
|
|
941
|
-
breakdown: {
|
|
942
|
-
mode,
|
|
943
|
-
items: analyzed.map((chunk) => ({
|
|
944
|
-
locale: chunk.locale,
|
|
945
|
-
text: chunk.text,
|
|
946
|
-
words: chunk.words,
|
|
947
|
-
segments: chunk.segments,
|
|
948
|
-
nonWords: chunk.nonWords
|
|
949
|
-
}))
|
|
950
|
-
}
|
|
732
|
+
//#region src/cli/progress/reporter.ts
|
|
733
|
+
const PROGRESS_BAR_WIDTH = 20;
|
|
734
|
+
const FILLED_BAR_CHAR = "█";
|
|
735
|
+
const EMPTY_BAR_CHAR = "░";
|
|
736
|
+
function clamp(value, min, max) {
|
|
737
|
+
return Math.max(min, Math.min(max, value));
|
|
738
|
+
}
|
|
739
|
+
function buildProgressBar(completed, total) {
|
|
740
|
+
const safeTotal = Math.max(total, 1);
|
|
741
|
+
const ratio = clamp(completed / safeTotal, 0, 1);
|
|
742
|
+
const filled = completed >= safeTotal ? PROGRESS_BAR_WIDTH : Math.floor(ratio * PROGRESS_BAR_WIDTH);
|
|
743
|
+
const empty = PROGRESS_BAR_WIDTH - filled;
|
|
744
|
+
return `${FILLED_BAR_CHAR.repeat(filled)}${EMPTY_BAR_CHAR.repeat(empty)}`;
|
|
745
|
+
}
|
|
746
|
+
function formatElapsed(startedAtMs) {
|
|
747
|
+
const elapsedMs = Date.now() - startedAtMs;
|
|
748
|
+
const totalSeconds = Math.max(0, Math.floor(elapsedMs / 1e3));
|
|
749
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
750
|
+
const seconds = totalSeconds % 60;
|
|
751
|
+
const tenths = Math.floor(Math.max(0, elapsedMs) % 1e3 / 100);
|
|
752
|
+
return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}.${tenths}`;
|
|
753
|
+
}
|
|
754
|
+
function buildProgressLine(completed, total, startedAtMs) {
|
|
755
|
+
const safeTotal = Math.max(total, 1);
|
|
756
|
+
const percent = completed >= safeTotal ? 100 : Math.floor(completed / safeTotal * 100);
|
|
757
|
+
return `Counting files [${buildProgressBar(completed, safeTotal)}] ${`${String(percent).padStart(3, " ")}%`} ${String(completed).padStart(String(safeTotal).length, " ")}/${safeTotal} elapsed ${formatElapsed(startedAtMs)}`;
|
|
758
|
+
}
|
|
759
|
+
function buildFinalizingLine(startedAtMs) {
|
|
760
|
+
return `Finalizing aggregate... elapsed ${formatElapsed(startedAtMs)}`;
|
|
761
|
+
}
|
|
762
|
+
function createBatchProgressReporter(options) {
|
|
763
|
+
const enabled = options.enabled;
|
|
764
|
+
const isTTY = Boolean(options.stream.isTTY);
|
|
765
|
+
const clearOnFinish = options.clearOnFinish ?? true;
|
|
766
|
+
let active = false;
|
|
767
|
+
let total = 0;
|
|
768
|
+
let lastLineLength = 0;
|
|
769
|
+
let startedAtMs = 0;
|
|
770
|
+
let lastRenderedPercent = -1;
|
|
771
|
+
let finalizingStarted = false;
|
|
772
|
+
const writeTTYLine = (line) => {
|
|
773
|
+
const trailingPadding = lastLineLength > line.length ? " ".repeat(lastLineLength - line.length) : "";
|
|
774
|
+
options.stream.write(`\r${line}${trailingPadding}`);
|
|
775
|
+
lastLineLength = line.length;
|
|
951
776
|
};
|
|
952
|
-
|
|
953
|
-
total,
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
777
|
+
const render = (completed) => {
|
|
778
|
+
const line = buildProgressLine(completed, total, startedAtMs);
|
|
779
|
+
const safeTotal = Math.max(total, 1);
|
|
780
|
+
const percent = completed >= safeTotal ? 100 : Math.floor(completed / safeTotal * 100);
|
|
781
|
+
if (!isTTY && percent === lastRenderedPercent && completed < safeTotal) return;
|
|
782
|
+
lastRenderedPercent = percent;
|
|
783
|
+
if (isTTY) {
|
|
784
|
+
writeTTYLine(line);
|
|
785
|
+
return;
|
|
959
786
|
}
|
|
787
|
+
lastLineLength = line.length;
|
|
788
|
+
options.stream.write(`${line}\n`);
|
|
960
789
|
};
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
790
|
+
const clearLine = () => {
|
|
791
|
+
if (lastLineLength === 0) return;
|
|
792
|
+
options.stream.write(`\r${" ".repeat(lastLineLength)}\r`);
|
|
793
|
+
lastLineLength = 0;
|
|
794
|
+
};
|
|
795
|
+
return {
|
|
796
|
+
enabled,
|
|
797
|
+
start(nextTotal, nextStartedAtMs) {
|
|
798
|
+
if (!enabled || nextTotal <= 1) return;
|
|
799
|
+
total = nextTotal;
|
|
800
|
+
active = true;
|
|
801
|
+
startedAtMs = nextStartedAtMs ?? Date.now();
|
|
802
|
+
lastRenderedPercent = -1;
|
|
803
|
+
finalizingStarted = false;
|
|
804
|
+
render(0);
|
|
805
|
+
},
|
|
806
|
+
advance(snapshot) {
|
|
807
|
+
if (!active) return;
|
|
808
|
+
render(snapshot.completed);
|
|
809
|
+
},
|
|
810
|
+
startFinalizing() {
|
|
811
|
+
if (!active || finalizingStarted) return;
|
|
812
|
+
finalizingStarted = true;
|
|
813
|
+
const line = buildFinalizingLine(startedAtMs);
|
|
814
|
+
if (isTTY) {
|
|
815
|
+
if (!clearOnFinish) {
|
|
816
|
+
options.stream.write(`\n${line}`);
|
|
817
|
+
lastLineLength = line.length;
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
writeTTYLine(line);
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
lastLineLength = line.length;
|
|
824
|
+
options.stream.write(`${line}\n`);
|
|
825
|
+
},
|
|
826
|
+
finish() {
|
|
827
|
+
if (!active) return;
|
|
828
|
+
if (isTTY) if (clearOnFinish) clearLine();
|
|
829
|
+
else options.stream.write("\n");
|
|
830
|
+
active = false;
|
|
831
|
+
}
|
|
973
832
|
};
|
|
974
833
|
}
|
|
975
|
-
function getNonWordTotal(nonWords) {
|
|
976
|
-
return nonWords.counts.emoji + nonWords.counts.symbols + nonWords.counts.punctuation + (nonWords.counts.whitespace ?? 0);
|
|
977
|
-
}
|
|
978
|
-
function collectNonWordsAggregate(analyzed, enabled) {
|
|
979
|
-
if (!enabled) return;
|
|
980
|
-
const collection = createNonWordCollection();
|
|
981
|
-
for (const chunk of analyzed) {
|
|
982
|
-
if (!chunk.nonWords) continue;
|
|
983
|
-
mergeNonWordCollections(collection, chunk.nonWords);
|
|
984
|
-
}
|
|
985
|
-
return collection;
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
//#endregion
|
|
989
|
-
//#region src/wc/index.ts
|
|
990
|
-
var wc_default = wordCounter;
|
|
991
834
|
|
|
992
835
|
//#endregion
|
|
993
|
-
//#region src/markdown/
|
|
994
|
-
function
|
|
995
|
-
|
|
996
|
-
if (
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
} catch {
|
|
1001
|
-
return String(value);
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
function buildPerKeyItems(data, mode, options) {
|
|
1005
|
-
if (!data || typeof data !== "object" || Array.isArray(data)) return [];
|
|
1006
|
-
return Object.entries(data).map(([key, value]) => {
|
|
1007
|
-
const valueText = normalizeText(value);
|
|
1008
|
-
return {
|
|
1009
|
-
name: key,
|
|
1010
|
-
source: "frontmatter",
|
|
1011
|
-
result: wc_default(valueText ? `${key}: ${valueText}` : key, options)
|
|
1012
|
-
};
|
|
1013
|
-
});
|
|
1014
|
-
}
|
|
1015
|
-
function buildSingleItem(name, text, mode, options, source) {
|
|
1016
|
-
return [{
|
|
1017
|
-
name,
|
|
1018
|
-
source,
|
|
1019
|
-
result: wc_default(text, options)
|
|
1020
|
-
}];
|
|
1021
|
-
}
|
|
1022
|
-
function sumTotals(items) {
|
|
1023
|
-
return items.reduce((sum, item) => sum + item.result.total, 0);
|
|
836
|
+
//#region src/markdown/toml/arrays.ts
|
|
837
|
+
function ensureArrayContainer(result, key) {
|
|
838
|
+
const existing = result[key];
|
|
839
|
+
if (Array.isArray(existing)) return existing;
|
|
840
|
+
const list = [];
|
|
841
|
+
result[key] = list;
|
|
842
|
+
return list;
|
|
1024
843
|
}
|
|
1025
|
-
function
|
|
1026
|
-
const
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
return {
|
|
1030
|
-
section,
|
|
1031
|
-
total: result.total,
|
|
1032
|
-
frontmatterType: null,
|
|
1033
|
-
items: [{
|
|
1034
|
-
name: "all",
|
|
1035
|
-
source: "content",
|
|
1036
|
-
result
|
|
1037
|
-
}]
|
|
1038
|
-
};
|
|
844
|
+
function flattenArrayTables(result) {
|
|
845
|
+
for (const [key, value] of Object.entries(result)) {
|
|
846
|
+
if (!Array.isArray(value)) continue;
|
|
847
|
+
result[key] = value.map((entry) => Object.entries(entry).map(([entryKey, entryValue]) => `${entryKey}=${entryValue}`).join(", ")).join(" | ");
|
|
1039
848
|
}
|
|
1040
|
-
const parsed = parseMarkdown(input);
|
|
1041
|
-
const frontmatterText = parsed.frontmatter ?? "";
|
|
1042
|
-
const contentText = parsed.content ?? "";
|
|
1043
|
-
let items = [];
|
|
1044
|
-
if (section === "frontmatter") items = buildSingleItem("frontmatter", frontmatterText, mode, options, "frontmatter");
|
|
1045
|
-
else if (section === "content") items = buildSingleItem("content", contentText, mode, options, "content");
|
|
1046
|
-
else if (section === "split") items = [...buildSingleItem("frontmatter", frontmatterText, mode, options, "frontmatter"), ...buildSingleItem("content", contentText, mode, options, "content")];
|
|
1047
|
-
else if (section === "per-key") items = buildPerKeyItems(parsed.data, mode, options);
|
|
1048
|
-
else if (section === "split-per-key") items = [...buildPerKeyItems(parsed.data, mode, options), ...buildSingleItem("content", contentText, mode, options, "content")];
|
|
1049
|
-
return {
|
|
1050
|
-
section,
|
|
1051
|
-
total: sumTotals(items),
|
|
1052
|
-
frontmatterType: parsed.frontmatterType,
|
|
1053
|
-
items
|
|
1054
|
-
};
|
|
1055
849
|
}
|
|
1056
850
|
|
|
1057
851
|
//#endregion
|
|
1058
|
-
//#region src/
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
".
|
|
1062
|
-
|
|
1063
|
-
".mdc",
|
|
1064
|
-
".txt"
|
|
1065
|
-
]);
|
|
1066
|
-
function collectExtensionOption(value, previous = []) {
|
|
1067
|
-
return [...previous, value];
|
|
852
|
+
//#region src/markdown/toml/keys.ts
|
|
853
|
+
function stripKeyQuotes(key) {
|
|
854
|
+
const trimmed = key.trim();
|
|
855
|
+
if (trimmed.startsWith("\"") && trimmed.endsWith("\"") || trimmed.startsWith("'") && trimmed.endsWith("'")) return trimmed.slice(1, -1);
|
|
856
|
+
return trimmed;
|
|
1068
857
|
}
|
|
1069
|
-
function
|
|
1070
|
-
const trimmed =
|
|
858
|
+
function normalizeKeyPath(key) {
|
|
859
|
+
const trimmed = key.trim();
|
|
1071
860
|
if (!trimmed) return null;
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
}
|
|
1076
|
-
function parseExtensionValues(values) {
|
|
1077
|
-
const parsed = /* @__PURE__ */ new Set();
|
|
1078
|
-
if (!values || values.length === 0) return parsed;
|
|
1079
|
-
for (const value of values) for (const token of value.split(",")) {
|
|
1080
|
-
const normalized = normalizeExtensionToken(token);
|
|
1081
|
-
if (!normalized) continue;
|
|
1082
|
-
parsed.add(normalized);
|
|
861
|
+
if (trimmed.startsWith("\"") && trimmed.endsWith("\"") || trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
862
|
+
const unquoted = stripKeyQuotes(trimmed);
|
|
863
|
+
return unquoted ? unquoted : null;
|
|
1083
864
|
}
|
|
1084
|
-
|
|
865
|
+
const segments = trimmed.split(".").map((segment) => segment.trim());
|
|
866
|
+
if (segments.some((segment) => !segment)) return null;
|
|
867
|
+
return segments.join(".");
|
|
1085
868
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
869
|
+
|
|
870
|
+
//#endregion
|
|
871
|
+
//#region src/markdown/toml/strings.ts
|
|
872
|
+
function stripInlineComment(line) {
|
|
873
|
+
let inString = null;
|
|
874
|
+
let escaped = false;
|
|
875
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
876
|
+
const char = line[i] ?? "";
|
|
877
|
+
if (inString) {
|
|
878
|
+
if (escaped) {
|
|
879
|
+
escaped = false;
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
if (char === "\\" && inString === "double") {
|
|
883
|
+
escaped = true;
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
if (inString === "double" && char === "\"") {
|
|
887
|
+
inString = null;
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
if (inString === "single" && char === "'") {
|
|
891
|
+
inString = null;
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
if (char === "\"") {
|
|
897
|
+
inString = "double";
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
if (char === "'") {
|
|
901
|
+
inString = "single";
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
if (char === "#") return line.slice(0, i).trimEnd();
|
|
1094
905
|
}
|
|
1095
|
-
return
|
|
1096
|
-
includeExtensions,
|
|
1097
|
-
excludeExtensions,
|
|
1098
|
-
effectiveIncludeExtensions
|
|
1099
|
-
};
|
|
906
|
+
return line;
|
|
1100
907
|
}
|
|
1101
|
-
function
|
|
1102
|
-
|
|
1103
|
-
|
|
908
|
+
function unescapeBasic(input) {
|
|
909
|
+
return input.replace(/\\\\/g, "\\").replace(/\\"/g, "\"").replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\r/g, "\r");
|
|
910
|
+
}
|
|
911
|
+
function parseStringLiteral(value) {
|
|
912
|
+
if (value.startsWith("\"\"\"") && value.endsWith("\"\"\"")) return unescapeBasic(value.slice(3, -3));
|
|
913
|
+
if (value.startsWith("'''") && value.endsWith("'''")) return value.slice(3, -3);
|
|
914
|
+
if (value.startsWith("\"") && value.endsWith("\"")) return unescapeBasic(value.slice(1, -1));
|
|
915
|
+
if (value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
|
|
916
|
+
return null;
|
|
1104
917
|
}
|
|
1105
918
|
|
|
1106
919
|
//#endregion
|
|
1107
|
-
//#region src/
|
|
1108
|
-
function
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
suspicious += 1;
|
|
1119
|
-
}
|
|
1120
|
-
return suspicious / sampleSize > .3;
|
|
920
|
+
//#region src/markdown/toml/values.ts
|
|
921
|
+
function parsePrimitive(raw) {
|
|
922
|
+
const value = raw.trim();
|
|
923
|
+
if (!value) return null;
|
|
924
|
+
const stringLiteral = parseStringLiteral(value);
|
|
925
|
+
if (stringLiteral !== null) return stringLiteral;
|
|
926
|
+
if (value === "true") return true;
|
|
927
|
+
if (value === "false") return false;
|
|
928
|
+
if (/^[+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(value)) return Number(value);
|
|
929
|
+
if (/^\d{4}-\d{2}-\d{2}/.test(value)) return value;
|
|
930
|
+
return value;
|
|
1121
931
|
}
|
|
1122
|
-
|
|
1123
|
-
const
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
932
|
+
function parseArray(raw) {
|
|
933
|
+
const value = raw.trim();
|
|
934
|
+
if (!value.startsWith("[") || !value.endsWith("]")) return null;
|
|
935
|
+
const inner = value.slice(1, -1).trim();
|
|
936
|
+
if (!inner) return [];
|
|
937
|
+
const items = [];
|
|
938
|
+
let current = "";
|
|
939
|
+
let inString = null;
|
|
940
|
+
let escaped = false;
|
|
941
|
+
for (let i = 0; i < inner.length; i += 1) {
|
|
942
|
+
const char = inner[i] ?? "";
|
|
943
|
+
if (inString) {
|
|
944
|
+
current += char;
|
|
945
|
+
if (escaped) {
|
|
946
|
+
escaped = false;
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
if (char === "\\" && inString === "double") {
|
|
950
|
+
escaped = true;
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
if (inString === "double" && char === "\"") inString = null;
|
|
954
|
+
else if (inString === "single" && char === "'") inString = null;
|
|
1135
955
|
continue;
|
|
1136
956
|
}
|
|
1137
|
-
if (
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
957
|
+
if (char === "\"") {
|
|
958
|
+
inString = "double";
|
|
959
|
+
current += char;
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
if (char === "'") {
|
|
963
|
+
inString = "single";
|
|
964
|
+
current += char;
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
if (char === ",") {
|
|
968
|
+
const item = parsePrimitive(current);
|
|
969
|
+
if (item === null) return null;
|
|
970
|
+
items.push(item);
|
|
971
|
+
current = "";
|
|
1142
972
|
continue;
|
|
1143
973
|
}
|
|
1144
|
-
|
|
1145
|
-
path: filePath,
|
|
1146
|
-
content: buffer.toString("utf8")
|
|
1147
|
-
});
|
|
974
|
+
current += char;
|
|
1148
975
|
}
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
976
|
+
const finalItem = parsePrimitive(current);
|
|
977
|
+
if (finalItem === null) return null;
|
|
978
|
+
items.push(finalItem);
|
|
979
|
+
return items;
|
|
1153
980
|
}
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
directory: directoryPath,
|
|
1172
|
-
reason: `directory read failed: ${message}`
|
|
1173
|
-
});
|
|
1174
|
-
return [];
|
|
1175
|
-
}
|
|
1176
|
-
const sortedEntries = entries.slice().sort((left, right) => left.name.localeCompare(right.name));
|
|
1177
|
-
const files = [];
|
|
1178
|
-
debug.emit("path.resolve.expand.start", {
|
|
1179
|
-
directory: directoryPath,
|
|
1180
|
-
entries: sortedEntries.length,
|
|
1181
|
-
recursive
|
|
1182
|
-
});
|
|
1183
|
-
for (const entry of sortedEntries) {
|
|
1184
|
-
const entryPath = resolve(directoryPath, entry.name);
|
|
1185
|
-
if (entry.isFile()) {
|
|
1186
|
-
if (!shouldIncludeFromDirectory(entryPath, filter)) {
|
|
1187
|
-
skipped.push({
|
|
1188
|
-
path: entryPath,
|
|
1189
|
-
reason: "extension excluded"
|
|
1190
|
-
});
|
|
1191
|
-
debug.emit("path.resolve.filter.excluded", {
|
|
1192
|
-
path: entryPath,
|
|
1193
|
-
reason: "extension excluded"
|
|
1194
|
-
}, { verbosity: "verbose" });
|
|
1195
|
-
stats.filterExcluded += 1;
|
|
981
|
+
function parseInlineTable(raw) {
|
|
982
|
+
const trimmed = raw.trim();
|
|
983
|
+
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return null;
|
|
984
|
+
const inner = trimmed.slice(1, -1).trim();
|
|
985
|
+
if (!inner) return {};
|
|
986
|
+
const pairs = [];
|
|
987
|
+
let current = "";
|
|
988
|
+
let inString = null;
|
|
989
|
+
let escaped = false;
|
|
990
|
+
let bracketDepth = 0;
|
|
991
|
+
let braceDepth = 0;
|
|
992
|
+
for (let i = 0; i < inner.length; i += 1) {
|
|
993
|
+
const char = inner[i] ?? "";
|
|
994
|
+
if (inString) {
|
|
995
|
+
current += char;
|
|
996
|
+
if (escaped) {
|
|
997
|
+
escaped = false;
|
|
1196
998
|
continue;
|
|
1197
999
|
}
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1000
|
+
if (char === "\\" && inString === "double") {
|
|
1001
|
+
escaped = true;
|
|
1002
|
+
continue;
|
|
1003
|
+
}
|
|
1004
|
+
if (inString === "double" && char === "\"") inString = null;
|
|
1005
|
+
else if (inString === "single" && char === "'") inString = null;
|
|
1204
1006
|
continue;
|
|
1205
1007
|
}
|
|
1206
|
-
if (
|
|
1207
|
-
|
|
1008
|
+
if (char === "\"") {
|
|
1009
|
+
inString = "double";
|
|
1010
|
+
current += char;
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
if (char === "'") {
|
|
1014
|
+
inString = "single";
|
|
1015
|
+
current += char;
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
if (char === "[") {
|
|
1019
|
+
bracketDepth += 1;
|
|
1020
|
+
current += char;
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
if (char === "]") {
|
|
1024
|
+
if (bracketDepth > 0) bracketDepth -= 1;
|
|
1025
|
+
current += char;
|
|
1026
|
+
continue;
|
|
1027
|
+
}
|
|
1028
|
+
if (char === "{") {
|
|
1029
|
+
braceDepth += 1;
|
|
1030
|
+
current += char;
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
if (char === "}") {
|
|
1034
|
+
if (braceDepth > 0) braceDepth -= 1;
|
|
1035
|
+
current += char;
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
if (char === "," && bracketDepth === 0 && braceDepth === 0) {
|
|
1039
|
+
pairs.push(current);
|
|
1040
|
+
current = "";
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
1043
|
+
current += char;
|
|
1208
1044
|
}
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1045
|
+
if (current.trim()) pairs.push(current);
|
|
1046
|
+
const output = {};
|
|
1047
|
+
for (const pair of pairs) {
|
|
1048
|
+
const separatorIndex = pair.indexOf("=");
|
|
1049
|
+
if (separatorIndex === -1) return null;
|
|
1050
|
+
const key = normalizeKeyPath(pair.slice(0, separatorIndex));
|
|
1051
|
+
if (!key) return null;
|
|
1052
|
+
const valueRaw = pair.slice(separatorIndex + 1).trim();
|
|
1053
|
+
if (!valueRaw) return null;
|
|
1054
|
+
if (valueRaw.startsWith("{")) return null;
|
|
1055
|
+
const normalized = normalizeValue(valueRaw);
|
|
1056
|
+
if (normalized === null) return null;
|
|
1057
|
+
if (typeof normalized === "object" && !Array.isArray(normalized)) return null;
|
|
1058
|
+
output[key] = normalized;
|
|
1059
|
+
}
|
|
1060
|
+
return output;
|
|
1214
1061
|
}
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
const
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
return;
|
|
1062
|
+
function normalizeValue(value) {
|
|
1063
|
+
if (!value) return null;
|
|
1064
|
+
const trimmed = value.trim();
|
|
1065
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) return parseInlineTable(trimmed);
|
|
1066
|
+
const array = parseArray(trimmed);
|
|
1067
|
+
if (array) return array;
|
|
1068
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) return null;
|
|
1069
|
+
return parsePrimitive(trimmed);
|
|
1070
|
+
}
|
|
1071
|
+
function toPlainText(value) {
|
|
1072
|
+
if (value == null) return "";
|
|
1073
|
+
if (Array.isArray(value)) return value.map((item) => String(item)).join(", ");
|
|
1074
|
+
return String(value);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
//#endregion
|
|
1078
|
+
//#region src/markdown/toml/parse-frontmatter.ts
|
|
1079
|
+
function parseTomlFrontmatter(frontmatter) {
|
|
1080
|
+
const result = {};
|
|
1081
|
+
const lines = frontmatter.split("\n");
|
|
1082
|
+
let tablePrefix = "";
|
|
1083
|
+
let tableTarget = null;
|
|
1084
|
+
let tablePrefixInList = false;
|
|
1085
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
1086
|
+
const rawLine = lines[index] ?? "";
|
|
1087
|
+
const trimmedLine = rawLine.trim();
|
|
1088
|
+
if (!trimmedLine || trimmedLine.startsWith("#")) continue;
|
|
1089
|
+
if (trimmedLine.startsWith("[[")) {
|
|
1090
|
+
const match = trimmedLine.match(/^\[\[([^\]]+)]]$/);
|
|
1091
|
+
if (!match) return null;
|
|
1092
|
+
const normalizedTable = normalizeKeyPath(match[1] ?? "");
|
|
1093
|
+
if (!normalizedTable) return null;
|
|
1094
|
+
const list = ensureArrayContainer(result, normalizedTable);
|
|
1095
|
+
const newEntry = {};
|
|
1096
|
+
list.push(newEntry);
|
|
1097
|
+
tableTarget = newEntry;
|
|
1098
|
+
tablePrefix = normalizedTable;
|
|
1099
|
+
tablePrefixInList = true;
|
|
1100
|
+
continue;
|
|
1245
1101
|
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
};
|
|
1254
|
-
for (const rawPath of pathInputs) {
|
|
1255
|
-
const targetPath = resolve(rawPath);
|
|
1256
|
-
debug.emit("path.resolve.input", {
|
|
1257
|
-
rawPath,
|
|
1258
|
-
resolvedPath: targetPath
|
|
1259
|
-
});
|
|
1260
|
-
let metadata;
|
|
1261
|
-
try {
|
|
1262
|
-
metadata = await stat(targetPath);
|
|
1263
|
-
} catch (error) {
|
|
1264
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1265
|
-
skipped.push({
|
|
1266
|
-
path: targetPath,
|
|
1267
|
-
reason: `not readable: ${message}`
|
|
1268
|
-
});
|
|
1269
|
-
debug.emit("path.resolve.skip", {
|
|
1270
|
-
path: targetPath,
|
|
1271
|
-
reason: `not readable: ${message}`
|
|
1272
|
-
});
|
|
1102
|
+
const tableMatch = trimmedLine.match(/^\[([^\]]+)]$/);
|
|
1103
|
+
if (tableMatch) {
|
|
1104
|
+
const normalizedTable = normalizeKeyPath(tableMatch[1] ?? "");
|
|
1105
|
+
if (!normalizedTable) return null;
|
|
1106
|
+
tablePrefix = normalizedTable;
|
|
1107
|
+
tablePrefixInList = false;
|
|
1108
|
+
tableTarget = null;
|
|
1273
1109
|
continue;
|
|
1274
1110
|
}
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1111
|
+
const lineForParsing = /("""|''')/.test(rawLine) ? rawLine : stripInlineComment(rawLine);
|
|
1112
|
+
const separatorIndex = lineForParsing.indexOf("=");
|
|
1113
|
+
if (separatorIndex === -1) return null;
|
|
1114
|
+
const key = normalizeKeyPath(lineForParsing.slice(0, separatorIndex));
|
|
1115
|
+
let valueRaw = lineForParsing.slice(separatorIndex + 1).trim();
|
|
1116
|
+
if (!key) return null;
|
|
1117
|
+
const tripleDelimiter = valueRaw.startsWith("\"\"\"") ? "\"\"\"" : valueRaw.startsWith("'''") ? "'''" : null;
|
|
1118
|
+
if (tripleDelimiter) {
|
|
1119
|
+
const closingIndex = valueRaw.indexOf(tripleDelimiter, tripleDelimiter.length);
|
|
1120
|
+
if (closingIndex !== -1) {
|
|
1121
|
+
const strippedAfter = stripInlineComment(valueRaw.slice(closingIndex + tripleDelimiter.length));
|
|
1122
|
+
valueRaw = `${valueRaw.slice(0, closingIndex + tripleDelimiter.length)}${strippedAfter}`;
|
|
1123
|
+
} else {
|
|
1124
|
+
const delimiter = tripleDelimiter;
|
|
1125
|
+
let combined = valueRaw;
|
|
1126
|
+
let closed = false;
|
|
1127
|
+
while (index + 1 < lines.length) {
|
|
1128
|
+
index += 1;
|
|
1129
|
+
const nextLine = lines[index] ?? "";
|
|
1130
|
+
combined += `\n${nextLine}`;
|
|
1131
|
+
if (new RegExp(`${delimiter}\\s*$`).test(nextLine)) {
|
|
1132
|
+
closed = true;
|
|
1133
|
+
break;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
if (!closed) return null;
|
|
1137
|
+
valueRaw = combined;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
const normalized = normalizeValue(valueRaw);
|
|
1141
|
+
if (normalized === null) return null;
|
|
1142
|
+
const fullKey = tablePrefix ? `${tablePrefix}.${key}` : key;
|
|
1143
|
+
if (typeof normalized === "object" && !Array.isArray(normalized)) {
|
|
1144
|
+
for (const [inlineKey, inlineValue] of Object.entries(normalized)) {
|
|
1145
|
+
const entryKey = tablePrefixInList ? `${key}.${inlineKey}` : `${fullKey}.${inlineKey}`;
|
|
1146
|
+
if (tablePrefixInList && tableTarget) tableTarget[entryKey] = toPlainText(inlineValue);
|
|
1147
|
+
else result[entryKey] = toPlainText(inlineValue);
|
|
1148
|
+
}
|
|
1285
1149
|
continue;
|
|
1286
1150
|
}
|
|
1287
|
-
if (
|
|
1288
|
-
|
|
1289
|
-
path: targetPath,
|
|
1290
|
-
reason: "not a regular file"
|
|
1291
|
-
});
|
|
1292
|
-
debug.emit("path.resolve.skip", {
|
|
1293
|
-
path: targetPath,
|
|
1294
|
-
reason: "not a regular file"
|
|
1295
|
-
});
|
|
1151
|
+
if (tablePrefixInList && tableTarget) {
|
|
1152
|
+
tableTarget[key] = toPlainText(normalized);
|
|
1296
1153
|
continue;
|
|
1297
1154
|
}
|
|
1298
|
-
|
|
1299
|
-
source: "direct",
|
|
1300
|
-
input: targetPath
|
|
1301
|
-
});
|
|
1155
|
+
result[fullKey] = toPlainText(normalized);
|
|
1302
1156
|
}
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
excluded: stats.filterExcluded,
|
|
1306
|
-
included: stats.directoryIncluded
|
|
1307
|
-
});
|
|
1308
|
-
debug.emit("path.resolve.dedupe.summary", {
|
|
1309
|
-
accepted: stats.dedupeAccepted,
|
|
1310
|
-
duplicates: stats.dedupeDuplicates
|
|
1311
|
-
});
|
|
1312
|
-
debug.emit("path.resolve.complete", {
|
|
1313
|
-
files: files.length,
|
|
1314
|
-
skipped: skipped.length,
|
|
1315
|
-
ordering: "absolute-path-ascending"
|
|
1316
|
-
});
|
|
1317
|
-
return {
|
|
1318
|
-
files,
|
|
1319
|
-
skipped
|
|
1320
|
-
};
|
|
1157
|
+
flattenArrayTables(result);
|
|
1158
|
+
return result;
|
|
1321
1159
|
}
|
|
1322
1160
|
|
|
1323
1161
|
//#endregion
|
|
1324
|
-
//#region src/
|
|
1325
|
-
const
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
const safeTotal = Math.max(total, 1);
|
|
1333
|
-
const ratio = clamp(completed / safeTotal, 0, 1);
|
|
1334
|
-
const filled = completed >= safeTotal ? PROGRESS_BAR_WIDTH : Math.floor(ratio * PROGRESS_BAR_WIDTH);
|
|
1335
|
-
const empty = PROGRESS_BAR_WIDTH - filled;
|
|
1336
|
-
return `${FILLED_BAR_CHAR.repeat(filled)}${EMPTY_BAR_CHAR.repeat(empty)}`;
|
|
1162
|
+
//#region src/markdown/parse-markdown.ts
|
|
1163
|
+
const FENCE_TO_TYPE = {
|
|
1164
|
+
"---": "yaml",
|
|
1165
|
+
"+++": "toml",
|
|
1166
|
+
";;;": "json"
|
|
1167
|
+
};
|
|
1168
|
+
function normalizeNewlines(input) {
|
|
1169
|
+
return input.replace(/\r\n/g, "\n");
|
|
1337
1170
|
}
|
|
1338
|
-
function
|
|
1339
|
-
|
|
1340
|
-
const totalSeconds = Math.max(0, Math.floor(elapsedMs / 1e3));
|
|
1341
|
-
const minutes = Math.floor(totalSeconds / 60);
|
|
1342
|
-
const seconds = totalSeconds % 60;
|
|
1343
|
-
const tenths = Math.floor(Math.max(0, elapsedMs) % 1e3 / 100);
|
|
1344
|
-
return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}.${tenths}`;
|
|
1171
|
+
function stripBom(line) {
|
|
1172
|
+
return line.startsWith("") ? line.slice(1) : line;
|
|
1345
1173
|
}
|
|
1346
|
-
function
|
|
1347
|
-
const
|
|
1348
|
-
|
|
1349
|
-
return
|
|
1174
|
+
function getFenceType(line) {
|
|
1175
|
+
const match = line.match(/^[\t ]*(---|\+\+\+|;;;)[\t ]*$/);
|
|
1176
|
+
if (!match) return null;
|
|
1177
|
+
return FENCE_TO_TYPE[match[1] ?? ""] ?? null;
|
|
1350
1178
|
}
|
|
1351
|
-
function
|
|
1352
|
-
|
|
1179
|
+
function parseFrontmatter(frontmatter, type) {
|
|
1180
|
+
if (!type) return null;
|
|
1181
|
+
if (type === "json") try {
|
|
1182
|
+
return JSON.parse(frontmatter);
|
|
1183
|
+
} catch {
|
|
1184
|
+
return null;
|
|
1185
|
+
}
|
|
1186
|
+
if (type === "yaml") {
|
|
1187
|
+
const doc = parseDocument(frontmatter, { prettyErrors: false });
|
|
1188
|
+
if (doc.errors.length > 0) return null;
|
|
1189
|
+
const data = doc.toJSON();
|
|
1190
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return null;
|
|
1191
|
+
return data;
|
|
1192
|
+
}
|
|
1193
|
+
if (type === "toml") return parseTomlFrontmatter(frontmatter);
|
|
1194
|
+
return null;
|
|
1353
1195
|
}
|
|
1354
|
-
function
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
let
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
const writeTTYLine = (line) => {
|
|
1365
|
-
const trailingPadding = lastLineLength > line.length ? " ".repeat(lastLineLength - line.length) : "";
|
|
1366
|
-
options.stream.write(`\r${line}${trailingPadding}`);
|
|
1367
|
-
lastLineLength = line.length;
|
|
1368
|
-
};
|
|
1369
|
-
const render = (completed) => {
|
|
1370
|
-
const line = buildProgressLine(completed, total, startedAtMs);
|
|
1371
|
-
const safeTotal = Math.max(total, 1);
|
|
1372
|
-
const percent = completed >= safeTotal ? 100 : Math.floor(completed / safeTotal * 100);
|
|
1373
|
-
if (!isTTY && percent === lastRenderedPercent && completed < safeTotal) return;
|
|
1374
|
-
lastRenderedPercent = percent;
|
|
1375
|
-
if (isTTY) {
|
|
1376
|
-
writeTTYLine(line);
|
|
1377
|
-
return;
|
|
1378
|
-
}
|
|
1379
|
-
lastLineLength = line.length;
|
|
1380
|
-
options.stream.write(`${line}\n`);
|
|
1381
|
-
};
|
|
1382
|
-
const clearLine = () => {
|
|
1383
|
-
if (lastLineLength === 0) return;
|
|
1384
|
-
options.stream.write(`\r${" ".repeat(lastLineLength)}\r`);
|
|
1385
|
-
lastLineLength = 0;
|
|
1386
|
-
};
|
|
1387
|
-
return {
|
|
1388
|
-
enabled,
|
|
1389
|
-
start(nextTotal, nextStartedAtMs) {
|
|
1390
|
-
if (!enabled || nextTotal <= 1) return;
|
|
1391
|
-
total = nextTotal;
|
|
1392
|
-
active = true;
|
|
1393
|
-
startedAtMs = nextStartedAtMs ?? Date.now();
|
|
1394
|
-
lastRenderedPercent = -1;
|
|
1395
|
-
finalizingStarted = false;
|
|
1396
|
-
render(0);
|
|
1397
|
-
},
|
|
1398
|
-
advance(snapshot) {
|
|
1399
|
-
if (!active) return;
|
|
1400
|
-
render(snapshot.completed);
|
|
1401
|
-
},
|
|
1402
|
-
startFinalizing() {
|
|
1403
|
-
if (!active || finalizingStarted) return;
|
|
1404
|
-
finalizingStarted = true;
|
|
1405
|
-
const line = buildFinalizingLine(startedAtMs);
|
|
1406
|
-
if (isTTY) {
|
|
1407
|
-
if (!clearOnFinish) {
|
|
1408
|
-
options.stream.write(`\n${line}`);
|
|
1409
|
-
lastLineLength = line.length;
|
|
1410
|
-
return;
|
|
1411
|
-
}
|
|
1412
|
-
writeTTYLine(line);
|
|
1413
|
-
return;
|
|
1196
|
+
function extractJsonBlock(text, startIndex) {
|
|
1197
|
+
let depth = 0;
|
|
1198
|
+
let inString = false;
|
|
1199
|
+
let escaped = false;
|
|
1200
|
+
for (let i = startIndex; i < text.length; i += 1) {
|
|
1201
|
+
const char = text[i] ?? "";
|
|
1202
|
+
if (inString) {
|
|
1203
|
+
if (escaped) {
|
|
1204
|
+
escaped = false;
|
|
1205
|
+
continue;
|
|
1414
1206
|
}
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
if (
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1207
|
+
if (char === "\\") {
|
|
1208
|
+
escaped = true;
|
|
1209
|
+
continue;
|
|
1210
|
+
}
|
|
1211
|
+
if (char === "\"") inString = false;
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
if (char === "\"") {
|
|
1215
|
+
inString = true;
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
if (char === "{") {
|
|
1219
|
+
depth += 1;
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1222
|
+
if (char === "}") {
|
|
1223
|
+
depth -= 1;
|
|
1224
|
+
if (depth === 0) return {
|
|
1225
|
+
jsonText: text.slice(startIndex, i + 1),
|
|
1226
|
+
endIndex: i
|
|
1227
|
+
};
|
|
1423
1228
|
}
|
|
1229
|
+
}
|
|
1230
|
+
return null;
|
|
1231
|
+
}
|
|
1232
|
+
function parseMarkdown(input) {
|
|
1233
|
+
const normalized = normalizeNewlines(input);
|
|
1234
|
+
const lines = normalized.split("\n");
|
|
1235
|
+
if (lines.length === 0) return {
|
|
1236
|
+
frontmatter: null,
|
|
1237
|
+
content: normalized,
|
|
1238
|
+
data: null,
|
|
1239
|
+
frontmatterType: null
|
|
1240
|
+
};
|
|
1241
|
+
lines[0] = stripBom(lines[0] ?? "");
|
|
1242
|
+
const normalizedWithoutBom = lines.join("\n");
|
|
1243
|
+
const openingType = getFenceType(lines[0] ?? "");
|
|
1244
|
+
if (!openingType) {
|
|
1245
|
+
const jsonStart = (normalizedWithoutBom.match(/^[\t \n]*/)?.[0] ?? "").length;
|
|
1246
|
+
if (normalizedWithoutBom[jsonStart] !== "{") return {
|
|
1247
|
+
frontmatter: null,
|
|
1248
|
+
content: normalizedWithoutBom,
|
|
1249
|
+
data: null,
|
|
1250
|
+
frontmatterType: null
|
|
1251
|
+
};
|
|
1252
|
+
const jsonBlock = extractJsonBlock(normalizedWithoutBom, jsonStart);
|
|
1253
|
+
if (!jsonBlock) return {
|
|
1254
|
+
frontmatter: null,
|
|
1255
|
+
content: normalizedWithoutBom,
|
|
1256
|
+
data: null,
|
|
1257
|
+
frontmatterType: null
|
|
1258
|
+
};
|
|
1259
|
+
const frontmatter = jsonBlock.jsonText;
|
|
1260
|
+
let content = normalizedWithoutBom.slice(jsonBlock.endIndex + 1);
|
|
1261
|
+
if (content.startsWith("\n")) content = content.slice(1);
|
|
1262
|
+
const data = parseFrontmatter(frontmatter, "json");
|
|
1263
|
+
if (!data) return {
|
|
1264
|
+
frontmatter: null,
|
|
1265
|
+
content: normalizedWithoutBom,
|
|
1266
|
+
data: null,
|
|
1267
|
+
frontmatterType: null
|
|
1268
|
+
};
|
|
1269
|
+
return {
|
|
1270
|
+
frontmatter,
|
|
1271
|
+
content,
|
|
1272
|
+
data,
|
|
1273
|
+
frontmatterType: "json"
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
let closingIndex = -1;
|
|
1277
|
+
for (let i = 1; i < lines.length; i += 1) if (getFenceType(lines[i] ?? "") === openingType) {
|
|
1278
|
+
closingIndex = i;
|
|
1279
|
+
break;
|
|
1280
|
+
}
|
|
1281
|
+
if (closingIndex === -1) return {
|
|
1282
|
+
frontmatter: null,
|
|
1283
|
+
content: normalizedWithoutBom,
|
|
1284
|
+
data: null,
|
|
1285
|
+
frontmatterType: null
|
|
1286
|
+
};
|
|
1287
|
+
const frontmatter = lines.slice(1, closingIndex).join("\n");
|
|
1288
|
+
return {
|
|
1289
|
+
frontmatter,
|
|
1290
|
+
content: lines.slice(closingIndex + 1).join("\n"),
|
|
1291
|
+
data: parseFrontmatter(frontmatter, openingType),
|
|
1292
|
+
frontmatterType: openingType
|
|
1424
1293
|
};
|
|
1425
1294
|
}
|
|
1426
1295
|
|
|
1427
1296
|
//#endregion
|
|
1428
|
-
//#region src/
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
const
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1297
|
+
//#region src/wc/segmenter.ts
|
|
1298
|
+
const segmenterCache = /* @__PURE__ */ new Map();
|
|
1299
|
+
const graphemeSegmenterCache = /* @__PURE__ */ new Map();
|
|
1300
|
+
function getSegmenter(locale) {
|
|
1301
|
+
const cached = segmenterCache.get(locale);
|
|
1302
|
+
if (cached) return cached;
|
|
1303
|
+
const segmenter = new Intl.Segmenter(locale, { granularity: "word" });
|
|
1304
|
+
segmenterCache.set(locale, segmenter);
|
|
1305
|
+
return segmenter;
|
|
1306
|
+
}
|
|
1307
|
+
function getGraphemeSegmenter(locale) {
|
|
1308
|
+
const cached = graphemeSegmenterCache.get(locale);
|
|
1309
|
+
if (cached) return cached;
|
|
1310
|
+
const segmenter = new Intl.Segmenter(locale, { granularity: "grapheme" });
|
|
1311
|
+
graphemeSegmenterCache.set(locale, segmenter);
|
|
1312
|
+
return segmenter;
|
|
1313
|
+
}
|
|
1314
|
+
function supportsSegmenter() {
|
|
1315
|
+
return typeof Intl !== "undefined" && typeof Intl.Segmenter === "function";
|
|
1316
|
+
}
|
|
1317
|
+
function countCharsForLocale(text, locale) {
|
|
1318
|
+
if (!supportsSegmenter()) return Array.from(text).length;
|
|
1319
|
+
const segmenter = getGraphemeSegmenter(locale);
|
|
1320
|
+
let count = 0;
|
|
1321
|
+
for (const _segment of segmenter.segment(text)) count++;
|
|
1322
|
+
return count;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
//#endregion
|
|
1326
|
+
//#region src/wc/non-words.ts
|
|
1327
|
+
const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation})/u;
|
|
1328
|
+
const emojiPresentationRegex = /\p{Emoji_Presentation}/u;
|
|
1329
|
+
const keycapEmojiRegex = /[0-9#*]\uFE0F?\u20E3/u;
|
|
1330
|
+
const symbolRegex = /\p{S}/u;
|
|
1331
|
+
const punctuationRegex = /\p{P}/u;
|
|
1332
|
+
const whitespaceRegex = /\s/u;
|
|
1333
|
+
const newlineChars = new Set([
|
|
1334
|
+
"\n",
|
|
1335
|
+
"\r",
|
|
1336
|
+
"\u2028",
|
|
1337
|
+
"\u2029"
|
|
1338
|
+
]);
|
|
1339
|
+
function createNonWordCollection() {
|
|
1340
|
+
return {
|
|
1341
|
+
emoji: [],
|
|
1342
|
+
symbols: [],
|
|
1343
|
+
punctuation: [],
|
|
1344
|
+
counts: {
|
|
1345
|
+
emoji: 0,
|
|
1346
|
+
symbols: 0,
|
|
1347
|
+
punctuation: 0
|
|
1443
1348
|
}
|
|
1444
1349
|
};
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1350
|
+
}
|
|
1351
|
+
function addNonWord(collection, category, segment) {
|
|
1352
|
+
if (category === "emoji") {
|
|
1353
|
+
collection.emoji.push(segment);
|
|
1354
|
+
collection.counts.emoji += 1;
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
if (category === "symbol") {
|
|
1358
|
+
collection.symbols.push(segment);
|
|
1359
|
+
collection.counts.symbols += 1;
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
collection.punctuation.push(segment);
|
|
1363
|
+
collection.counts.punctuation += 1;
|
|
1364
|
+
}
|
|
1365
|
+
function addWhitespace(collection, segment) {
|
|
1366
|
+
let whitespace = collection.whitespace;
|
|
1367
|
+
let count = 0;
|
|
1368
|
+
for (const char of segment) {
|
|
1369
|
+
if (char === " ") {
|
|
1370
|
+
whitespace = whitespace ?? createWhitespaceCounts();
|
|
1371
|
+
whitespace.spaces += 1;
|
|
1372
|
+
count += 1;
|
|
1373
|
+
continue;
|
|
1451
1374
|
}
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
mode: "char",
|
|
1458
|
-
items: [...left.breakdown.items, ...right.breakdown.items]
|
|
1375
|
+
if (char === " ") {
|
|
1376
|
+
whitespace = whitespace ?? createWhitespaceCounts();
|
|
1377
|
+
whitespace.tabs += 1;
|
|
1378
|
+
count += 1;
|
|
1379
|
+
continue;
|
|
1459
1380
|
}
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
for (const item of items) {
|
|
1466
|
-
const existing = mergedByLocale.get(item.locale);
|
|
1467
|
-
if (existing) {
|
|
1468
|
-
existing.words += item.words;
|
|
1469
|
-
if (preserveCollectorSegments) appendAll(existing.segments, item.segments);
|
|
1470
|
-
continue;
|
|
1471
|
-
}
|
|
1472
|
-
localeOrder.push(item.locale);
|
|
1473
|
-
mergedByLocale.set(item.locale, {
|
|
1474
|
-
locale: item.locale,
|
|
1475
|
-
words: item.words,
|
|
1476
|
-
segments: preserveCollectorSegments ? [...item.segments] : []
|
|
1477
|
-
});
|
|
1478
|
-
}
|
|
1479
|
-
};
|
|
1480
|
-
addItems(left.breakdown.items);
|
|
1481
|
-
addItems(right.breakdown.items);
|
|
1482
|
-
let mergedNonWords;
|
|
1483
|
-
if (left.breakdown.nonWords || right.breakdown.nonWords) {
|
|
1484
|
-
mergedNonWords = createNonWordCollection();
|
|
1485
|
-
if (left.breakdown.nonWords) mergeNonWordCollections(mergedNonWords, left.breakdown.nonWords);
|
|
1486
|
-
if (right.breakdown.nonWords) mergeNonWordCollections(mergedNonWords, right.breakdown.nonWords);
|
|
1381
|
+
if (newlineChars.has(char)) {
|
|
1382
|
+
whitespace = whitespace ?? createWhitespaceCounts();
|
|
1383
|
+
whitespace.newlines += 1;
|
|
1384
|
+
count += 1;
|
|
1385
|
+
continue;
|
|
1487
1386
|
}
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1387
|
+
if (whitespaceRegex.test(char)) {
|
|
1388
|
+
whitespace = whitespace ?? createWhitespaceCounts();
|
|
1389
|
+
whitespace.other += 1;
|
|
1390
|
+
count += 1;
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
if (count > 0) {
|
|
1394
|
+
collection.whitespace = whitespace ?? createWhitespaceCounts();
|
|
1395
|
+
collection.counts.whitespace = (collection.counts.whitespace ?? 0) + count;
|
|
1396
|
+
}
|
|
1397
|
+
return count;
|
|
1398
|
+
}
|
|
1399
|
+
function classifyNonWordSegment(segment) {
|
|
1400
|
+
const hasEmojiVariationSelector = segment.includes("️");
|
|
1401
|
+
if (keycapEmojiRegex.test(segment) || emojiPresentationRegex.test(segment) || hasEmojiVariationSelector && emojiRegex.test(segment)) return "emoji";
|
|
1402
|
+
if (symbolRegex.test(segment)) return "symbol";
|
|
1403
|
+
if (punctuationRegex.test(segment)) return "punctuation";
|
|
1404
|
+
return null;
|
|
1405
|
+
}
|
|
1406
|
+
function mergeNonWordCollections(target, source) {
|
|
1407
|
+
if (source.counts.emoji > 0) {
|
|
1408
|
+
appendAll(target.emoji, source.emoji);
|
|
1409
|
+
target.counts.emoji += source.counts.emoji;
|
|
1410
|
+
}
|
|
1411
|
+
if (source.counts.symbols > 0) {
|
|
1412
|
+
appendAll(target.symbols, source.symbols);
|
|
1413
|
+
target.counts.symbols += source.counts.symbols;
|
|
1414
|
+
}
|
|
1415
|
+
if (source.counts.punctuation > 0) {
|
|
1416
|
+
appendAll(target.punctuation, source.punctuation);
|
|
1417
|
+
target.counts.punctuation += source.counts.punctuation;
|
|
1418
|
+
}
|
|
1419
|
+
if (source.counts.whitespace && source.counts.whitespace > 0 && source.whitespace) {
|
|
1420
|
+
const whitespace = target.whitespace ?? createWhitespaceCounts();
|
|
1421
|
+
whitespace.spaces += source.whitespace.spaces;
|
|
1422
|
+
whitespace.tabs += source.whitespace.tabs;
|
|
1423
|
+
whitespace.newlines += source.whitespace.newlines;
|
|
1424
|
+
whitespace.other += source.whitespace.other;
|
|
1425
|
+
target.whitespace = whitespace;
|
|
1426
|
+
target.counts.whitespace = (target.counts.whitespace ?? 0) + source.counts.whitespace;
|
|
1501
1427
|
}
|
|
1428
|
+
return target;
|
|
1429
|
+
}
|
|
1430
|
+
function createWhitespaceCounts() {
|
|
1502
1431
|
return {
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1432
|
+
spaces: 0,
|
|
1433
|
+
tabs: 0,
|
|
1434
|
+
newlines: 0,
|
|
1435
|
+
other: 0
|
|
1506
1436
|
};
|
|
1507
1437
|
}
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1438
|
+
|
|
1439
|
+
//#endregion
|
|
1440
|
+
//#region src/wc/analyze.ts
|
|
1441
|
+
function analyzeChunk(chunk, collectNonWords, includeWhitespace) {
|
|
1442
|
+
const segmenter = getSegmenter(chunk.locale);
|
|
1443
|
+
const segments = [];
|
|
1444
|
+
const nonWords = collectNonWords ? createNonWordCollection() : null;
|
|
1445
|
+
for (const part of segmenter.segment(chunk.text)) if (part.isWordLike) segments.push(part.segment);
|
|
1446
|
+
else if (collectNonWords && nonWords) {
|
|
1447
|
+
if (includeWhitespace) addWhitespace(nonWords, part.segment);
|
|
1448
|
+
const category = classifyNonWordSegment(part.segment);
|
|
1449
|
+
if (category) addNonWord(nonWords, category, part.segment);
|
|
1517
1450
|
}
|
|
1518
|
-
return
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
if (results.length === 0) return {
|
|
1525
|
-
section: "all",
|
|
1526
|
-
total: 0,
|
|
1527
|
-
frontmatterType: null,
|
|
1528
|
-
items: []
|
|
1451
|
+
return {
|
|
1452
|
+
locale: chunk.locale,
|
|
1453
|
+
text: chunk.text,
|
|
1454
|
+
segments,
|
|
1455
|
+
words: segments.length,
|
|
1456
|
+
nonWords: nonWords ?? void 0
|
|
1529
1457
|
};
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
const
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1458
|
+
}
|
|
1459
|
+
function analyzeCharChunk(chunk, collectNonWords, includeWhitespace) {
|
|
1460
|
+
const segmenter = getSegmenter(chunk.locale);
|
|
1461
|
+
const nonWords = collectNonWords ? createNonWordCollection() : null;
|
|
1462
|
+
let chars = 0;
|
|
1463
|
+
let wordChars = 0;
|
|
1464
|
+
let nonWordChars = 0;
|
|
1465
|
+
for (const part of segmenter.segment(chunk.text)) {
|
|
1466
|
+
if (part.isWordLike) {
|
|
1467
|
+
const count = countCharsForLocale(part.segment, chunk.locale);
|
|
1468
|
+
chars += count;
|
|
1469
|
+
wordChars += count;
|
|
1470
|
+
continue;
|
|
1471
|
+
}
|
|
1472
|
+
if (collectNonWords && nonWords) {
|
|
1473
|
+
let whitespaceCount = 0;
|
|
1474
|
+
if (includeWhitespace) whitespaceCount = addWhitespace(nonWords, part.segment);
|
|
1475
|
+
const category = classifyNonWordSegment(part.segment);
|
|
1476
|
+
if (category) addNonWord(nonWords, category, part.segment);
|
|
1477
|
+
if (category || whitespaceCount > 0) {
|
|
1478
|
+
const count = countCharsForLocale(part.segment, chunk.locale);
|
|
1479
|
+
chars += count;
|
|
1480
|
+
nonWordChars += count;
|
|
1548
1481
|
}
|
|
1549
|
-
existing.items.push(item.result);
|
|
1550
1482
|
}
|
|
1551
1483
|
}
|
|
1552
|
-
const sourceOrder = new Map([["frontmatter", 0], ["content", 1]]);
|
|
1553
|
-
const items = [...grouped.values()].sort((left, right) => {
|
|
1554
|
-
const sourceDiff = (sourceOrder.get(left.source) ?? 0) - (sourceOrder.get(right.source) ?? 0);
|
|
1555
|
-
if (sourceDiff !== 0) return sourceDiff;
|
|
1556
|
-
return left.name.localeCompare(right.name);
|
|
1557
|
-
}).map((entry) => ({
|
|
1558
|
-
name: entry.name,
|
|
1559
|
-
source: entry.source,
|
|
1560
|
-
result: aggregateWordCounterResults(entry.items, preserveCollectorSegments)
|
|
1561
|
-
}));
|
|
1562
1484
|
return {
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1485
|
+
locale: chunk.locale,
|
|
1486
|
+
text: chunk.text,
|
|
1487
|
+
chars,
|
|
1488
|
+
wordChars,
|
|
1489
|
+
nonWordChars,
|
|
1490
|
+
nonWords: nonWords ?? void 0
|
|
1567
1491
|
};
|
|
1568
1492
|
}
|
|
1569
|
-
function
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1493
|
+
function aggregateCharsByLocale(chunks) {
|
|
1494
|
+
const order = [];
|
|
1495
|
+
const map = /* @__PURE__ */ new Map();
|
|
1496
|
+
for (const chunk of chunks) {
|
|
1497
|
+
const existing = map.get(chunk.locale);
|
|
1498
|
+
if (existing) {
|
|
1499
|
+
existing.chars += chunk.chars;
|
|
1500
|
+
existing.wordChars += chunk.wordChars;
|
|
1501
|
+
existing.nonWordChars += chunk.nonWordChars;
|
|
1502
|
+
if (chunk.nonWords) {
|
|
1503
|
+
if (!existing.nonWords) existing.nonWords = createNonWordCollection();
|
|
1504
|
+
mergeNonWordCollections(existing.nonWords, chunk.nonWords);
|
|
1505
|
+
}
|
|
1506
|
+
continue;
|
|
1507
|
+
}
|
|
1508
|
+
order.push(chunk.locale);
|
|
1509
|
+
map.set(chunk.locale, {
|
|
1510
|
+
locale: chunk.locale,
|
|
1511
|
+
chars: chunk.chars,
|
|
1512
|
+
wordChars: chunk.wordChars,
|
|
1513
|
+
nonWordChars: chunk.nonWordChars,
|
|
1514
|
+
nonWords: chunk.nonWords ? mergeNonWordCollections(createNonWordCollection(), chunk.nonWords) : void 0
|
|
1590
1515
|
});
|
|
1591
1516
|
}
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1517
|
+
return order.map((locale) => map.get(locale));
|
|
1518
|
+
}
|
|
1519
|
+
function aggregateByLocale(chunks) {
|
|
1520
|
+
const order = [];
|
|
1521
|
+
const map = /* @__PURE__ */ new Map();
|
|
1522
|
+
for (const chunk of chunks) {
|
|
1523
|
+
const existing = map.get(chunk.locale);
|
|
1524
|
+
if (existing) {
|
|
1525
|
+
existing.words += chunk.words;
|
|
1526
|
+
appendAll(existing.segments, chunk.segments);
|
|
1527
|
+
continue;
|
|
1601
1528
|
}
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1529
|
+
order.push(chunk.locale);
|
|
1530
|
+
map.set(chunk.locale, {
|
|
1531
|
+
locale: chunk.locale,
|
|
1532
|
+
words: chunk.words,
|
|
1533
|
+
segments: [...chunk.segments]
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
return order.map((locale) => map.get(locale));
|
|
1608
1537
|
}
|
|
1609
1538
|
|
|
1610
1539
|
//#endregion
|
|
1611
|
-
//#region src/
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
} finally {
|
|
1677
|
-
if (progressEnabled) options.progressReporter.finish();
|
|
1678
|
-
options.debug.emit("batch.progress.complete", {
|
|
1679
|
-
enabled: progressEnabled,
|
|
1680
|
-
total: loaded.files.length
|
|
1681
|
-
});
|
|
1682
|
-
}
|
|
1683
|
-
if (!emittedCountTiming) {
|
|
1684
|
-
const countElapsedMs = Date.now() - countStartedAtMs;
|
|
1685
|
-
options.debug.emit("batch.stage.timing", {
|
|
1686
|
-
stage: "count",
|
|
1687
|
-
elapsedMs: countElapsedMs
|
|
1688
|
-
});
|
|
1689
|
-
}
|
|
1690
|
-
const finalizeElapsedMs = finalizeStartedAtMs === null ? 0 : Date.now() - finalizeStartedAtMs;
|
|
1691
|
-
options.debug.emit("batch.stage.timing", {
|
|
1692
|
-
stage: "finalize",
|
|
1693
|
-
elapsedMs: finalizeElapsedMs
|
|
1694
|
-
});
|
|
1695
|
-
appendAll(summary.skipped, resolved.skipped);
|
|
1696
|
-
appendAll(summary.skipped, loaded.skipped);
|
|
1697
|
-
options.debug.emit("batch.aggregate.complete", {
|
|
1698
|
-
files: summary.files.length,
|
|
1699
|
-
skipped: summary.skipped.length,
|
|
1700
|
-
total: summary.aggregate.total
|
|
1701
|
-
});
|
|
1702
|
-
return summary;
|
|
1540
|
+
//#region src/wc/mode.ts
|
|
1541
|
+
const MODE_ALIASES = {
|
|
1542
|
+
chunk: "chunk",
|
|
1543
|
+
chunks: "chunk",
|
|
1544
|
+
segments: "segments",
|
|
1545
|
+
segment: "segments",
|
|
1546
|
+
seg: "segments",
|
|
1547
|
+
collector: "collector",
|
|
1548
|
+
collect: "collector",
|
|
1549
|
+
colle: "collector",
|
|
1550
|
+
char: "char",
|
|
1551
|
+
chars: "char",
|
|
1552
|
+
character: "char",
|
|
1553
|
+
characters: "char",
|
|
1554
|
+
"char-collector": "char-collector"
|
|
1555
|
+
};
|
|
1556
|
+
const CHAR_MODE_ALIASES = new Set([
|
|
1557
|
+
"char",
|
|
1558
|
+
"chars",
|
|
1559
|
+
"character",
|
|
1560
|
+
"characters"
|
|
1561
|
+
]);
|
|
1562
|
+
const COLLECTOR_MODE_ALIASES = new Set([
|
|
1563
|
+
"collector",
|
|
1564
|
+
"collect",
|
|
1565
|
+
"colle",
|
|
1566
|
+
"col"
|
|
1567
|
+
]);
|
|
1568
|
+
function collapseSeparators(value) {
|
|
1569
|
+
return value.replace(/[-_\s]+/g, "");
|
|
1570
|
+
}
|
|
1571
|
+
function isComposedCharCollectorFromTokens(value) {
|
|
1572
|
+
const tokens = value.split(/[-_\s]+/).map((token) => token.trim()).filter((token) => token.length > 0);
|
|
1573
|
+
if (tokens.length < 2) return false;
|
|
1574
|
+
let hasCharAlias = false;
|
|
1575
|
+
let hasCollectorAlias = false;
|
|
1576
|
+
for (const token of tokens) {
|
|
1577
|
+
if (CHAR_MODE_ALIASES.has(token)) {
|
|
1578
|
+
hasCharAlias = true;
|
|
1579
|
+
continue;
|
|
1580
|
+
}
|
|
1581
|
+
if (COLLECTOR_MODE_ALIASES.has(token)) {
|
|
1582
|
+
hasCollectorAlias = true;
|
|
1583
|
+
continue;
|
|
1584
|
+
}
|
|
1585
|
+
return false;
|
|
1586
|
+
}
|
|
1587
|
+
return hasCharAlias && hasCollectorAlias;
|
|
1588
|
+
}
|
|
1589
|
+
function isComposedCharCollectorCompact(value) {
|
|
1590
|
+
for (const charAlias of CHAR_MODE_ALIASES) for (const collectorAlias of COLLECTOR_MODE_ALIASES) if (value === `${charAlias}${collectorAlias}` || value === `${collectorAlias}${charAlias}`) return true;
|
|
1591
|
+
return false;
|
|
1592
|
+
}
|
|
1593
|
+
function normalizeMode(input) {
|
|
1594
|
+
if (!input) return null;
|
|
1595
|
+
const normalized = input.trim().toLowerCase();
|
|
1596
|
+
const direct = MODE_ALIASES[normalized];
|
|
1597
|
+
if (direct) return direct;
|
|
1598
|
+
if (isComposedCharCollectorFromTokens(normalized)) return "char-collector";
|
|
1599
|
+
const compact = collapseSeparators(normalized);
|
|
1600
|
+
if (isComposedCharCollectorCompact(compact)) return "char-collector";
|
|
1601
|
+
return MODE_ALIASES[compact] ?? null;
|
|
1602
|
+
}
|
|
1603
|
+
function resolveMode(input, fallback = "chunk") {
|
|
1604
|
+
return normalizeMode(input) ?? fallback;
|
|
1703
1605
|
}
|
|
1704
1606
|
|
|
1705
1607
|
//#endregion
|
|
1706
|
-
//#region src/
|
|
1707
|
-
const
|
|
1708
|
-
|
|
1709
|
-
|
|
1608
|
+
//#region src/wc/locale-detect.ts
|
|
1609
|
+
const DEFAULT_LOCALE = "und-Latn";
|
|
1610
|
+
const DEFAULT_HAN_TAG = "zh-Hani";
|
|
1611
|
+
const regex = {
|
|
1612
|
+
hiragana: /\p{Script=Hiragana}/u,
|
|
1613
|
+
katakana: /\p{Script=Katakana}/u,
|
|
1614
|
+
hangul: /\p{Script=Hangul}/u,
|
|
1615
|
+
han: /\p{Script=Han}/u,
|
|
1616
|
+
latin: /\p{Script=Latin}/u,
|
|
1617
|
+
arabic: /\p{Script=Arabic}/u,
|
|
1618
|
+
cyrillic: /\p{Script=Cyrillic}/u,
|
|
1619
|
+
devanagari: /\p{Script=Devanagari}/u,
|
|
1620
|
+
thai: /\p{Script=Thai}/u
|
|
1621
|
+
};
|
|
1622
|
+
const latinLocaleHints = [
|
|
1623
|
+
{
|
|
1624
|
+
locale: "de",
|
|
1625
|
+
regex: /[äöüÄÖÜß]/
|
|
1626
|
+
},
|
|
1627
|
+
{
|
|
1628
|
+
locale: "es",
|
|
1629
|
+
regex: /[ñÑ¿¡]/
|
|
1630
|
+
},
|
|
1631
|
+
{
|
|
1632
|
+
locale: "pt",
|
|
1633
|
+
regex: /[ãõÃÕ]/
|
|
1634
|
+
},
|
|
1635
|
+
{
|
|
1636
|
+
locale: "fr",
|
|
1637
|
+
regex: /[œŒæÆ]/
|
|
1638
|
+
}
|
|
1639
|
+
];
|
|
1640
|
+
const latinLocales = new Set([DEFAULT_LOCALE, ...latinLocaleHints.map((hint) => hint.locale)]);
|
|
1641
|
+
function isLatinLocale(locale) {
|
|
1642
|
+
return latinLocales.has(locale);
|
|
1710
1643
|
}
|
|
1711
|
-
function
|
|
1712
|
-
|
|
1644
|
+
function detectLatinLocale(char) {
|
|
1645
|
+
for (const hint of latinLocaleHints) if (hint.regex.test(char)) return hint.locale;
|
|
1646
|
+
return DEFAULT_LOCALE;
|
|
1713
1647
|
}
|
|
1714
|
-
function
|
|
1715
|
-
|
|
1648
|
+
function resolveLatinHint(options) {
|
|
1649
|
+
const latinTagHint = options.latinTagHint?.trim();
|
|
1650
|
+
if (latinTagHint) return latinTagHint;
|
|
1651
|
+
const latinLanguageHint = options.latinLanguageHint?.trim();
|
|
1652
|
+
if (latinLanguageHint) return latinLanguageHint;
|
|
1653
|
+
const latinLocaleHint = options.latinLocaleHint?.trim();
|
|
1654
|
+
if (latinLocaleHint) return latinLocaleHint;
|
|
1716
1655
|
}
|
|
1717
|
-
function
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
const
|
|
1721
|
-
|
|
1656
|
+
function resolveHanHint(options) {
|
|
1657
|
+
const hanTagHint = options.hanTagHint?.trim();
|
|
1658
|
+
if (hanTagHint) return hanTagHint;
|
|
1659
|
+
const hanLanguageHint = options.hanLanguageHint?.trim();
|
|
1660
|
+
if (hanLanguageHint) return hanLanguageHint;
|
|
1722
1661
|
}
|
|
1723
|
-
function
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
if (
|
|
1730
|
-
|
|
1731
|
-
return
|
|
1662
|
+
function detectLocaleForChar(char, previousLocale, options = {}) {
|
|
1663
|
+
if (regex.hiragana.test(char) || regex.katakana.test(char)) return "ja";
|
|
1664
|
+
if (regex.hangul.test(char)) return "ko";
|
|
1665
|
+
if (regex.arabic.test(char)) return "ar";
|
|
1666
|
+
if (regex.cyrillic.test(char)) return "ru";
|
|
1667
|
+
if (regex.devanagari.test(char)) return "hi";
|
|
1668
|
+
if (regex.thai.test(char)) return "th";
|
|
1669
|
+
if (regex.han.test(char)) {
|
|
1670
|
+
if (previousLocale && previousLocale.startsWith("ja")) return previousLocale;
|
|
1671
|
+
return resolveHanHint(options) ?? DEFAULT_HAN_TAG;
|
|
1732
1672
|
}
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1673
|
+
if (regex.latin.test(char)) {
|
|
1674
|
+
const hintedLocale = detectLatinLocale(char);
|
|
1675
|
+
if (hintedLocale !== DEFAULT_LOCALE) return hintedLocale;
|
|
1676
|
+
if (previousLocale && isLatinLocale(previousLocale) && previousLocale !== DEFAULT_LOCALE) return previousLocale;
|
|
1677
|
+
const latinHint = resolveLatinHint(options);
|
|
1678
|
+
if (latinHint) return latinHint;
|
|
1679
|
+
return DEFAULT_LOCALE;
|
|
1738
1680
|
}
|
|
1739
|
-
return
|
|
1740
|
-
}
|
|
1741
|
-
function createTerminalSink() {
|
|
1742
|
-
return {
|
|
1743
|
-
write(line) {
|
|
1744
|
-
console.error(`[debug] ${line}`);
|
|
1745
|
-
},
|
|
1746
|
-
close: NOOP_CLOSE
|
|
1747
|
-
};
|
|
1681
|
+
return null;
|
|
1748
1682
|
}
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1683
|
+
|
|
1684
|
+
//#endregion
|
|
1685
|
+
//#region src/wc/segment.ts
|
|
1686
|
+
function segmentTextByLocale(text, options = {}) {
|
|
1687
|
+
const chunks = [];
|
|
1688
|
+
let currentLocale = DEFAULT_LOCALE;
|
|
1689
|
+
let buffer = "";
|
|
1690
|
+
let bufferHasScript = false;
|
|
1691
|
+
for (const char of text) {
|
|
1692
|
+
const detected = detectLocaleForChar(char, currentLocale, options);
|
|
1693
|
+
const targetLocale = detected ?? currentLocale;
|
|
1694
|
+
if (buffer === "") {
|
|
1695
|
+
currentLocale = targetLocale;
|
|
1696
|
+
buffer = char;
|
|
1697
|
+
bufferHasScript = detected !== null;
|
|
1698
|
+
continue;
|
|
1699
|
+
}
|
|
1700
|
+
if (detected !== null && !bufferHasScript) {
|
|
1701
|
+
currentLocale = targetLocale;
|
|
1702
|
+
buffer += char;
|
|
1703
|
+
bufferHasScript = true;
|
|
1704
|
+
continue;
|
|
1705
|
+
}
|
|
1706
|
+
if (targetLocale !== currentLocale && detected !== null) {
|
|
1707
|
+
if (currentLocale === DEFAULT_LOCALE && isLatinLocale(targetLocale)) {
|
|
1708
|
+
currentLocale = targetLocale;
|
|
1709
|
+
buffer += char;
|
|
1710
|
+
bufferHasScript = true;
|
|
1711
|
+
continue;
|
|
1712
|
+
}
|
|
1713
|
+
chunks.push({
|
|
1714
|
+
locale: currentLocale,
|
|
1715
|
+
text: buffer
|
|
1716
|
+
});
|
|
1717
|
+
currentLocale = targetLocale;
|
|
1718
|
+
buffer = char;
|
|
1719
|
+
bufferHasScript = true;
|
|
1720
|
+
continue;
|
|
1721
|
+
}
|
|
1722
|
+
buffer += char;
|
|
1723
|
+
if (detected !== null) bufferHasScript = true;
|
|
1755
1724
|
}
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1725
|
+
if (buffer.length > 0) chunks.push({
|
|
1726
|
+
locale: currentLocale,
|
|
1727
|
+
text: buffer
|
|
1759
1728
|
});
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1729
|
+
return mergeAdjacentChunks(chunks);
|
|
1730
|
+
}
|
|
1731
|
+
function mergeAdjacentChunks(chunks) {
|
|
1732
|
+
if (chunks.length === 0) return chunks;
|
|
1733
|
+
const merged = [];
|
|
1734
|
+
let last = chunks[0];
|
|
1735
|
+
for (let i = 1; i < chunks.length; i++) {
|
|
1736
|
+
const chunk = chunks[i];
|
|
1737
|
+
if (chunk.locale === last.locale) last = {
|
|
1738
|
+
locale: last.locale,
|
|
1739
|
+
text: last.text + chunk.text
|
|
1740
|
+
};
|
|
1741
|
+
else {
|
|
1742
|
+
merged.push(last);
|
|
1743
|
+
last = chunk;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
merged.push(last);
|
|
1747
|
+
return merged;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
//#endregion
|
|
1751
|
+
//#region src/wc/wc.ts
|
|
1752
|
+
function wordCounter(text, options = {}) {
|
|
1753
|
+
const mode = resolveMode(options.mode, "chunk");
|
|
1754
|
+
const collectNonWords = Boolean(options.nonWords);
|
|
1755
|
+
const includeWhitespace = Boolean(options.includeWhitespace);
|
|
1756
|
+
const chunks = segmentTextByLocale(text, {
|
|
1757
|
+
latinLanguageHint: options.latinLanguageHint,
|
|
1758
|
+
latinTagHint: options.latinTagHint,
|
|
1759
|
+
latinLocaleHint: options.latinLocaleHint,
|
|
1760
|
+
hanLanguageHint: options.hanLanguageHint,
|
|
1761
|
+
hanTagHint: options.hanTagHint
|
|
1763
1762
|
});
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1763
|
+
if (mode === "char" || mode === "char-collector") {
|
|
1764
|
+
const analyzed = chunks.map((chunk) => analyzeCharChunk(chunk, collectNonWords, includeWhitespace));
|
|
1765
|
+
const total = analyzed.reduce((sum, chunk) => sum + chunk.chars, 0);
|
|
1766
|
+
const counts = collectNonWords ? {
|
|
1767
|
+
words: analyzed.reduce((sum, chunk) => sum + chunk.wordChars, 0),
|
|
1768
|
+
nonWords: analyzed.reduce((sum, chunk) => sum + chunk.nonWordChars, 0),
|
|
1769
|
+
total
|
|
1770
|
+
} : void 0;
|
|
1771
|
+
if (mode === "char") return {
|
|
1772
|
+
total,
|
|
1773
|
+
counts,
|
|
1774
|
+
breakdown: {
|
|
1775
|
+
mode,
|
|
1776
|
+
items: analyzed.map((chunk) => ({
|
|
1777
|
+
locale: chunk.locale,
|
|
1778
|
+
text: chunk.text,
|
|
1779
|
+
chars: chunk.chars,
|
|
1780
|
+
nonWords: chunk.nonWords
|
|
1781
|
+
}))
|
|
1782
|
+
}
|
|
1783
|
+
};
|
|
1784
|
+
return {
|
|
1785
|
+
total,
|
|
1786
|
+
counts,
|
|
1787
|
+
breakdown: {
|
|
1788
|
+
mode,
|
|
1789
|
+
items: aggregateCharsByLocale(analyzed).map((chunk) => ({
|
|
1790
|
+
locale: chunk.locale,
|
|
1791
|
+
chars: chunk.chars,
|
|
1792
|
+
nonWords: chunk.nonWords
|
|
1793
|
+
}))
|
|
1794
|
+
}
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
const analyzed = chunks.map((chunk) => analyzeChunk(chunk, collectNonWords, includeWhitespace));
|
|
1798
|
+
const wordsTotal = analyzed.reduce((sum, chunk) => sum + chunk.words, 0);
|
|
1799
|
+
const nonWordsTotal = collectNonWords ? analyzed.reduce((sum, chunk) => {
|
|
1800
|
+
if (!chunk.nonWords) return sum;
|
|
1801
|
+
return sum + getNonWordTotal(chunk.nonWords);
|
|
1802
|
+
}, 0) : 0;
|
|
1803
|
+
const total = analyzed.reduce((sum, chunk) => {
|
|
1804
|
+
let chunkTotal = chunk.words;
|
|
1805
|
+
if (collectNonWords && chunk.nonWords) chunkTotal += getNonWordTotal(chunk.nonWords);
|
|
1806
|
+
return sum + chunkTotal;
|
|
1807
|
+
}, 0);
|
|
1808
|
+
const counts = collectNonWords ? {
|
|
1809
|
+
words: wordsTotal,
|
|
1810
|
+
nonWords: nonWordsTotal,
|
|
1811
|
+
total
|
|
1812
|
+
} : void 0;
|
|
1813
|
+
if (mode === "segments") return {
|
|
1814
|
+
total,
|
|
1815
|
+
counts,
|
|
1816
|
+
breakdown: {
|
|
1817
|
+
mode,
|
|
1818
|
+
items: analyzed.map((chunk) => ({
|
|
1819
|
+
locale: chunk.locale,
|
|
1820
|
+
text: chunk.text,
|
|
1821
|
+
words: chunk.words,
|
|
1822
|
+
segments: chunk.segments,
|
|
1823
|
+
nonWords: chunk.nonWords
|
|
1824
|
+
}))
|
|
1776
1825
|
}
|
|
1777
1826
|
};
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1827
|
+
if (mode === "collector") return {
|
|
1828
|
+
total,
|
|
1829
|
+
counts,
|
|
1830
|
+
breakdown: {
|
|
1831
|
+
mode,
|
|
1832
|
+
items: aggregateByLocale(analyzed),
|
|
1833
|
+
nonWords: collectNonWordsAggregate(analyzed, collectNonWords)
|
|
1834
|
+
}
|
|
1785
1835
|
};
|
|
1786
|
-
const verbosity = options.verbosity ?? "compact";
|
|
1787
|
-
const sinks = [];
|
|
1788
|
-
let reportPath;
|
|
1789
|
-
if (options.report) {
|
|
1790
|
-
const now = options.now?.() ?? /* @__PURE__ */ new Date();
|
|
1791
|
-
const pid = options.pid ?? process.pid;
|
|
1792
|
-
reportPath = resolveReportPath(options.report, now, pid);
|
|
1793
|
-
sinks.push(createFileSink(reportPath));
|
|
1794
|
-
if (options.report.tee) sinks.push(createTerminalSink());
|
|
1795
|
-
} else sinks.push(createTerminalSink());
|
|
1796
1836
|
return {
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
},
|
|
1808
|
-
async close() {
|
|
1809
|
-
for (const sink of sinks) await sink.close();
|
|
1837
|
+
total,
|
|
1838
|
+
counts,
|
|
1839
|
+
breakdown: {
|
|
1840
|
+
mode,
|
|
1841
|
+
items: analyzed.map((chunk) => ({
|
|
1842
|
+
locale: chunk.locale,
|
|
1843
|
+
text: chunk.text,
|
|
1844
|
+
words: chunk.words,
|
|
1845
|
+
nonWords: chunk.nonWords
|
|
1846
|
+
}))
|
|
1810
1847
|
}
|
|
1811
1848
|
};
|
|
1812
1849
|
}
|
|
1850
|
+
function getNonWordTotal(nonWords) {
|
|
1851
|
+
return nonWords.counts.emoji + nonWords.counts.symbols + nonWords.counts.punctuation + (nonWords.counts.whitespace ?? 0);
|
|
1852
|
+
}
|
|
1853
|
+
function collectNonWordsAggregate(analyzed, enabled) {
|
|
1854
|
+
if (!enabled) return;
|
|
1855
|
+
const collection = createNonWordCollection();
|
|
1856
|
+
for (const chunk of analyzed) {
|
|
1857
|
+
if (!chunk.nonWords) continue;
|
|
1858
|
+
mergeNonWordCollections(collection, chunk.nonWords);
|
|
1859
|
+
}
|
|
1860
|
+
return collection;
|
|
1861
|
+
}
|
|
1813
1862
|
|
|
1814
1863
|
//#endregion
|
|
1815
|
-
//#region src/
|
|
1816
|
-
|
|
1817
|
-
return `${count} ${word}${count === 1 ? "" : "s"}`;
|
|
1818
|
-
}
|
|
1864
|
+
//#region src/wc/index.ts
|
|
1865
|
+
var wc_default = wordCounter;
|
|
1819
1866
|
|
|
1820
1867
|
//#endregion
|
|
1821
|
-
//#region src/
|
|
1822
|
-
|
|
1823
|
-
"
|
|
1824
|
-
"
|
|
1825
|
-
"
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
}
|
|
1840
|
-
|
|
1868
|
+
//#region src/markdown/section-count.ts
|
|
1869
|
+
function normalizeText(value) {
|
|
1870
|
+
if (value == null) return "";
|
|
1871
|
+
if (typeof value === "string") return value;
|
|
1872
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
1873
|
+
try {
|
|
1874
|
+
return JSON.stringify(value);
|
|
1875
|
+
} catch {
|
|
1876
|
+
return String(value);
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
function buildPerKeyItems(data, mode, options) {
|
|
1880
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return [];
|
|
1881
|
+
return Object.entries(data).map(([key, value]) => {
|
|
1882
|
+
const valueText = normalizeText(value);
|
|
1883
|
+
return {
|
|
1884
|
+
name: key,
|
|
1885
|
+
source: "frontmatter",
|
|
1886
|
+
result: wc_default(valueText ? `${key}: ${valueText}` : key, options)
|
|
1887
|
+
};
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
function buildSingleItem(name, text, mode, options, source) {
|
|
1891
|
+
return [{
|
|
1892
|
+
name,
|
|
1893
|
+
source,
|
|
1894
|
+
result: wc_default(text, options)
|
|
1895
|
+
}];
|
|
1896
|
+
}
|
|
1897
|
+
function sumTotals(items) {
|
|
1898
|
+
return items.reduce((sum, item) => sum + item.result.total, 0);
|
|
1899
|
+
}
|
|
1900
|
+
function countSections(input, section, options = {}) {
|
|
1901
|
+
const mode = options.mode ?? "chunk";
|
|
1902
|
+
if (section === "all") {
|
|
1903
|
+
const result = wc_default(input, options);
|
|
1904
|
+
return {
|
|
1905
|
+
section,
|
|
1906
|
+
total: result.total,
|
|
1907
|
+
frontmatterType: null,
|
|
1908
|
+
items: [{
|
|
1909
|
+
name: "all",
|
|
1910
|
+
source: "content",
|
|
1911
|
+
result
|
|
1912
|
+
}]
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
const parsed = parseMarkdown(input);
|
|
1916
|
+
const frontmatterText = parsed.frontmatter ?? "";
|
|
1917
|
+
const contentText = parsed.content ?? "";
|
|
1918
|
+
let items = [];
|
|
1919
|
+
if (section === "frontmatter") items = buildSingleItem("frontmatter", frontmatterText, mode, options, "frontmatter");
|
|
1920
|
+
else if (section === "content") items = buildSingleItem("content", contentText, mode, options, "content");
|
|
1921
|
+
else if (section === "split") items = [...buildSingleItem("frontmatter", frontmatterText, mode, options, "frontmatter"), ...buildSingleItem("content", contentText, mode, options, "content")];
|
|
1922
|
+
else if (section === "per-key") items = buildPerKeyItems(parsed.data, mode, options);
|
|
1923
|
+
else if (section === "split-per-key") items = [...buildPerKeyItems(parsed.data, mode, options), ...buildSingleItem("content", contentText, mode, options, "content")];
|
|
1841
1924
|
return {
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
whitespace: 0
|
|
1925
|
+
section,
|
|
1926
|
+
total: sumTotals(items),
|
|
1927
|
+
frontmatterType: parsed.frontmatterType,
|
|
1928
|
+
items
|
|
1847
1929
|
};
|
|
1848
1930
|
}
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1931
|
+
|
|
1932
|
+
//#endregion
|
|
1933
|
+
//#region src/cli/batch/aggregate.ts
|
|
1934
|
+
function mergeWordCounterResult(left, right, preserveCollectorSegments) {
|
|
1935
|
+
if (left.breakdown.mode !== right.breakdown.mode) throw new Error("Cannot merge different breakdown modes.");
|
|
1936
|
+
const total = left.total + right.total;
|
|
1937
|
+
const counts = left.counts || right.counts ? {
|
|
1938
|
+
words: (left.counts?.words ?? 0) + (right.counts?.words ?? 0),
|
|
1939
|
+
nonWords: (left.counts?.nonWords ?? 0) + (right.counts?.nonWords ?? 0),
|
|
1940
|
+
total: (left.counts?.total ?? 0) + (right.counts?.total ?? 0)
|
|
1941
|
+
} : void 0;
|
|
1942
|
+
if (left.breakdown.mode === "chunk" && right.breakdown.mode === "chunk") return {
|
|
1943
|
+
total,
|
|
1944
|
+
counts,
|
|
1945
|
+
breakdown: {
|
|
1946
|
+
mode: "chunk",
|
|
1947
|
+
items: [...left.breakdown.items, ...right.breakdown.items]
|
|
1948
|
+
}
|
|
1949
|
+
};
|
|
1950
|
+
if (left.breakdown.mode === "segments" && right.breakdown.mode === "segments") return {
|
|
1951
|
+
total,
|
|
1952
|
+
counts,
|
|
1953
|
+
breakdown: {
|
|
1954
|
+
mode: "segments",
|
|
1955
|
+
items: [...left.breakdown.items, ...right.breakdown.items]
|
|
1956
|
+
}
|
|
1957
|
+
};
|
|
1958
|
+
if (left.breakdown.mode === "char" && right.breakdown.mode === "char") return {
|
|
1959
|
+
total,
|
|
1960
|
+
counts,
|
|
1961
|
+
breakdown: {
|
|
1962
|
+
mode: "char",
|
|
1963
|
+
items: [...left.breakdown.items, ...right.breakdown.items]
|
|
1964
|
+
}
|
|
1965
|
+
};
|
|
1966
|
+
if (left.breakdown.mode === "char-collector" && right.breakdown.mode === "char-collector") {
|
|
1967
|
+
const localeOrder = [];
|
|
1968
|
+
const mergedByLocale = /* @__PURE__ */ new Map();
|
|
1969
|
+
const addItems = (items) => {
|
|
1970
|
+
for (const item of items) {
|
|
1971
|
+
const existing = mergedByLocale.get(item.locale);
|
|
1972
|
+
if (existing) {
|
|
1973
|
+
existing.chars += item.chars;
|
|
1974
|
+
if (item.nonWords) {
|
|
1975
|
+
if (!existing.nonWords) existing.nonWords = createNonWordCollection();
|
|
1976
|
+
mergeNonWordCollections(existing.nonWords, item.nonWords);
|
|
1977
|
+
}
|
|
1978
|
+
continue;
|
|
1979
|
+
}
|
|
1980
|
+
localeOrder.push(item.locale);
|
|
1981
|
+
mergedByLocale.set(item.locale, {
|
|
1982
|
+
locale: item.locale,
|
|
1983
|
+
chars: item.chars,
|
|
1984
|
+
nonWords: item.nonWords ? mergeNonWordCollections(createNonWordCollection(), item.nonWords) : void 0
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
};
|
|
1988
|
+
addItems(left.breakdown.items);
|
|
1989
|
+
addItems(right.breakdown.items);
|
|
1990
|
+
return {
|
|
1991
|
+
total,
|
|
1992
|
+
counts,
|
|
1993
|
+
breakdown: {
|
|
1994
|
+
mode: "char-collector",
|
|
1995
|
+
items: localeOrder.map((locale) => {
|
|
1996
|
+
const value = mergedByLocale.get(locale);
|
|
1997
|
+
if (!value) throw new Error(`Missing char-collector entry for locale: ${locale}`);
|
|
1998
|
+
return value;
|
|
1999
|
+
})
|
|
2000
|
+
}
|
|
2001
|
+
};
|
|
2002
|
+
}
|
|
2003
|
+
if (left.breakdown.mode === "collector" && right.breakdown.mode === "collector") {
|
|
2004
|
+
const localeOrder = [];
|
|
2005
|
+
const mergedByLocale = /* @__PURE__ */ new Map();
|
|
2006
|
+
const addItems = (items) => {
|
|
2007
|
+
for (const item of items) {
|
|
2008
|
+
const existing = mergedByLocale.get(item.locale);
|
|
2009
|
+
if (existing) {
|
|
2010
|
+
existing.words += item.words;
|
|
2011
|
+
if (preserveCollectorSegments) appendAll(existing.segments, item.segments);
|
|
2012
|
+
continue;
|
|
2013
|
+
}
|
|
2014
|
+
localeOrder.push(item.locale);
|
|
2015
|
+
mergedByLocale.set(item.locale, {
|
|
2016
|
+
locale: item.locale,
|
|
2017
|
+
words: item.words,
|
|
2018
|
+
segments: preserveCollectorSegments ? [...item.segments] : []
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
};
|
|
2022
|
+
addItems(left.breakdown.items);
|
|
2023
|
+
addItems(right.breakdown.items);
|
|
2024
|
+
let mergedNonWords;
|
|
2025
|
+
if (left.breakdown.nonWords || right.breakdown.nonWords) {
|
|
2026
|
+
mergedNonWords = createNonWordCollection();
|
|
2027
|
+
if (left.breakdown.nonWords) mergeNonWordCollections(mergedNonWords, left.breakdown.nonWords);
|
|
2028
|
+
if (right.breakdown.nonWords) mergeNonWordCollections(mergedNonWords, right.breakdown.nonWords);
|
|
2029
|
+
}
|
|
2030
|
+
return {
|
|
2031
|
+
total,
|
|
2032
|
+
counts,
|
|
2033
|
+
breakdown: {
|
|
2034
|
+
mode: "collector",
|
|
2035
|
+
items: localeOrder.map((locale) => {
|
|
2036
|
+
const value = mergedByLocale.get(locale);
|
|
2037
|
+
if (!value) throw new Error(`Missing collector entry for locale: ${locale}`);
|
|
2038
|
+
return value;
|
|
2039
|
+
}),
|
|
2040
|
+
nonWords: mergedNonWords
|
|
2041
|
+
}
|
|
2042
|
+
};
|
|
1862
2043
|
}
|
|
1863
|
-
|
|
1864
|
-
|
|
2044
|
+
return {
|
|
2045
|
+
total,
|
|
2046
|
+
counts,
|
|
2047
|
+
breakdown: left.breakdown
|
|
2048
|
+
};
|
|
1865
2049
|
}
|
|
1866
|
-
function
|
|
1867
|
-
if (
|
|
1868
|
-
const
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
2050
|
+
function aggregateWordCounterResults(results, preserveCollectorSegments) {
|
|
2051
|
+
if (results.length === 0) return wc_default("", { mode: "chunk" });
|
|
2052
|
+
const first = results[0];
|
|
2053
|
+
if (!first) return wc_default("", { mode: "chunk" });
|
|
2054
|
+
let aggregate = first;
|
|
2055
|
+
for (let index = 1; index < results.length; index += 1) {
|
|
2056
|
+
const current = results[index];
|
|
2057
|
+
if (!current) continue;
|
|
2058
|
+
aggregate = mergeWordCounterResult(aggregate, current, preserveCollectorSegments);
|
|
1872
2059
|
}
|
|
1873
|
-
return
|
|
2060
|
+
return aggregate;
|
|
1874
2061
|
}
|
|
1875
|
-
function
|
|
1876
|
-
|
|
1877
|
-
if (canonical) return canonical;
|
|
1878
|
-
throw new Error(`Invalid --total-of part: ${token}. Allowed: ${TOTAL_OF_PARTS.join(", ")}.`);
|
|
2062
|
+
function buildSectionKey(name, source) {
|
|
2063
|
+
return `${source}:${name}`;
|
|
1879
2064
|
}
|
|
1880
|
-
function
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
2065
|
+
function aggregateSectionedResults(results, preserveCollectorSegments) {
|
|
2066
|
+
if (results.length === 0) return {
|
|
2067
|
+
section: "all",
|
|
2068
|
+
total: 0,
|
|
2069
|
+
frontmatterType: null,
|
|
2070
|
+
items: []
|
|
2071
|
+
};
|
|
2072
|
+
const section = results[0]?.section ?? "all";
|
|
2073
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
2074
|
+
let total = 0;
|
|
2075
|
+
let frontmatterType = results[0]?.frontmatterType ?? null;
|
|
2076
|
+
for (const result of results) {
|
|
2077
|
+
total += result.total;
|
|
2078
|
+
if (result.section !== section) throw new Error("Cannot aggregate section results with different section modes.");
|
|
2079
|
+
if (frontmatterType !== result.frontmatterType) frontmatterType = null;
|
|
2080
|
+
for (const item of result.items) {
|
|
2081
|
+
const key = buildSectionKey(item.name, item.source);
|
|
2082
|
+
const existing = grouped.get(key);
|
|
2083
|
+
if (!existing) {
|
|
2084
|
+
grouped.set(key, {
|
|
2085
|
+
name: item.name,
|
|
2086
|
+
source: item.source,
|
|
2087
|
+
items: [item.result]
|
|
2088
|
+
});
|
|
2089
|
+
continue;
|
|
2090
|
+
}
|
|
2091
|
+
existing.items.push(item.result);
|
|
2092
|
+
}
|
|
1890
2093
|
}
|
|
1891
|
-
|
|
2094
|
+
const sourceOrder = new Map([["frontmatter", 0], ["content", 1]]);
|
|
2095
|
+
const items = [...grouped.values()].sort((left, right) => {
|
|
2096
|
+
const sourceDiff = (sourceOrder.get(left.source) ?? 0) - (sourceOrder.get(right.source) ?? 0);
|
|
2097
|
+
if (sourceDiff !== 0) return sourceDiff;
|
|
2098
|
+
return left.name.localeCompare(right.name);
|
|
2099
|
+
}).map((entry) => ({
|
|
2100
|
+
name: entry.name,
|
|
2101
|
+
source: entry.source,
|
|
2102
|
+
result: aggregateWordCounterResults(entry.items, preserveCollectorSegments)
|
|
2103
|
+
}));
|
|
2104
|
+
return {
|
|
2105
|
+
section,
|
|
2106
|
+
total,
|
|
2107
|
+
frontmatterType,
|
|
2108
|
+
items
|
|
2109
|
+
};
|
|
1892
2110
|
}
|
|
1893
|
-
function
|
|
1894
|
-
if (
|
|
1895
|
-
|
|
2111
|
+
function stripCollectorSegmentsFromWordCounterResult(result) {
|
|
2112
|
+
if (result.breakdown.mode !== "collector") return;
|
|
2113
|
+
for (const item of result.breakdown.items) item.segments = [];
|
|
1896
2114
|
}
|
|
1897
|
-
function
|
|
1898
|
-
|
|
1899
|
-
return parts.includes("whitespace");
|
|
2115
|
+
function stripCollectorSegmentsFromSectionedResult(result) {
|
|
2116
|
+
for (const item of result.items) stripCollectorSegmentsFromWordCounterResult(item.result);
|
|
1900
2117
|
}
|
|
1901
|
-
function
|
|
1902
|
-
|
|
1903
|
-
const
|
|
1904
|
-
|
|
1905
|
-
|
|
2118
|
+
async function buildBatchSummary(inputs, section, wcOptions, options = {}) {
|
|
2119
|
+
const preserveCollectorSegments = options.preserveCollectorSegments ?? true;
|
|
2120
|
+
const files = [];
|
|
2121
|
+
for (const input of inputs) {
|
|
2122
|
+
const result = section === "all" ? wc_default(input.content, wcOptions) : countSections(input.content, section, wcOptions);
|
|
2123
|
+
if (!preserveCollectorSegments) if ("section" in result) stripCollectorSegmentsFromSectionedResult(result);
|
|
2124
|
+
else stripCollectorSegmentsFromWordCounterResult(result);
|
|
2125
|
+
files.push({
|
|
2126
|
+
path: input.path,
|
|
2127
|
+
result
|
|
2128
|
+
});
|
|
2129
|
+
options.onFileCounted?.({
|
|
2130
|
+
completed: files.length,
|
|
2131
|
+
total: inputs.length
|
|
2132
|
+
});
|
|
2133
|
+
}
|
|
2134
|
+
options.onFinalizeStart?.();
|
|
2135
|
+
if (files.length === 0) return {
|
|
2136
|
+
files,
|
|
2137
|
+
skipped: [],
|
|
2138
|
+
aggregate: section === "all" ? wc_default("", wcOptions) : {
|
|
2139
|
+
section,
|
|
2140
|
+
total: 0,
|
|
2141
|
+
frontmatterType: null,
|
|
2142
|
+
items: []
|
|
2143
|
+
}
|
|
2144
|
+
};
|
|
1906
2145
|
return {
|
|
1907
|
-
|
|
1908
|
-
|
|
2146
|
+
files,
|
|
2147
|
+
skipped: [],
|
|
2148
|
+
aggregate: section === "all" ? aggregateWordCounterResults(files.map((file) => file.result), preserveCollectorSegments) : aggregateSectionedResults(files.map((file) => file.result), preserveCollectorSegments)
|
|
1909
2149
|
};
|
|
1910
2150
|
}
|
|
1911
|
-
function formatTotalOfParts(parts) {
|
|
1912
|
-
return parts.join(", ");
|
|
1913
|
-
}
|
|
1914
2151
|
|
|
1915
2152
|
//#endregion
|
|
1916
|
-
//#region
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
}
|
|
2153
|
+
//#region src/cli/batch/run.ts
|
|
2154
|
+
async function runBatchCount(options) {
|
|
2155
|
+
const batchStartedAtMs = Date.now();
|
|
2156
|
+
const resolveStartedAtMs = Date.now();
|
|
2157
|
+
options.debug.emit("batch.resolve.start", {
|
|
2158
|
+
inputs: options.pathInputs.length,
|
|
2159
|
+
pathMode: options.batchOptions.pathMode,
|
|
2160
|
+
recursive: options.batchOptions.recursive
|
|
2161
|
+
});
|
|
2162
|
+
const resolved = await resolveBatchFilePaths(options.pathInputs, {
|
|
2163
|
+
pathMode: options.batchOptions.pathMode,
|
|
2164
|
+
recursive: options.batchOptions.recursive,
|
|
2165
|
+
extensionFilter: options.extensionFilter,
|
|
2166
|
+
directoryRegexPattern: options.batchOptions.directoryRegexPattern,
|
|
2167
|
+
debug: options.debug
|
|
2168
|
+
});
|
|
2169
|
+
const resolveElapsedMs = Date.now() - resolveStartedAtMs;
|
|
2170
|
+
options.debug.emit("batch.resolve.complete", {
|
|
2171
|
+
files: resolved.files.length,
|
|
2172
|
+
skipped: resolved.skipped.length,
|
|
2173
|
+
elapsedMs: resolveElapsedMs
|
|
2174
|
+
});
|
|
2175
|
+
options.debug.emit("batch.stage.timing", {
|
|
2176
|
+
stage: "resolve",
|
|
2177
|
+
elapsedMs: resolveElapsedMs
|
|
2178
|
+
});
|
|
2179
|
+
const loadStartedAtMs = Date.now();
|
|
2180
|
+
options.debug.emit("batch.load.start", { files: resolved.files.length });
|
|
2181
|
+
const loaded = await loadBatchInputs(resolved.files);
|
|
2182
|
+
const loadElapsedMs = Date.now() - loadStartedAtMs;
|
|
2183
|
+
options.debug.emit("batch.load.complete", {
|
|
2184
|
+
files: loaded.files.length,
|
|
2185
|
+
skipped: loaded.skipped.length,
|
|
2186
|
+
elapsedMs: loadElapsedMs
|
|
2187
|
+
});
|
|
2188
|
+
options.debug.emit("batch.stage.timing", {
|
|
2189
|
+
stage: "load",
|
|
2190
|
+
elapsedMs: loadElapsedMs
|
|
2191
|
+
});
|
|
2192
|
+
const progressEnabled = options.progressReporter.enabled && loaded.files.length > 1;
|
|
2193
|
+
options.debug.emit("batch.progress.start", {
|
|
2194
|
+
enabled: progressEnabled,
|
|
2195
|
+
total: loaded.files.length
|
|
2196
|
+
});
|
|
2197
|
+
if (progressEnabled) options.progressReporter.start(loaded.files.length, batchStartedAtMs);
|
|
2198
|
+
let summary;
|
|
2199
|
+
const countStartedAtMs = Date.now();
|
|
2200
|
+
let finalizeStartedAtMs = null;
|
|
2201
|
+
let emittedCountTiming = false;
|
|
2202
|
+
try {
|
|
2203
|
+
summary = await buildBatchSummary(loaded.files, options.section, options.wcOptions, {
|
|
2204
|
+
onFileCounted: (snapshot) => {
|
|
2205
|
+
if (progressEnabled) options.progressReporter.advance(snapshot);
|
|
2206
|
+
},
|
|
2207
|
+
onFinalizeStart: () => {
|
|
2208
|
+
finalizeStartedAtMs = Date.now();
|
|
2209
|
+
if (progressEnabled) options.progressReporter.startFinalizing();
|
|
2210
|
+
const countElapsedMs = finalizeStartedAtMs - countStartedAtMs;
|
|
2211
|
+
options.debug.emit("batch.stage.timing", {
|
|
2212
|
+
stage: "count",
|
|
2213
|
+
elapsedMs: countElapsedMs
|
|
2214
|
+
});
|
|
2215
|
+
emittedCountTiming = true;
|
|
2216
|
+
},
|
|
2217
|
+
preserveCollectorSegments: options.preserveCollectorSegments
|
|
2218
|
+
});
|
|
2219
|
+
} finally {
|
|
2220
|
+
if (progressEnabled) options.progressReporter.finish();
|
|
2221
|
+
options.debug.emit("batch.progress.complete", {
|
|
2222
|
+
enabled: progressEnabled,
|
|
2223
|
+
total: loaded.files.length
|
|
2224
|
+
});
|
|
2225
|
+
}
|
|
2226
|
+
if (!emittedCountTiming) {
|
|
2227
|
+
const countElapsedMs = Date.now() - countStartedAtMs;
|
|
2228
|
+
options.debug.emit("batch.stage.timing", {
|
|
2229
|
+
stage: "count",
|
|
2230
|
+
elapsedMs: countElapsedMs
|
|
2231
|
+
});
|
|
2232
|
+
}
|
|
2233
|
+
const finalizeElapsedMs = finalizeStartedAtMs === null ? 0 : Date.now() - finalizeStartedAtMs;
|
|
2234
|
+
options.debug.emit("batch.stage.timing", {
|
|
2235
|
+
stage: "finalize",
|
|
2236
|
+
elapsedMs: finalizeElapsedMs
|
|
2237
|
+
});
|
|
2238
|
+
appendAll(summary.skipped, resolved.skipped);
|
|
2239
|
+
appendAll(summary.skipped, loaded.skipped);
|
|
2240
|
+
options.debug.emit("batch.aggregate.complete", {
|
|
2241
|
+
files: summary.files.length,
|
|
2242
|
+
skipped: summary.skipped.length,
|
|
2243
|
+
total: summary.aggregate.total
|
|
2244
|
+
});
|
|
2245
|
+
return summary;
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
//#endregion
|
|
2249
|
+
//#region src/utils/show-singular-or-plural-word.ts
|
|
2250
|
+
function showSingularOrPluralWord(count, word) {
|
|
2251
|
+
return `${count} ${word}${count === 1 ? "" : "s"}`;
|
|
2252
|
+
}
|
|
1983
2253
|
|
|
1984
2254
|
//#endregion
|
|
1985
2255
|
//#region src/cli/output/render.ts
|
|
1986
|
-
var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
|
|
1987
2256
|
function getCountUnit(mode) {
|
|
1988
|
-
return mode === "char" ? "character" : "word";
|
|
2257
|
+
return mode === "char" || mode === "char-collector" ? "character" : "word";
|
|
1989
2258
|
}
|
|
1990
2259
|
function getTotalLabels(mode, includeNonWords) {
|
|
1991
|
-
const unit = mode === "char" ? "characters" : "words";
|
|
2260
|
+
const unit = mode === "char" || mode === "char-collector" ? "characters" : "words";
|
|
1992
2261
|
if (includeNonWords) return {
|
|
1993
2262
|
overall: "Total count",
|
|
1994
2263
|
section: "total count"
|
|
@@ -2044,7 +2313,7 @@ function renderStandardResult(result, totalLabel, totalOfOverride) {
|
|
|
2044
2313
|
renderNonWords(result.breakdown.nonWords, false);
|
|
2045
2314
|
return;
|
|
2046
2315
|
}
|
|
2047
|
-
if (result.breakdown.mode === "char") {
|
|
2316
|
+
if (result.breakdown.mode === "char" || result.breakdown.mode === "char-collector") {
|
|
2048
2317
|
renderCountBreakdown(result.breakdown.items.map((item) => ({
|
|
2049
2318
|
locale: item.locale,
|
|
2050
2319
|
count: item.chars,
|
|
@@ -2082,7 +2351,7 @@ function renderStandardSectionedResult(result, labels, totalOfOverride) {
|
|
|
2082
2351
|
renderNonWords(item.result.breakdown.nonWords, false);
|
|
2083
2352
|
continue;
|
|
2084
2353
|
}
|
|
2085
|
-
if (item.result.breakdown.mode === "char") {
|
|
2354
|
+
if (item.result.breakdown.mode === "char" || item.result.breakdown.mode === "char-collector") {
|
|
2086
2355
|
renderCountBreakdown(item.result.breakdown.items.map((chunk) => ({
|
|
2087
2356
|
locale: chunk.locale,
|
|
2088
2357
|
count: chunk.chars,
|
|
@@ -2128,78 +2397,7 @@ function renderPerFileStandard(summary, labels, resolveTotalOfOverride) {
|
|
|
2128
2397
|
}
|
|
2129
2398
|
|
|
2130
2399
|
//#endregion
|
|
2131
|
-
//#region src/
|
|
2132
|
-
const MODE_CHOICES = [
|
|
2133
|
-
"chunk",
|
|
2134
|
-
"segments",
|
|
2135
|
-
"collector",
|
|
2136
|
-
"char"
|
|
2137
|
-
];
|
|
2138
|
-
const FORMAT_CHOICES = [
|
|
2139
|
-
"standard",
|
|
2140
|
-
"raw",
|
|
2141
|
-
"json"
|
|
2142
|
-
];
|
|
2143
|
-
const SECTION_CHOICES = [
|
|
2144
|
-
"all",
|
|
2145
|
-
"split",
|
|
2146
|
-
"frontmatter",
|
|
2147
|
-
"content",
|
|
2148
|
-
"per-key",
|
|
2149
|
-
"split-per-key"
|
|
2150
|
-
];
|
|
2151
|
-
const PATH_MODE_CHOICES = ["auto", "manual"];
|
|
2152
|
-
function getPackageVersion() {
|
|
2153
|
-
const packageCandidates = [new URL("../package.json", import.meta.url), new URL("../../package.json", import.meta.url)];
|
|
2154
|
-
let version = "0.0.0";
|
|
2155
|
-
for (const packageUrl of packageCandidates) try {
|
|
2156
|
-
const raw = readFileSync(packageUrl, "utf8");
|
|
2157
|
-
const data = JSON.parse(raw);
|
|
2158
|
-
if (data.version) {
|
|
2159
|
-
version = data.version;
|
|
2160
|
-
break;
|
|
2161
|
-
}
|
|
2162
|
-
} catch {
|
|
2163
|
-
continue;
|
|
2164
|
-
}
|
|
2165
|
-
return import_picocolors.default.bgBlack(import_picocolors.default.bold(import_picocolors.default.italic(` word-counter ${import_picocolors.default.cyanBright(`ver.${version}`)} `)));
|
|
2166
|
-
}
|
|
2167
|
-
async function readStdin() {
|
|
2168
|
-
if (process.stdin.isTTY) return "";
|
|
2169
|
-
return new Promise((resolve, reject) => {
|
|
2170
|
-
const chunks = [];
|
|
2171
|
-
process.stdin.setEncoding("utf8");
|
|
2172
|
-
process.stdin.on("data", (chunk) => chunks.push(String(chunk)));
|
|
2173
|
-
process.stdin.on("end", () => resolve(chunks.join("")));
|
|
2174
|
-
process.stdin.on("error", (error) => reject(error));
|
|
2175
|
-
process.stdin.resume();
|
|
2176
|
-
});
|
|
2177
|
-
}
|
|
2178
|
-
async function resolveInput(textTokens) {
|
|
2179
|
-
if (textTokens.length > 0) return textTokens.join(" ");
|
|
2180
|
-
return readStdin();
|
|
2181
|
-
}
|
|
2182
|
-
function collectPathValue(value, previous = []) {
|
|
2183
|
-
return [...previous, value];
|
|
2184
|
-
}
|
|
2185
|
-
function resolveBatchScope(argv) {
|
|
2186
|
-
let scope = "merged";
|
|
2187
|
-
for (const token of argv) {
|
|
2188
|
-
if (token === "--merged") {
|
|
2189
|
-
scope = "merged";
|
|
2190
|
-
continue;
|
|
2191
|
-
}
|
|
2192
|
-
if (token === "--per-file") scope = "per-file";
|
|
2193
|
-
}
|
|
2194
|
-
return scope;
|
|
2195
|
-
}
|
|
2196
|
-
function hasPathInput(pathValues) {
|
|
2197
|
-
return Array.isArray(pathValues) && pathValues.length > 0;
|
|
2198
|
-
}
|
|
2199
|
-
function resolveDebugReportPathOption(rawValue) {
|
|
2200
|
-
if (rawValue === void 0 || rawValue === false) return;
|
|
2201
|
-
if (typeof rawValue === "string") return rawValue;
|
|
2202
|
-
}
|
|
2400
|
+
//#region src/cli/output/normalize-base.ts
|
|
2203
2401
|
function normalizeWordCounterResultBase(result) {
|
|
2204
2402
|
result.total = result.counts?.words ?? result.total;
|
|
2205
2403
|
delete result.counts;
|
|
@@ -2207,7 +2405,7 @@ function normalizeWordCounterResultBase(result) {
|
|
|
2207
2405
|
delete result.breakdown.nonWords;
|
|
2208
2406
|
return result;
|
|
2209
2407
|
}
|
|
2210
|
-
if (result.breakdown.mode === "char") {
|
|
2408
|
+
if (result.breakdown.mode === "char" || result.breakdown.mode === "char-collector") {
|
|
2211
2409
|
for (const item of result.breakdown.items) {
|
|
2212
2410
|
const nonWordCount = (item.nonWords?.counts.emoji ?? 0) + (item.nonWords?.counts.symbols ?? 0) + (item.nonWords?.counts.punctuation ?? 0) + (item.nonWords?.counts.whitespace ?? 0);
|
|
2213
2411
|
item.chars = Math.max(0, item.chars - nonWordCount);
|
|
@@ -2236,6 +2434,247 @@ function normalizeBatchSummaryBase(summary) {
|
|
|
2236
2434
|
normalizeResultBase(summary.aggregate);
|
|
2237
2435
|
return summary;
|
|
2238
2436
|
}
|
|
2437
|
+
|
|
2438
|
+
//#endregion
|
|
2439
|
+
//#region src/cli/runtime/options.ts
|
|
2440
|
+
function hasPathInput(pathValues) {
|
|
2441
|
+
return Array.isArray(pathValues) && pathValues.length > 0;
|
|
2442
|
+
}
|
|
2443
|
+
function countLongOptionOccurrences(argv, optionName) {
|
|
2444
|
+
let count = 0;
|
|
2445
|
+
for (let index = 2; index < argv.length; index += 1) {
|
|
2446
|
+
const token = argv[index];
|
|
2447
|
+
if (!token) continue;
|
|
2448
|
+
if (token === "--") break;
|
|
2449
|
+
if (token === optionName) {
|
|
2450
|
+
count += 1;
|
|
2451
|
+
index += 1;
|
|
2452
|
+
continue;
|
|
2453
|
+
}
|
|
2454
|
+
if (token.startsWith(`${optionName}=`)) {
|
|
2455
|
+
count += 1;
|
|
2456
|
+
continue;
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
return count;
|
|
2460
|
+
}
|
|
2461
|
+
function validateSingleRegexOptionUsage(argv) {
|
|
2462
|
+
if (countLongOptionOccurrences(argv, "--regex") > 1) throw new Error("`--regex` can only be provided once.");
|
|
2463
|
+
}
|
|
2464
|
+
function resolveBatchScope(argv) {
|
|
2465
|
+
let scope = "merged";
|
|
2466
|
+
for (const token of argv) {
|
|
2467
|
+
if (token === "--merged") {
|
|
2468
|
+
scope = "merged";
|
|
2469
|
+
continue;
|
|
2470
|
+
}
|
|
2471
|
+
if (token === "--per-file") scope = "per-file";
|
|
2472
|
+
}
|
|
2473
|
+
return scope;
|
|
2474
|
+
}
|
|
2475
|
+
function resolveDebugReportPathOption(rawValue) {
|
|
2476
|
+
if (rawValue === void 0 || rawValue === false) return;
|
|
2477
|
+
if (typeof rawValue === "string") return rawValue;
|
|
2478
|
+
}
|
|
2479
|
+
function resolveCountRunOptions(options) {
|
|
2480
|
+
const useSection = options.section !== "all";
|
|
2481
|
+
const totalOfParts = options.totalOf;
|
|
2482
|
+
const requestedNonWords = Boolean(options.nonWords || options.includeWhitespace || options.misc);
|
|
2483
|
+
const collectNonWordsForOverride = requiresNonWordCollection(totalOfParts);
|
|
2484
|
+
const collectWhitespaceForOverride = requiresWhitespaceCollection(totalOfParts);
|
|
2485
|
+
const enableNonWords = Boolean(options.nonWords || options.includeWhitespace || options.misc || collectNonWordsForOverride);
|
|
2486
|
+
const enableWhitespace = Boolean(options.includeWhitespace || options.misc || collectWhitespaceForOverride);
|
|
2487
|
+
return {
|
|
2488
|
+
useSection,
|
|
2489
|
+
totalOfParts,
|
|
2490
|
+
requestedNonWords,
|
|
2491
|
+
shouldNormalizeBaseOutput: !requestedNonWords && enableNonWords,
|
|
2492
|
+
wcOptions: {
|
|
2493
|
+
mode: options.mode,
|
|
2494
|
+
latinLanguageHint: options.latinLanguage,
|
|
2495
|
+
latinTagHint: options.latinTag,
|
|
2496
|
+
latinLocaleHint: options.latinLocale,
|
|
2497
|
+
hanLanguageHint: options.hanLanguage,
|
|
2498
|
+
hanTagHint: options.hanTag,
|
|
2499
|
+
nonWords: enableNonWords,
|
|
2500
|
+
includeWhitespace: enableWhitespace
|
|
2501
|
+
}
|
|
2502
|
+
};
|
|
2503
|
+
}
|
|
2504
|
+
function formatInputReadError(error) {
|
|
2505
|
+
return `Failed to read input: ${error instanceof Error ? error.message : String(error)}`;
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
//#endregion
|
|
2509
|
+
//#region src/cli/runtime/batch.ts
|
|
2510
|
+
async function executeBatchCount({ argv, options, runtime, resolved, debug, teeEnabled }) {
|
|
2511
|
+
const batchOptions = {
|
|
2512
|
+
scope: resolveBatchScope(argv),
|
|
2513
|
+
pathMode: options.pathMode,
|
|
2514
|
+
recursive: options.recursive,
|
|
2515
|
+
quietSkips: Boolean(options.quietSkips),
|
|
2516
|
+
directoryRegexPattern: options.regex
|
|
2517
|
+
};
|
|
2518
|
+
const extensionFilter = buildDirectoryExtensionFilter(options.includeExt, options.excludeExt);
|
|
2519
|
+
const debugEnabled = Boolean(options.debug);
|
|
2520
|
+
const mirrorDebugToTerminal = debugEnabled && (!debug.reportPath || teeEnabled);
|
|
2521
|
+
const summary = await runBatchCount({
|
|
2522
|
+
pathInputs: options.path ?? [],
|
|
2523
|
+
batchOptions,
|
|
2524
|
+
extensionFilter,
|
|
2525
|
+
section: options.section,
|
|
2526
|
+
wcOptions: resolved.wcOptions,
|
|
2527
|
+
preserveCollectorSegments: options.format === "json",
|
|
2528
|
+
debug,
|
|
2529
|
+
progressReporter: createBatchProgressReporter({
|
|
2530
|
+
enabled: options.format === "standard" && options.progress,
|
|
2531
|
+
stream: runtime.stderr ?? process.stderr,
|
|
2532
|
+
clearOnFinish: !(mirrorDebugToTerminal || options.keepProgress)
|
|
2533
|
+
})
|
|
2534
|
+
});
|
|
2535
|
+
const showSkipDiagnostics = debugEnabled && !batchOptions.quietSkips;
|
|
2536
|
+
debug.emit("batch.skips.policy", {
|
|
2537
|
+
enabled: showSkipDiagnostics,
|
|
2538
|
+
quietSkips: batchOptions.quietSkips
|
|
2539
|
+
});
|
|
2540
|
+
if (showSkipDiagnostics) {
|
|
2541
|
+
debug.emit("batch.skips.report", { count: summary.skipped.length });
|
|
2542
|
+
if (options.verbose) for (const skip of summary.skipped) debug.emit("batch.skips.item", {
|
|
2543
|
+
path: skip.path,
|
|
2544
|
+
reason: skip.reason
|
|
2545
|
+
}, { verbosity: "verbose" });
|
|
2546
|
+
if (mirrorDebugToTerminal) reportSkipped(summary.skipped);
|
|
2547
|
+
}
|
|
2548
|
+
if (summary.files.length === 0) throw new Error("No readable text-like inputs were found from --path.");
|
|
2549
|
+
let aggregateTotalOfOverride;
|
|
2550
|
+
let totalOfOverridesByResult;
|
|
2551
|
+
if (resolved.totalOfParts && resolved.totalOfParts.length > 0) {
|
|
2552
|
+
totalOfOverridesByResult = /* @__PURE__ */ new WeakMap();
|
|
2553
|
+
const aggregateOverride = resolveTotalOfOverride(summary.aggregate, resolved.totalOfParts);
|
|
2554
|
+
if (aggregateOverride) {
|
|
2555
|
+
totalOfOverridesByResult.set(summary.aggregate, aggregateOverride);
|
|
2556
|
+
aggregateTotalOfOverride = aggregateOverride;
|
|
2557
|
+
}
|
|
2558
|
+
for (const file of summary.files) {
|
|
2559
|
+
const fileOverride = resolveTotalOfOverride(file.result, resolved.totalOfParts);
|
|
2560
|
+
if (!fileOverride) continue;
|
|
2561
|
+
totalOfOverridesByResult.set(file.result, fileOverride);
|
|
2562
|
+
}
|
|
2563
|
+
} else aggregateTotalOfOverride = resolveTotalOfOverride(summary.aggregate, resolved.totalOfParts);
|
|
2564
|
+
if (resolved.shouldNormalizeBaseOutput) normalizeBatchSummaryBase(summary);
|
|
2565
|
+
if (!aggregateTotalOfOverride && totalOfOverridesByResult) aggregateTotalOfOverride = totalOfOverridesByResult.get(summary.aggregate);
|
|
2566
|
+
if (options.format === "raw") {
|
|
2567
|
+
console.log(aggregateTotalOfOverride?.total ?? summary.aggregate.total);
|
|
2568
|
+
return;
|
|
2569
|
+
}
|
|
2570
|
+
if (options.format === "json") {
|
|
2571
|
+
const spacing = options.pretty ? 2 : 0;
|
|
2572
|
+
if (batchOptions.scope === "per-file") {
|
|
2573
|
+
const skipped = showSkipDiagnostics ? summary.skipped : void 0;
|
|
2574
|
+
const meta = resolved.totalOfParts && resolved.totalOfParts.length > 0 ? {
|
|
2575
|
+
totalOf: resolved.totalOfParts,
|
|
2576
|
+
aggregateTotalOfOverride: aggregateTotalOfOverride?.total ?? summary.aggregate.total
|
|
2577
|
+
} : void 0;
|
|
2578
|
+
const payload = {
|
|
2579
|
+
scope: "per-file",
|
|
2580
|
+
files: summary.files.map((file) => ({
|
|
2581
|
+
path: file.path,
|
|
2582
|
+
result: file.result
|
|
2583
|
+
})),
|
|
2584
|
+
...skipped ? { skipped } : {},
|
|
2585
|
+
aggregate: summary.aggregate,
|
|
2586
|
+
...meta ? { meta } : {}
|
|
2587
|
+
};
|
|
2588
|
+
console.log(JSON.stringify(payload, null, spacing));
|
|
2589
|
+
return;
|
|
2590
|
+
}
|
|
2591
|
+
if (!aggregateTotalOfOverride) {
|
|
2592
|
+
console.log(JSON.stringify(summary.aggregate, null, spacing));
|
|
2593
|
+
return;
|
|
2594
|
+
}
|
|
2595
|
+
console.log(JSON.stringify({
|
|
2596
|
+
...summary.aggregate,
|
|
2597
|
+
meta: {
|
|
2598
|
+
totalOf: aggregateTotalOfOverride.parts,
|
|
2599
|
+
totalOfOverride: aggregateTotalOfOverride.total
|
|
2600
|
+
}
|
|
2601
|
+
}, null, spacing));
|
|
2602
|
+
return;
|
|
2603
|
+
}
|
|
2604
|
+
const labels = getTotalLabels(options.mode, resolved.requestedNonWords);
|
|
2605
|
+
const totalOfResolver = resolved.totalOfParts && resolved.totalOfParts.length > 0 ? (result) => totalOfOverridesByResult?.get(result) ?? resolveTotalOfOverride(result, resolved.totalOfParts) : void 0;
|
|
2606
|
+
if (batchOptions.scope === "per-file") {
|
|
2607
|
+
renderPerFileStandard(summary, labels, totalOfResolver);
|
|
2608
|
+
return;
|
|
2609
|
+
}
|
|
2610
|
+
if (isSectionedResult(summary.aggregate)) {
|
|
2611
|
+
renderStandardSectionedResult(summary.aggregate, labels, aggregateTotalOfOverride);
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
renderStandardResult(summary.aggregate, labels.overall, aggregateTotalOfOverride);
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
//#endregion
|
|
2618
|
+
//#region src/cli/runtime/input.ts
|
|
2619
|
+
async function readStdin() {
|
|
2620
|
+
if (process.stdin.isTTY) return "";
|
|
2621
|
+
return new Promise((resolve, reject) => {
|
|
2622
|
+
const chunks = [];
|
|
2623
|
+
process.stdin.setEncoding("utf8");
|
|
2624
|
+
process.stdin.on("data", (chunk) => chunks.push(String(chunk)));
|
|
2625
|
+
process.stdin.on("end", () => resolve(chunks.join("")));
|
|
2626
|
+
process.stdin.on("error", (error) => reject(error));
|
|
2627
|
+
process.stdin.resume();
|
|
2628
|
+
});
|
|
2629
|
+
}
|
|
2630
|
+
async function resolveInput(textTokens) {
|
|
2631
|
+
if (textTokens.length > 0) return textTokens.join(" ");
|
|
2632
|
+
return readStdin();
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
//#endregion
|
|
2636
|
+
//#region src/cli/runtime/single.ts
|
|
2637
|
+
async function executeSingleCount({ textTokens, options, resolved }) {
|
|
2638
|
+
let input;
|
|
2639
|
+
try {
|
|
2640
|
+
input = await resolveInput(textTokens);
|
|
2641
|
+
} catch (error) {
|
|
2642
|
+
throw new Error(formatInputReadError(error));
|
|
2643
|
+
}
|
|
2644
|
+
const trimmed = input.trim();
|
|
2645
|
+
if (!trimmed) throw new Error("No input provided. Pass text, pipe stdin, or use --path.");
|
|
2646
|
+
const result = resolved.useSection ? countSections(trimmed, options.section, resolved.wcOptions) : wc_default(trimmed, resolved.wcOptions);
|
|
2647
|
+
const totalOfOverride = resolveTotalOfOverride(result, resolved.totalOfParts);
|
|
2648
|
+
const displayResult = resolved.shouldNormalizeBaseOutput ? normalizeResultBase(result) : result;
|
|
2649
|
+
if (options.format === "raw") {
|
|
2650
|
+
console.log(totalOfOverride?.total ?? displayResult.total);
|
|
2651
|
+
return;
|
|
2652
|
+
}
|
|
2653
|
+
if (options.format === "json") {
|
|
2654
|
+
const spacing = options.pretty ? 2 : 0;
|
|
2655
|
+
if (!totalOfOverride) {
|
|
2656
|
+
console.log(JSON.stringify(displayResult, null, spacing));
|
|
2657
|
+
return;
|
|
2658
|
+
}
|
|
2659
|
+
console.log(JSON.stringify({
|
|
2660
|
+
...displayResult,
|
|
2661
|
+
meta: {
|
|
2662
|
+
totalOf: totalOfOverride.parts,
|
|
2663
|
+
totalOfOverride: totalOfOverride.total
|
|
2664
|
+
}
|
|
2665
|
+
}, null, spacing));
|
|
2666
|
+
return;
|
|
2667
|
+
}
|
|
2668
|
+
const labels = getTotalLabels(options.mode, resolved.requestedNonWords);
|
|
2669
|
+
if (isSectionedResult(displayResult)) {
|
|
2670
|
+
renderStandardSectionedResult(displayResult, labels, totalOfOverride);
|
|
2671
|
+
return;
|
|
2672
|
+
}
|
|
2673
|
+
renderStandardResult(displayResult, labels.overall, totalOfOverride);
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
//#endregion
|
|
2677
|
+
//#region src/command.ts
|
|
2239
2678
|
async function runCli(argv = process.argv, runtime = {}) {
|
|
2240
2679
|
const program = new Command();
|
|
2241
2680
|
const parseMode = (value) => {
|
|
@@ -2243,7 +2682,8 @@ async function runCli(argv = process.argv, runtime = {}) {
|
|
|
2243
2682
|
if (!normalized) throw new Error(`Invalid mode: ${value}`);
|
|
2244
2683
|
return normalized;
|
|
2245
2684
|
};
|
|
2246
|
-
program.name("word-counter").description("Locale-aware word counting powered by Intl.Segmenter.").version(
|
|
2685
|
+
program.name("word-counter").description("Locale-aware word counting powered by Intl.Segmenter.").version(getFormattedVersionLabel(), "-v, --version", "output the version number");
|
|
2686
|
+
configureProgramOptions(program, parseMode);
|
|
2247
2687
|
program.action(async (textTokens, options) => {
|
|
2248
2688
|
const debugEnabled = Boolean(options.debug);
|
|
2249
2689
|
const debugReportPath = resolveDebugReportPathOption(options.debugReport);
|
|
@@ -2261,6 +2701,13 @@ async function runCli(argv = process.argv, runtime = {}) {
|
|
|
2261
2701
|
program.error(import_picocolors.default.red("`--debug-report-tee` (alias: `--debug-tee`) requires `--debug-report`."));
|
|
2262
2702
|
return;
|
|
2263
2703
|
}
|
|
2704
|
+
try {
|
|
2705
|
+
validateSingleRegexOptionUsage(argv);
|
|
2706
|
+
} catch (error) {
|
|
2707
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2708
|
+
program.error(import_picocolors.default.red(message));
|
|
2709
|
+
return;
|
|
2710
|
+
}
|
|
2264
2711
|
let debug;
|
|
2265
2712
|
try {
|
|
2266
2713
|
debug = createDebugChannel({
|
|
@@ -2277,173 +2724,34 @@ async function runCli(argv = process.argv, runtime = {}) {
|
|
|
2277
2724
|
return;
|
|
2278
2725
|
}
|
|
2279
2726
|
try {
|
|
2280
|
-
const
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
latinLanguageHint: options.latinLanguage,
|
|
2291
|
-
latinTagHint: options.latinTag,
|
|
2292
|
-
latinLocaleHint: options.latinLocale,
|
|
2293
|
-
hanLanguageHint: options.hanLanguage,
|
|
2294
|
-
hanTagHint: options.hanTag,
|
|
2295
|
-
nonWords: enableNonWords,
|
|
2296
|
-
includeWhitespace: enableWhitespace
|
|
2297
|
-
};
|
|
2298
|
-
if (!hasPathInput(options.path)) {
|
|
2299
|
-
let input;
|
|
2300
|
-
try {
|
|
2301
|
-
input = await resolveInput(textTokens);
|
|
2302
|
-
} catch (error) {
|
|
2303
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2304
|
-
program.error(`Failed to read input: ${message}`);
|
|
2305
|
-
return;
|
|
2306
|
-
}
|
|
2307
|
-
const trimmed = input.trim();
|
|
2308
|
-
if (!trimmed) {
|
|
2309
|
-
program.error(import_picocolors.default.red("No input provided. Pass text, pipe stdin, or use --path."));
|
|
2310
|
-
return;
|
|
2311
|
-
}
|
|
2312
|
-
const result = useSection ? countSections(trimmed, options.section, wcOptions) : wc_default(trimmed, wcOptions);
|
|
2313
|
-
const totalOfOverride = resolveTotalOfOverride(result, totalOfParts);
|
|
2314
|
-
const displayResult = shouldNormalizeBaseOutput ? normalizeResultBase(result) : result;
|
|
2315
|
-
if (options.format === "raw") {
|
|
2316
|
-
console.log(totalOfOverride?.total ?? displayResult.total);
|
|
2317
|
-
return;
|
|
2318
|
-
}
|
|
2319
|
-
if (options.format === "json") {
|
|
2320
|
-
const spacing = options.pretty ? 2 : 0;
|
|
2321
|
-
if (!totalOfOverride) {
|
|
2322
|
-
console.log(JSON.stringify(displayResult, null, spacing));
|
|
2323
|
-
return;
|
|
2324
|
-
}
|
|
2325
|
-
console.log(JSON.stringify({
|
|
2326
|
-
...displayResult,
|
|
2327
|
-
meta: {
|
|
2328
|
-
totalOf: totalOfOverride.parts,
|
|
2329
|
-
totalOfOverride: totalOfOverride.total
|
|
2330
|
-
}
|
|
2331
|
-
}, null, spacing));
|
|
2332
|
-
return;
|
|
2333
|
-
}
|
|
2334
|
-
const labels = getTotalLabels(options.mode, requestedNonWords);
|
|
2335
|
-
if (isSectionedResult(displayResult)) {
|
|
2336
|
-
renderStandardSectionedResult(displayResult, labels, totalOfOverride);
|
|
2337
|
-
return;
|
|
2338
|
-
}
|
|
2339
|
-
renderStandardResult(displayResult, labels.overall, totalOfOverride);
|
|
2727
|
+
const resolved = resolveCountRunOptions(options);
|
|
2728
|
+
if (hasPathInput(options.path)) {
|
|
2729
|
+
await executeBatchCount({
|
|
2730
|
+
argv,
|
|
2731
|
+
options,
|
|
2732
|
+
runtime,
|
|
2733
|
+
resolved,
|
|
2734
|
+
debug,
|
|
2735
|
+
teeEnabled
|
|
2736
|
+
});
|
|
2340
2737
|
return;
|
|
2341
2738
|
}
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
quietSkips: Boolean(options.quietSkips)
|
|
2347
|
-
};
|
|
2348
|
-
const extensionFilter = buildDirectoryExtensionFilter(options.includeExt, options.excludeExt);
|
|
2349
|
-
const mirrorDebugToTerminal = debugEnabled && (!debug.reportPath || teeEnabled);
|
|
2350
|
-
const summary = await runBatchCount({
|
|
2351
|
-
pathInputs: options.path,
|
|
2352
|
-
batchOptions,
|
|
2353
|
-
extensionFilter,
|
|
2354
|
-
section: options.section,
|
|
2355
|
-
wcOptions,
|
|
2356
|
-
preserveCollectorSegments: options.format === "json",
|
|
2357
|
-
debug,
|
|
2358
|
-
progressReporter: createBatchProgressReporter({
|
|
2359
|
-
enabled: options.format === "standard" && options.progress,
|
|
2360
|
-
stream: runtime.stderr ?? process.stderr,
|
|
2361
|
-
clearOnFinish: !(mirrorDebugToTerminal || options.keepProgress)
|
|
2362
|
-
})
|
|
2739
|
+
await executeSingleCount({
|
|
2740
|
+
textTokens,
|
|
2741
|
+
options,
|
|
2742
|
+
resolved
|
|
2363
2743
|
});
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
});
|
|
2369
|
-
if (showSkipDiagnostics) {
|
|
2370
|
-
debug.emit("batch.skips.report", { count: summary.skipped.length });
|
|
2371
|
-
if (options.verbose) for (const skip of summary.skipped) debug.emit("batch.skips.item", {
|
|
2372
|
-
path: skip.path,
|
|
2373
|
-
reason: skip.reason
|
|
2374
|
-
}, { verbosity: "verbose" });
|
|
2375
|
-
if (mirrorDebugToTerminal) reportSkipped(summary.skipped);
|
|
2376
|
-
}
|
|
2377
|
-
if (summary.files.length === 0) {
|
|
2378
|
-
program.error(import_picocolors.default.red("No readable text-like inputs were found from --path."));
|
|
2379
|
-
return;
|
|
2380
|
-
}
|
|
2381
|
-
let aggregateTotalOfOverride;
|
|
2382
|
-
let totalOfOverridesByResult;
|
|
2383
|
-
if (totalOfParts && totalOfParts.length > 0) {
|
|
2384
|
-
totalOfOverridesByResult = /* @__PURE__ */ new WeakMap();
|
|
2385
|
-
const aggregateOverride = resolveTotalOfOverride(summary.aggregate, totalOfParts);
|
|
2386
|
-
if (aggregateOverride) {
|
|
2387
|
-
totalOfOverridesByResult.set(summary.aggregate, aggregateOverride);
|
|
2388
|
-
aggregateTotalOfOverride = aggregateOverride;
|
|
2389
|
-
}
|
|
2390
|
-
for (const file of summary.files) {
|
|
2391
|
-
const fileOverride = resolveTotalOfOverride(file.result, totalOfParts);
|
|
2392
|
-
if (!fileOverride) continue;
|
|
2393
|
-
totalOfOverridesByResult.set(file.result, fileOverride);
|
|
2394
|
-
}
|
|
2395
|
-
} else aggregateTotalOfOverride = resolveTotalOfOverride(summary.aggregate, totalOfParts);
|
|
2396
|
-
if (shouldNormalizeBaseOutput) normalizeBatchSummaryBase(summary);
|
|
2397
|
-
if (!aggregateTotalOfOverride && totalOfOverridesByResult) aggregateTotalOfOverride = totalOfOverridesByResult.get(summary.aggregate);
|
|
2398
|
-
if (options.format === "raw") {
|
|
2399
|
-
console.log(aggregateTotalOfOverride?.total ?? summary.aggregate.total);
|
|
2400
|
-
return;
|
|
2401
|
-
}
|
|
2402
|
-
if (options.format === "json") {
|
|
2403
|
-
const spacing = options.pretty ? 2 : 0;
|
|
2404
|
-
if (batchOptions.scope === "per-file") {
|
|
2405
|
-
const skipped = showSkipDiagnostics ? summary.skipped : void 0;
|
|
2406
|
-
const meta = totalOfParts && totalOfParts.length > 0 ? {
|
|
2407
|
-
totalOf: totalOfParts,
|
|
2408
|
-
aggregateTotalOfOverride: aggregateTotalOfOverride?.total ?? summary.aggregate.total
|
|
2409
|
-
} : void 0;
|
|
2410
|
-
const payload = {
|
|
2411
|
-
scope: "per-file",
|
|
2412
|
-
files: summary.files.map((file) => ({
|
|
2413
|
-
path: file.path,
|
|
2414
|
-
result: file.result
|
|
2415
|
-
})),
|
|
2416
|
-
...skipped ? { skipped } : {},
|
|
2417
|
-
aggregate: summary.aggregate,
|
|
2418
|
-
...meta ? { meta } : {}
|
|
2419
|
-
};
|
|
2420
|
-
console.log(JSON.stringify(payload, null, spacing));
|
|
2421
|
-
return;
|
|
2422
|
-
}
|
|
2423
|
-
if (!aggregateTotalOfOverride) {
|
|
2424
|
-
console.log(JSON.stringify(summary.aggregate, null, spacing));
|
|
2425
|
-
return;
|
|
2426
|
-
}
|
|
2427
|
-
console.log(JSON.stringify({
|
|
2428
|
-
...summary.aggregate,
|
|
2429
|
-
meta: {
|
|
2430
|
-
totalOf: aggregateTotalOfOverride.parts,
|
|
2431
|
-
totalOfOverride: aggregateTotalOfOverride.total
|
|
2432
|
-
}
|
|
2433
|
-
}, null, spacing));
|
|
2434
|
-
return;
|
|
2435
|
-
}
|
|
2436
|
-
const labels = getTotalLabels(options.mode, requestedNonWords);
|
|
2437
|
-
const totalOfResolver = totalOfParts && totalOfParts.length > 0 ? (result) => totalOfOverridesByResult?.get(result) ?? resolveTotalOfOverride(result, totalOfParts) : void 0;
|
|
2438
|
-
if (batchOptions.scope === "per-file") {
|
|
2439
|
-
renderPerFileStandard(summary, labels, totalOfResolver);
|
|
2744
|
+
} catch (error) {
|
|
2745
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2746
|
+
if (message === "No input provided. Pass text, pipe stdin, or use --path.") {
|
|
2747
|
+
program.error(import_picocolors.default.red(message));
|
|
2440
2748
|
return;
|
|
2441
2749
|
}
|
|
2442
|
-
if (
|
|
2443
|
-
|
|
2750
|
+
if (message === "No readable text-like inputs were found from --path.") {
|
|
2751
|
+
program.error(import_picocolors.default.red(message));
|
|
2444
2752
|
return;
|
|
2445
2753
|
}
|
|
2446
|
-
|
|
2754
|
+
program.error(message);
|
|
2447
2755
|
} finally {
|
|
2448
2756
|
await debug.close();
|
|
2449
2757
|
}
|