grapheme-conformance 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +169 -0
- package/SCOREBOARD.md +27 -0
- package/VERIFY.md +58 -0
- package/dist/cli.cjs +239 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +234 -0
- package/dist/index.cjs +184 -0
- package/dist/index.d.cts +78 -0
- package/dist/index.d.ts +78 -0
- package/dist/index.js +151 -0
- package/package.json +72 -0
- package/vectors/GraphemeBreakTest-15.0.0.txt +630 -0
- package/vectors/GraphemeBreakTest-15.1.0.txt +1215 -0
- package/vectors/GraphemeBreakTest-16.0.0.txt +1121 -0
- package/vectors/GraphemeBreakTest-17.0.0.txt +796 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { isAbsolute, resolve as resolve2 } from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
|
|
8
|
+
// src/rules.ts
|
|
9
|
+
var VIRAMA = /[्্੍્୍்్್്]/;
|
|
10
|
+
var INDIC_SCRIPT = /[\p{Script=Devanagari}\p{Script=Bengali}\p{Script=Gurmukhi}\p{Script=Gujarati}\p{Script=Oriya}\p{Script=Tamil}\p{Script=Telugu}\p{Script=Kannada}\p{Script=Malayalam}]/u;
|
|
11
|
+
var LETTER = /\p{L}/u;
|
|
12
|
+
var MARK = /\p{M}/u;
|
|
13
|
+
var EXT_PICT = /\p{Extended_Pictographic}/u;
|
|
14
|
+
var ZWJ = "\u200D";
|
|
15
|
+
function isRegionalIndicator(cp) {
|
|
16
|
+
const c = cp.codePointAt(0);
|
|
17
|
+
return c >= 127462 && c <= 127487;
|
|
18
|
+
}
|
|
19
|
+
function isHangulJamo(cp) {
|
|
20
|
+
const c = cp.codePointAt(0);
|
|
21
|
+
return c >= 4352 && c <= 4607 || c >= 43360 && c <= 43391 || c >= 55216 && c <= 55295;
|
|
22
|
+
}
|
|
23
|
+
function hasIndicConjunct(cps) {
|
|
24
|
+
for (let i = 0; i < cps.length - 1; i++) {
|
|
25
|
+
if (!VIRAMA.test(cps[i])) continue;
|
|
26
|
+
let j = i + 1;
|
|
27
|
+
while (j < cps.length && (cps[j] === ZWJ || MARK.test(cps[j]))) j++;
|
|
28
|
+
if (j < cps.length && LETTER.test(cps[j]) && INDIC_SCRIPT.test(cps[j])) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
function hasPictographicZwj(cps) {
|
|
35
|
+
for (let i = 1; i < cps.length - 1; i++) {
|
|
36
|
+
if (cps[i] === ZWJ && EXT_PICT.test(cps[i - 1]) && EXT_PICT.test(cps[i + 1])) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
function inferRule(input) {
|
|
43
|
+
const cps = [...input];
|
|
44
|
+
if (hasIndicConjunct(cps)) return "GB9c";
|
|
45
|
+
if (hasPictographicZwj(cps)) return "GB11";
|
|
46
|
+
if (cps.filter(isRegionalIndicator).length >= 2) return "GB12/GB13";
|
|
47
|
+
if (cps.some(isHangulJamo)) return "GB6/GB7/GB8";
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/score.ts
|
|
52
|
+
function toHex(input) {
|
|
53
|
+
return [...input].map((cp) => cp.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")).join(" ");
|
|
54
|
+
}
|
|
55
|
+
function sameClusters(a, b) {
|
|
56
|
+
if (a.length !== b.length) return false;
|
|
57
|
+
for (let i = 0; i < a.length; i++) {
|
|
58
|
+
if (a[i] !== b[i]) return false;
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
function score(segmenter, vectors2) {
|
|
63
|
+
const failures = [];
|
|
64
|
+
let passed = 0;
|
|
65
|
+
for (const vector of vectors2) {
|
|
66
|
+
let actual;
|
|
67
|
+
try {
|
|
68
|
+
const result = segmenter(vector.input);
|
|
69
|
+
actual = Array.isArray(result) && result.every((c) => typeof c === "string") ? [...result] : [];
|
|
70
|
+
} catch {
|
|
71
|
+
actual = [];
|
|
72
|
+
}
|
|
73
|
+
if (sameClusters(actual, vector.expected)) {
|
|
74
|
+
passed++;
|
|
75
|
+
} else {
|
|
76
|
+
failures.push({
|
|
77
|
+
line: vector.line,
|
|
78
|
+
input: vector.input,
|
|
79
|
+
inputHex: toHex(vector.input),
|
|
80
|
+
expected: [...vector.expected],
|
|
81
|
+
actual,
|
|
82
|
+
rule: inferRule(vector.input)
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const total = vectors2.length;
|
|
87
|
+
return { passed, total, rate: total === 0 ? 1 : passed / total, failures };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/vectors.ts
|
|
91
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
92
|
+
import { dirname, join, resolve } from "node:path";
|
|
93
|
+
import { fileURLToPath } from "node:url";
|
|
94
|
+
|
|
95
|
+
// src/parse.ts
|
|
96
|
+
var BREAK = "\xF7";
|
|
97
|
+
var NO_BREAK = "\xD7";
|
|
98
|
+
function parseBreakTest(source) {
|
|
99
|
+
const vectors2 = [];
|
|
100
|
+
const lines = source.split(/\r\n|\r|\n/);
|
|
101
|
+
for (let i = 0; i < lines.length; i++) {
|
|
102
|
+
const hash = lines[i].indexOf("#");
|
|
103
|
+
const body = (hash === -1 ? lines[i] : lines[i].slice(0, hash)).trim();
|
|
104
|
+
if (body === "") continue;
|
|
105
|
+
const tokens = body.split(/\s+/);
|
|
106
|
+
if (tokens[0] !== BREAK) continue;
|
|
107
|
+
const expected = [];
|
|
108
|
+
let cluster = "";
|
|
109
|
+
let malformed = false;
|
|
110
|
+
for (const token of tokens) {
|
|
111
|
+
if (token === BREAK) {
|
|
112
|
+
if (cluster !== "") expected.push(cluster);
|
|
113
|
+
cluster = "";
|
|
114
|
+
} else if (token === NO_BREAK) {
|
|
115
|
+
continue;
|
|
116
|
+
} else if (/^[0-9A-Fa-f]+$/.test(token)) {
|
|
117
|
+
cluster += String.fromCodePoint(parseInt(token, 16));
|
|
118
|
+
} else {
|
|
119
|
+
malformed = true;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (malformed) continue;
|
|
124
|
+
if (cluster !== "") expected.push(cluster);
|
|
125
|
+
if (expected.length === 0) continue;
|
|
126
|
+
vectors2.push({ input: expected.join(""), expected, line: i + 1 });
|
|
127
|
+
}
|
|
128
|
+
return vectors2;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/vectors.ts
|
|
132
|
+
var VERSIONS = ["15.0.0", "15.1.0", "16.0.0", "17.0.0"];
|
|
133
|
+
function vectorsDir() {
|
|
134
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
135
|
+
for (let i = 0; i < 4; i++) {
|
|
136
|
+
const candidate = join(dir, "vectors");
|
|
137
|
+
if (existsSync(join(candidate, "GraphemeBreakTest-15.1.0.txt"))) return candidate;
|
|
138
|
+
const parent = resolve(dir, "..");
|
|
139
|
+
if (parent === dir) break;
|
|
140
|
+
dir = parent;
|
|
141
|
+
}
|
|
142
|
+
throw new Error(
|
|
143
|
+
"grapheme-conformance: vendored vectors/ directory not found next to the package"
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
function load() {
|
|
147
|
+
const dir = vectorsDir();
|
|
148
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
149
|
+
for (const version of VERSIONS) {
|
|
150
|
+
const source = readFileSync(join(dir, `GraphemeBreakTest-${version}.txt`), "utf8");
|
|
151
|
+
out[version] = parseBreakTest(source);
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
var vectors = load();
|
|
156
|
+
|
|
157
|
+
// src/cli.ts
|
|
158
|
+
var USAGE = `Usage: grapheme-conformance --module <specifier> [options]
|
|
159
|
+
--module <specifier> module to load (bare name, or path relative to cwd)
|
|
160
|
+
--export <name> export to score (default: default)
|
|
161
|
+
--version <x.y.z> vendored vectors: ${Object.keys(vectors).join(", ")}
|
|
162
|
+
--min <0..1> minimum pass rate (default: 1.0)
|
|
163
|
+
--limit <n> failing cases to print (default: 10)
|
|
164
|
+
Exits non-zero when the pass rate is below --min.`;
|
|
165
|
+
function parseArgs(argv) {
|
|
166
|
+
const out = {};
|
|
167
|
+
for (let i = 0; i < argv.length; i++) {
|
|
168
|
+
const arg = argv[i];
|
|
169
|
+
if (arg === "-h" || arg === "--help") return { help: "true" };
|
|
170
|
+
if (!arg.startsWith("--")) continue;
|
|
171
|
+
const eq = arg.indexOf("=");
|
|
172
|
+
if (eq !== -1) out[arg.slice(2, eq)] = arg.slice(eq + 1);
|
|
173
|
+
else out[arg.slice(2)] = argv[++i] ?? "";
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
function fail(message) {
|
|
178
|
+
console.error(message);
|
|
179
|
+
process.exit(2);
|
|
180
|
+
}
|
|
181
|
+
async function load2(specifier, exportName) {
|
|
182
|
+
const relative = specifier.startsWith(".") || isAbsolute(specifier);
|
|
183
|
+
const target = relative ? pathToFileURL(resolve2(process.cwd(), specifier)).href : specifier;
|
|
184
|
+
let module;
|
|
185
|
+
try {
|
|
186
|
+
module = await import(target);
|
|
187
|
+
} catch {
|
|
188
|
+
const from = createRequire(resolve2(process.cwd(), "noop.js"));
|
|
189
|
+
module = await import(pathToFileURL(from.resolve(specifier)).href);
|
|
190
|
+
}
|
|
191
|
+
const direct = module[exportName];
|
|
192
|
+
const nested = module.default?.[exportName];
|
|
193
|
+
const picked = typeof direct === "function" ? direct : nested;
|
|
194
|
+
if (typeof picked !== "function") fail(`No callable export '${exportName}' in '${specifier}'.`);
|
|
195
|
+
return (input) => {
|
|
196
|
+
const result = picked(input);
|
|
197
|
+
if (Array.isArray(result)) return result;
|
|
198
|
+
if (result && typeof result[Symbol.iterator] === "function") {
|
|
199
|
+
return [...result];
|
|
200
|
+
}
|
|
201
|
+
throw new TypeError("segmenter did not return an array of clusters");
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
async function main() {
|
|
205
|
+
const args = parseArgs(process.argv.slice(2));
|
|
206
|
+
if (args.help || !args.module) {
|
|
207
|
+
console.log(USAGE);
|
|
208
|
+
process.exit(args.module ? 0 : 2);
|
|
209
|
+
}
|
|
210
|
+
const version = args.version ?? "16.0.0";
|
|
211
|
+
const cases = vectors[version];
|
|
212
|
+
if (!cases) fail(`Unknown version '${version}'. Have: ${Object.keys(vectors).join(", ")}`);
|
|
213
|
+
const min = args.min === void 0 ? 1 : Number(args.min);
|
|
214
|
+
if (!Number.isFinite(min) || min < 0 || min > 1) fail("--min must be between 0 and 1.");
|
|
215
|
+
const limit = args.limit === void 0 ? 10 : Number(args.limit);
|
|
216
|
+
const exportName = args.export ?? "default";
|
|
217
|
+
const report = score(await load2(args.module, exportName), cases);
|
|
218
|
+
console.log(`${args.module} (${exportName}) GraphemeBreakTest ${version}`);
|
|
219
|
+
console.log(` passed ${report.passed}/${report.total} ${(report.rate * 100).toFixed(2)}%`);
|
|
220
|
+
console.log(` failed ${report.failures.length}`);
|
|
221
|
+
if (report.failures.length > 0) {
|
|
222
|
+
console.log(`
|
|
223
|
+
${"line".padEnd(6)}${"input".padEnd(34)}${"want".padEnd(6)}${"got".padEnd(6)}rule`);
|
|
224
|
+
for (const f of report.failures.slice(0, Math.max(0, limit))) {
|
|
225
|
+
const hex = f.inputHex.length > 32 ? `${f.inputHex.slice(0, 29)}...` : f.inputHex;
|
|
226
|
+
console.log(
|
|
227
|
+
` ${String(f.line).padEnd(6)}${hex.padEnd(34)}${String(f.expected.length).padEnd(6)}${String(f.actual.length).padEnd(6)}${f.rule ?? "-"}`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
if (report.failures.length > limit) console.log(` ... and ${report.failures.length - limit} more`);
|
|
231
|
+
}
|
|
232
|
+
process.exit(report.rate < min ? 1 : 0);
|
|
233
|
+
}
|
|
234
|
+
main().catch((error) => fail(error instanceof Error ? error.message : String(error)));
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
parseBreakTest: () => parseBreakTest,
|
|
24
|
+
score: () => score,
|
|
25
|
+
vectors: () => vectors
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
|
|
29
|
+
// node_modules/tsup/assets/cjs_shims.js
|
|
30
|
+
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.src || new URL("main.js", document.baseURI).href;
|
|
31
|
+
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
32
|
+
|
|
33
|
+
// src/parse.ts
|
|
34
|
+
var BREAK = "\xF7";
|
|
35
|
+
var NO_BREAK = "\xD7";
|
|
36
|
+
function parseBreakTest(source) {
|
|
37
|
+
const vectors2 = [];
|
|
38
|
+
const lines = source.split(/\r\n|\r|\n/);
|
|
39
|
+
for (let i = 0; i < lines.length; i++) {
|
|
40
|
+
const hash = lines[i].indexOf("#");
|
|
41
|
+
const body = (hash === -1 ? lines[i] : lines[i].slice(0, hash)).trim();
|
|
42
|
+
if (body === "") continue;
|
|
43
|
+
const tokens = body.split(/\s+/);
|
|
44
|
+
if (tokens[0] !== BREAK) continue;
|
|
45
|
+
const expected = [];
|
|
46
|
+
let cluster = "";
|
|
47
|
+
let malformed = false;
|
|
48
|
+
for (const token of tokens) {
|
|
49
|
+
if (token === BREAK) {
|
|
50
|
+
if (cluster !== "") expected.push(cluster);
|
|
51
|
+
cluster = "";
|
|
52
|
+
} else if (token === NO_BREAK) {
|
|
53
|
+
continue;
|
|
54
|
+
} else if (/^[0-9A-Fa-f]+$/.test(token)) {
|
|
55
|
+
cluster += String.fromCodePoint(parseInt(token, 16));
|
|
56
|
+
} else {
|
|
57
|
+
malformed = true;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (malformed) continue;
|
|
62
|
+
if (cluster !== "") expected.push(cluster);
|
|
63
|
+
if (expected.length === 0) continue;
|
|
64
|
+
vectors2.push({ input: expected.join(""), expected, line: i + 1 });
|
|
65
|
+
}
|
|
66
|
+
return vectors2;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/rules.ts
|
|
70
|
+
var VIRAMA = /[्্੍્୍்్್്]/;
|
|
71
|
+
var INDIC_SCRIPT = /[\p{Script=Devanagari}\p{Script=Bengali}\p{Script=Gurmukhi}\p{Script=Gujarati}\p{Script=Oriya}\p{Script=Tamil}\p{Script=Telugu}\p{Script=Kannada}\p{Script=Malayalam}]/u;
|
|
72
|
+
var LETTER = /\p{L}/u;
|
|
73
|
+
var MARK = /\p{M}/u;
|
|
74
|
+
var EXT_PICT = /\p{Extended_Pictographic}/u;
|
|
75
|
+
var ZWJ = "\u200D";
|
|
76
|
+
function isRegionalIndicator(cp) {
|
|
77
|
+
const c = cp.codePointAt(0);
|
|
78
|
+
return c >= 127462 && c <= 127487;
|
|
79
|
+
}
|
|
80
|
+
function isHangulJamo(cp) {
|
|
81
|
+
const c = cp.codePointAt(0);
|
|
82
|
+
return c >= 4352 && c <= 4607 || c >= 43360 && c <= 43391 || c >= 55216 && c <= 55295;
|
|
83
|
+
}
|
|
84
|
+
function hasIndicConjunct(cps) {
|
|
85
|
+
for (let i = 0; i < cps.length - 1; i++) {
|
|
86
|
+
if (!VIRAMA.test(cps[i])) continue;
|
|
87
|
+
let j = i + 1;
|
|
88
|
+
while (j < cps.length && (cps[j] === ZWJ || MARK.test(cps[j]))) j++;
|
|
89
|
+
if (j < cps.length && LETTER.test(cps[j]) && INDIC_SCRIPT.test(cps[j])) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
function hasPictographicZwj(cps) {
|
|
96
|
+
for (let i = 1; i < cps.length - 1; i++) {
|
|
97
|
+
if (cps[i] === ZWJ && EXT_PICT.test(cps[i - 1]) && EXT_PICT.test(cps[i + 1])) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
function inferRule(input) {
|
|
104
|
+
const cps = [...input];
|
|
105
|
+
if (hasIndicConjunct(cps)) return "GB9c";
|
|
106
|
+
if (hasPictographicZwj(cps)) return "GB11";
|
|
107
|
+
if (cps.filter(isRegionalIndicator).length >= 2) return "GB12/GB13";
|
|
108
|
+
if (cps.some(isHangulJamo)) return "GB6/GB7/GB8";
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/score.ts
|
|
113
|
+
function toHex(input) {
|
|
114
|
+
return [...input].map((cp) => cp.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")).join(" ");
|
|
115
|
+
}
|
|
116
|
+
function sameClusters(a, b) {
|
|
117
|
+
if (a.length !== b.length) return false;
|
|
118
|
+
for (let i = 0; i < a.length; i++) {
|
|
119
|
+
if (a[i] !== b[i]) return false;
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
function score(segmenter, vectors2) {
|
|
124
|
+
const failures = [];
|
|
125
|
+
let passed = 0;
|
|
126
|
+
for (const vector of vectors2) {
|
|
127
|
+
let actual;
|
|
128
|
+
try {
|
|
129
|
+
const result = segmenter(vector.input);
|
|
130
|
+
actual = Array.isArray(result) && result.every((c) => typeof c === "string") ? [...result] : [];
|
|
131
|
+
} catch {
|
|
132
|
+
actual = [];
|
|
133
|
+
}
|
|
134
|
+
if (sameClusters(actual, vector.expected)) {
|
|
135
|
+
passed++;
|
|
136
|
+
} else {
|
|
137
|
+
failures.push({
|
|
138
|
+
line: vector.line,
|
|
139
|
+
input: vector.input,
|
|
140
|
+
inputHex: toHex(vector.input),
|
|
141
|
+
expected: [...vector.expected],
|
|
142
|
+
actual,
|
|
143
|
+
rule: inferRule(vector.input)
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const total = vectors2.length;
|
|
148
|
+
return { passed, total, rate: total === 0 ? 1 : passed / total, failures };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/vectors.ts
|
|
152
|
+
var import_node_fs = require("fs");
|
|
153
|
+
var import_node_path = require("path");
|
|
154
|
+
var import_node_url = require("url");
|
|
155
|
+
var VERSIONS = ["15.0.0", "15.1.0", "16.0.0", "17.0.0"];
|
|
156
|
+
function vectorsDir() {
|
|
157
|
+
let dir = (0, import_node_path.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
158
|
+
for (let i = 0; i < 4; i++) {
|
|
159
|
+
const candidate = (0, import_node_path.join)(dir, "vectors");
|
|
160
|
+
if ((0, import_node_fs.existsSync)((0, import_node_path.join)(candidate, "GraphemeBreakTest-15.1.0.txt"))) return candidate;
|
|
161
|
+
const parent = (0, import_node_path.resolve)(dir, "..");
|
|
162
|
+
if (parent === dir) break;
|
|
163
|
+
dir = parent;
|
|
164
|
+
}
|
|
165
|
+
throw new Error(
|
|
166
|
+
"grapheme-conformance: vendored vectors/ directory not found next to the package"
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
function load() {
|
|
170
|
+
const dir = vectorsDir();
|
|
171
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
172
|
+
for (const version of VERSIONS) {
|
|
173
|
+
const source = (0, import_node_fs.readFileSync)((0, import_node_path.join)(dir, `GraphemeBreakTest-${version}.txt`), "utf8");
|
|
174
|
+
out[version] = parseBreakTest(source);
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
var vectors = load();
|
|
179
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
180
|
+
0 && (module.exports = {
|
|
181
|
+
parseBreakTest,
|
|
182
|
+
score,
|
|
183
|
+
vectors
|
|
184
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A function that splits a string into user-perceived characters.
|
|
3
|
+
*
|
|
4
|
+
* Both `Intl.Segmenter` wrappers and library calls fit this shape:
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* const seg = new Intl.Segmenter('en', { granularity: 'grapheme' });
|
|
8
|
+
* const segmenter: Segmenter = (s) => [...seg.segment(s)].map((x) => x.segment);
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
type Segmenter = (input: string) => string[];
|
|
12
|
+
/** One case from `GraphemeBreakTest.txt`. */
|
|
13
|
+
interface Vector {
|
|
14
|
+
/** The full input string, all clusters concatenated. */
|
|
15
|
+
input: string;
|
|
16
|
+
/** The clusters the input must split into. */
|
|
17
|
+
expected: string[];
|
|
18
|
+
/** 1-based line number in the source `.txt`. */
|
|
19
|
+
line: number;
|
|
20
|
+
}
|
|
21
|
+
/** A single case a segmenter got wrong. */
|
|
22
|
+
interface Failure {
|
|
23
|
+
/** 1-based line number in the source `.txt`. */
|
|
24
|
+
line: number;
|
|
25
|
+
input: string;
|
|
26
|
+
/** Space-separated UPPERCASE hex code points, e.g. `'0915 094D 0937'`. */
|
|
27
|
+
inputHex: string;
|
|
28
|
+
expected: string[];
|
|
29
|
+
/** What the segmenter returned. `[]` if it threw. */
|
|
30
|
+
actual: string[];
|
|
31
|
+
/** Inferred rule id, or `null` when no rule matched. */
|
|
32
|
+
rule: string | null;
|
|
33
|
+
}
|
|
34
|
+
/** The result of scoring one segmenter against one set of vectors. */
|
|
35
|
+
interface Report {
|
|
36
|
+
passed: number;
|
|
37
|
+
total: number;
|
|
38
|
+
/** `passed / total`, or `1` when there are no vectors. */
|
|
39
|
+
rate: number;
|
|
40
|
+
/** Every failing case, ordered by `line`. */
|
|
41
|
+
failures: Failure[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Parse the text of a Unicode `GraphemeBreakTest.txt` into vectors.
|
|
46
|
+
*
|
|
47
|
+
* Pure: takes a string, touches no filesystem, no network.
|
|
48
|
+
*
|
|
49
|
+
* ```
|
|
50
|
+
* ÷ 0915 × 094D × 0937 ÷ 093F ÷ # comment
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* `÷` means break, `×` means no break, hex code points sit between them and
|
|
54
|
+
* `#` starts a comment. Lines that are blank, or whose first token is not `÷`,
|
|
55
|
+
* are skipped.
|
|
56
|
+
*/
|
|
57
|
+
declare function parseBreakTest(source: string): Vector[];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Run `segmenter` over every vector and report what it got wrong.
|
|
61
|
+
*
|
|
62
|
+
* Never throws. A segmenter that throws on an input, or returns anything that
|
|
63
|
+
* is not an array of strings, yields a failure with `actual: []`.
|
|
64
|
+
*
|
|
65
|
+
* `failures` is ordered by `line`, and repeated calls on the same inputs
|
|
66
|
+
* produce deeply equal reports.
|
|
67
|
+
*/
|
|
68
|
+
declare function score(segmenter: Segmenter, vectors: Vector[]): Report;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The vendored official test vectors, keyed by Unicode version.
|
|
72
|
+
*
|
|
73
|
+
* Read from the committed `.txt` files at import time. Nothing is fetched, at
|
|
74
|
+
* build time or any other time.
|
|
75
|
+
*/
|
|
76
|
+
declare const vectors: Record<string, Vector[]>;
|
|
77
|
+
|
|
78
|
+
export { type Failure, type Report, type Segmenter, type Vector, parseBreakTest, score, vectors };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A function that splits a string into user-perceived characters.
|
|
3
|
+
*
|
|
4
|
+
* Both `Intl.Segmenter` wrappers and library calls fit this shape:
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* const seg = new Intl.Segmenter('en', { granularity: 'grapheme' });
|
|
8
|
+
* const segmenter: Segmenter = (s) => [...seg.segment(s)].map((x) => x.segment);
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
type Segmenter = (input: string) => string[];
|
|
12
|
+
/** One case from `GraphemeBreakTest.txt`. */
|
|
13
|
+
interface Vector {
|
|
14
|
+
/** The full input string, all clusters concatenated. */
|
|
15
|
+
input: string;
|
|
16
|
+
/** The clusters the input must split into. */
|
|
17
|
+
expected: string[];
|
|
18
|
+
/** 1-based line number in the source `.txt`. */
|
|
19
|
+
line: number;
|
|
20
|
+
}
|
|
21
|
+
/** A single case a segmenter got wrong. */
|
|
22
|
+
interface Failure {
|
|
23
|
+
/** 1-based line number in the source `.txt`. */
|
|
24
|
+
line: number;
|
|
25
|
+
input: string;
|
|
26
|
+
/** Space-separated UPPERCASE hex code points, e.g. `'0915 094D 0937'`. */
|
|
27
|
+
inputHex: string;
|
|
28
|
+
expected: string[];
|
|
29
|
+
/** What the segmenter returned. `[]` if it threw. */
|
|
30
|
+
actual: string[];
|
|
31
|
+
/** Inferred rule id, or `null` when no rule matched. */
|
|
32
|
+
rule: string | null;
|
|
33
|
+
}
|
|
34
|
+
/** The result of scoring one segmenter against one set of vectors. */
|
|
35
|
+
interface Report {
|
|
36
|
+
passed: number;
|
|
37
|
+
total: number;
|
|
38
|
+
/** `passed / total`, or `1` when there are no vectors. */
|
|
39
|
+
rate: number;
|
|
40
|
+
/** Every failing case, ordered by `line`. */
|
|
41
|
+
failures: Failure[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Parse the text of a Unicode `GraphemeBreakTest.txt` into vectors.
|
|
46
|
+
*
|
|
47
|
+
* Pure: takes a string, touches no filesystem, no network.
|
|
48
|
+
*
|
|
49
|
+
* ```
|
|
50
|
+
* ÷ 0915 × 094D × 0937 ÷ 093F ÷ # comment
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* `÷` means break, `×` means no break, hex code points sit between them and
|
|
54
|
+
* `#` starts a comment. Lines that are blank, or whose first token is not `÷`,
|
|
55
|
+
* are skipped.
|
|
56
|
+
*/
|
|
57
|
+
declare function parseBreakTest(source: string): Vector[];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Run `segmenter` over every vector and report what it got wrong.
|
|
61
|
+
*
|
|
62
|
+
* Never throws. A segmenter that throws on an input, or returns anything that
|
|
63
|
+
* is not an array of strings, yields a failure with `actual: []`.
|
|
64
|
+
*
|
|
65
|
+
* `failures` is ordered by `line`, and repeated calls on the same inputs
|
|
66
|
+
* produce deeply equal reports.
|
|
67
|
+
*/
|
|
68
|
+
declare function score(segmenter: Segmenter, vectors: Vector[]): Report;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The vendored official test vectors, keyed by Unicode version.
|
|
72
|
+
*
|
|
73
|
+
* Read from the committed `.txt` files at import time. Nothing is fetched, at
|
|
74
|
+
* build time or any other time.
|
|
75
|
+
*/
|
|
76
|
+
declare const vectors: Record<string, Vector[]>;
|
|
77
|
+
|
|
78
|
+
export { type Failure, type Report, type Segmenter, type Vector, parseBreakTest, score, vectors };
|