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/dist/index.js ADDED
@@ -0,0 +1,151 @@
1
+ // src/parse.ts
2
+ var BREAK = "\xF7";
3
+ var NO_BREAK = "\xD7";
4
+ function parseBreakTest(source) {
5
+ const vectors2 = [];
6
+ const lines = source.split(/\r\n|\r|\n/);
7
+ for (let i = 0; i < lines.length; i++) {
8
+ const hash = lines[i].indexOf("#");
9
+ const body = (hash === -1 ? lines[i] : lines[i].slice(0, hash)).trim();
10
+ if (body === "") continue;
11
+ const tokens = body.split(/\s+/);
12
+ if (tokens[0] !== BREAK) continue;
13
+ const expected = [];
14
+ let cluster = "";
15
+ let malformed = false;
16
+ for (const token of tokens) {
17
+ if (token === BREAK) {
18
+ if (cluster !== "") expected.push(cluster);
19
+ cluster = "";
20
+ } else if (token === NO_BREAK) {
21
+ continue;
22
+ } else if (/^[0-9A-Fa-f]+$/.test(token)) {
23
+ cluster += String.fromCodePoint(parseInt(token, 16));
24
+ } else {
25
+ malformed = true;
26
+ break;
27
+ }
28
+ }
29
+ if (malformed) continue;
30
+ if (cluster !== "") expected.push(cluster);
31
+ if (expected.length === 0) continue;
32
+ vectors2.push({ input: expected.join(""), expected, line: i + 1 });
33
+ }
34
+ return vectors2;
35
+ }
36
+
37
+ // src/rules.ts
38
+ var VIRAMA = /[्্੍્୍்్್്]/;
39
+ 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;
40
+ var LETTER = /\p{L}/u;
41
+ var MARK = /\p{M}/u;
42
+ var EXT_PICT = /\p{Extended_Pictographic}/u;
43
+ var ZWJ = "\u200D";
44
+ function isRegionalIndicator(cp) {
45
+ const c = cp.codePointAt(0);
46
+ return c >= 127462 && c <= 127487;
47
+ }
48
+ function isHangulJamo(cp) {
49
+ const c = cp.codePointAt(0);
50
+ return c >= 4352 && c <= 4607 || c >= 43360 && c <= 43391 || c >= 55216 && c <= 55295;
51
+ }
52
+ function hasIndicConjunct(cps) {
53
+ for (let i = 0; i < cps.length - 1; i++) {
54
+ if (!VIRAMA.test(cps[i])) continue;
55
+ let j = i + 1;
56
+ while (j < cps.length && (cps[j] === ZWJ || MARK.test(cps[j]))) j++;
57
+ if (j < cps.length && LETTER.test(cps[j]) && INDIC_SCRIPT.test(cps[j])) {
58
+ return true;
59
+ }
60
+ }
61
+ return false;
62
+ }
63
+ function hasPictographicZwj(cps) {
64
+ for (let i = 1; i < cps.length - 1; i++) {
65
+ if (cps[i] === ZWJ && EXT_PICT.test(cps[i - 1]) && EXT_PICT.test(cps[i + 1])) {
66
+ return true;
67
+ }
68
+ }
69
+ return false;
70
+ }
71
+ function inferRule(input) {
72
+ const cps = [...input];
73
+ if (hasIndicConjunct(cps)) return "GB9c";
74
+ if (hasPictographicZwj(cps)) return "GB11";
75
+ if (cps.filter(isRegionalIndicator).length >= 2) return "GB12/GB13";
76
+ if (cps.some(isHangulJamo)) return "GB6/GB7/GB8";
77
+ return null;
78
+ }
79
+
80
+ // src/score.ts
81
+ function toHex(input) {
82
+ return [...input].map((cp) => cp.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")).join(" ");
83
+ }
84
+ function sameClusters(a, b) {
85
+ if (a.length !== b.length) return false;
86
+ for (let i = 0; i < a.length; i++) {
87
+ if (a[i] !== b[i]) return false;
88
+ }
89
+ return true;
90
+ }
91
+ function score(segmenter, vectors2) {
92
+ const failures = [];
93
+ let passed = 0;
94
+ for (const vector of vectors2) {
95
+ let actual;
96
+ try {
97
+ const result = segmenter(vector.input);
98
+ actual = Array.isArray(result) && result.every((c) => typeof c === "string") ? [...result] : [];
99
+ } catch {
100
+ actual = [];
101
+ }
102
+ if (sameClusters(actual, vector.expected)) {
103
+ passed++;
104
+ } else {
105
+ failures.push({
106
+ line: vector.line,
107
+ input: vector.input,
108
+ inputHex: toHex(vector.input),
109
+ expected: [...vector.expected],
110
+ actual,
111
+ rule: inferRule(vector.input)
112
+ });
113
+ }
114
+ }
115
+ const total = vectors2.length;
116
+ return { passed, total, rate: total === 0 ? 1 : passed / total, failures };
117
+ }
118
+
119
+ // src/vectors.ts
120
+ import { existsSync, readFileSync } from "node:fs";
121
+ import { dirname, join, resolve } from "node:path";
122
+ import { fileURLToPath } from "node:url";
123
+ var VERSIONS = ["15.0.0", "15.1.0", "16.0.0", "17.0.0"];
124
+ function vectorsDir() {
125
+ let dir = dirname(fileURLToPath(import.meta.url));
126
+ for (let i = 0; i < 4; i++) {
127
+ const candidate = join(dir, "vectors");
128
+ if (existsSync(join(candidate, "GraphemeBreakTest-15.1.0.txt"))) return candidate;
129
+ const parent = resolve(dir, "..");
130
+ if (parent === dir) break;
131
+ dir = parent;
132
+ }
133
+ throw new Error(
134
+ "grapheme-conformance: vendored vectors/ directory not found next to the package"
135
+ );
136
+ }
137
+ function load() {
138
+ const dir = vectorsDir();
139
+ const out = /* @__PURE__ */ Object.create(null);
140
+ for (const version of VERSIONS) {
141
+ const source = readFileSync(join(dir, `GraphemeBreakTest-${version}.txt`), "utf8");
142
+ out[version] = parseBreakTest(source);
143
+ }
144
+ return out;
145
+ }
146
+ var vectors = load();
147
+ export {
148
+ parseBreakTest,
149
+ score,
150
+ vectors
151
+ };
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "grapheme-conformance",
3
+ "version": "0.1.0",
4
+ "description": "Score any JavaScript grapheme segmenter against Unicode's official GraphemeBreakTest.txt. Find out which string splitters are wrong, and about what.",
5
+ "keywords": [
6
+ "unicode",
7
+ "grapheme",
8
+ "uax29",
9
+ "text-segmentation",
10
+ "conformance",
11
+ "emoji",
12
+ "i18n",
13
+ "intl-segmenter",
14
+ "testing",
15
+ "javascript"
16
+ ],
17
+ "license": "MIT",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/tamerkalla/grapheme-conformance.git"
21
+ },
22
+ "homepage": "https://github.com/tamerkalla/grapheme-conformance#readme",
23
+ "bugs": {
24
+ "url": "https://github.com/tamerkalla/grapheme-conformance/issues"
25
+ },
26
+ "type": "module",
27
+ "main": "./dist/index.cjs",
28
+ "module": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "import": "./dist/index.js",
34
+ "require": "./dist/index.cjs"
35
+ },
36
+ "./package.json": "./package.json"
37
+ },
38
+ "bin": {
39
+ "grapheme-conformance": "./dist/cli.js"
40
+ },
41
+ "files": [
42
+ "dist",
43
+ "vectors",
44
+ "README.md",
45
+ "SCOREBOARD.md",
46
+ "VERIFY.md",
47
+ "LICENSE"
48
+ ],
49
+ "engines": {
50
+ "node": ">=18"
51
+ },
52
+ "scripts": {
53
+ "build": "tsup",
54
+ "typecheck": "tsc --noEmit",
55
+ "test": "vitest run",
56
+ "prescoreboard": "npm run build",
57
+ "scoreboard": "node scripts/scoreboard.mjs",
58
+ "scoreboard:check": "npm run build && node scripts/check-scoreboard.mjs",
59
+ "smoke": "npm run build && node scripts/smoke.mjs",
60
+ "prepublishOnly": "npm run build"
61
+ },
62
+ "devDependencies": {
63
+ "@types/node": "22.10.2",
64
+ "grapheme-splitter": "1.0.4",
65
+ "graphemer": "1.4.0",
66
+ "runes2": "1.1.4",
67
+ "tsup": "8.3.5",
68
+ "typescript": "5.7.2",
69
+ "unicode-segmenter": "0.17.3",
70
+ "vitest": "2.1.8"
71
+ }
72
+ }