grapheme-conformance 0.1.2 → 0.1.4
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 +37 -20
- package/VERIFY.md +57 -29
- package/dist/cli.cjs +1 -1
- package/dist/cli.js +1 -1
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
**Your string splitter is probably wrong about Hindi.**
|
|
4
4
|
|
|
5
|
+
[](https://github.com/tamerkalla/grapheme-conformance/actions/workflows/release.yml)
|
|
6
|
+
[](https://www.npmjs.com/package/grapheme-conformance)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](https://www.npmjs.com/package/grapheme-conformance)
|
|
9
|
+
|
|
5
10
|
Unicode publishes `GraphemeBreakTest.txt`: a machine-readable answer key that
|
|
6
11
|
says exactly where a string may be split into user-perceived characters. This
|
|
7
12
|
package scores any JavaScript grapheme segmenter against it, tells you which
|
|
@@ -40,10 +45,22 @@ Cases passed:
|
|
|
40
45
|
| `grapheme-splitter@1.0.4` | 1175 | 1081 |
|
|
41
46
|
| `runes2@1.1.4` | 730 | 695 |
|
|
42
47
|
|
|
43
|
-
`Intl.Segmenter`
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
The one case `Intl.Segmenter` misses in both columns is `2701 200D 2701`, and
|
|
49
|
+
it is an artefact of the answer key, not a bug in ICU. Unicode 17.0 removed
|
|
50
|
+
U+2701 from `Extended_Pictographic`, so GB11 no longer applies and the correct
|
|
51
|
+
result became **two** clusters. Scored against the `17.0.0` key, ICU on Node 22
|
|
52
|
+
passes every case:
|
|
53
|
+
|
|
54
|
+
| implementation | U17.0.0 (766) |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `Intl.Segmenter` (ICU, Node 22) | 766 |
|
|
57
|
+
| `unicode-segmenter@0.17.3` | 766 |
|
|
58
|
+
| `graphemer@1.4.0` | 749 |
|
|
59
|
+
| `grapheme-splitter@1.0.4` | 746 |
|
|
60
|
+
| `runes2@1.1.4` | 501 |
|
|
61
|
+
|
|
62
|
+
Score an implementation against the key it targets. A current segmenter judged
|
|
63
|
+
by a superseded key reports failures it does not have.
|
|
47
64
|
|
|
48
65
|
## Install
|
|
49
66
|
|
|
@@ -57,23 +74,20 @@ Zero runtime dependencies. Dual ESM/CJS. Node 18+.
|
|
|
57
74
|
|
|
58
75
|
```sh
|
|
59
76
|
npx grapheme-conformance --module unicode-segmenter/grapheme \
|
|
60
|
-
--export splitGraphemes
|
|
77
|
+
--export splitGraphemes
|
|
61
78
|
```
|
|
62
79
|
|
|
63
80
|
```
|
|
64
|
-
unicode-segmenter/grapheme (splitGraphemes) GraphemeBreakTest
|
|
65
|
-
passed
|
|
66
|
-
failed
|
|
67
|
-
|
|
68
|
-
line input want got rule
|
|
69
|
-
1105 2701 200D 2701 1 2 -
|
|
81
|
+
unicode-segmenter/grapheme (splitGraphemes) GraphemeBreakTest 17.0.0
|
|
82
|
+
passed 766/766 100.00%
|
|
83
|
+
failed 0
|
|
70
84
|
```
|
|
71
85
|
|
|
72
86
|
| flag | default | meaning |
|
|
73
87
|
|---|---|---|
|
|
74
88
|
| `--module` | required | module to load: a bare name, or a path relative to cwd |
|
|
75
89
|
| `--export` | `default` | the export to score |
|
|
76
|
-
| `--version` | `
|
|
90
|
+
| `--version` | `17.0.0` | vendored vectors: `15.0.0`, `15.1.0`, `16.0.0`, `17.0.0` |
|
|
77
91
|
| `--min` | `1.0` | minimum pass rate before exiting non-zero |
|
|
78
92
|
| `--limit` | `10` | failing cases to print |
|
|
79
93
|
|
|
@@ -89,13 +103,13 @@ import { parseBreakTest, score, vectors } from 'grapheme-conformance';
|
|
|
89
103
|
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
|
|
90
104
|
const report = score(
|
|
91
105
|
(s) => [...segmenter.segment(s)].map((part) => part.segment),
|
|
92
|
-
vectors['
|
|
106
|
+
vectors['17.0.0'],
|
|
93
107
|
);
|
|
94
108
|
|
|
95
|
-
report.passed; //
|
|
96
|
-
report.total; //
|
|
97
|
-
report.rate; //
|
|
98
|
-
report.failures
|
|
109
|
+
report.passed; // 766
|
|
110
|
+
report.total; // 766
|
|
111
|
+
report.rate; // 1
|
|
112
|
+
report.failures; // []
|
|
99
113
|
```
|
|
100
114
|
|
|
101
115
|
```ts
|
|
@@ -144,9 +158,12 @@ UAX #29 and does not prove which rule an implementation actually got wrong.
|
|
|
144
158
|
## Intl.Segmenter and the host ICU
|
|
145
159
|
|
|
146
160
|
`Intl.Segmenter` is scored against whatever ICU the host Node ships, so its row
|
|
147
|
-
moves with the runtime
|
|
148
|
-
Node
|
|
149
|
-
|
|
161
|
+
moves with the runtime. Against the older keys the movement is not even
|
|
162
|
+
monotonic: Node 18.20.8 scores `1187` and `1093` where Node 20 and 22 score
|
|
163
|
+
`1186` and `1092`, because newer ICU implements the Unicode 17.0 property
|
|
164
|
+
change that those keys predate. Against the `17.0.0` key every one of them is
|
|
165
|
+
correct. The pure-JS libraries are pinned to exact versions and score
|
|
166
|
+
identically everywhere.
|
|
150
167
|
|
|
151
168
|
## Verifying this build
|
|
152
169
|
|
package/VERIFY.md
CHANGED
|
@@ -1,29 +1,50 @@
|
|
|
1
1
|
# VERIFY
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This reproduces the scoreboard's two Unicode-16.0.0-and-15.1.0 columns from the
|
|
4
|
+
published package, in a clean directory. It does not require this repository
|
|
5
|
+
to be checked out.
|
|
4
6
|
|
|
5
|
-
```
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
```bash
|
|
8
|
+
mkdir -p grapheme-conformance-verify && cd grapheme-conformance-verify
|
|
9
|
+
npm init -y >/dev/null 2>&1
|
|
10
|
+
npm install grapheme-conformance@latest grapheme-splitter@1.0.4 graphemer@1.4.0 runes2@1.1.4 unicode-segmenter@0.17.3 >/dev/null 2>&1
|
|
11
|
+
cat > verify.mjs <<'JS'
|
|
12
|
+
import { score, vectors } from 'grapheme-conformance';
|
|
13
|
+
import { splitGraphemes } from 'unicode-segmenter/grapheme';
|
|
14
|
+
import graphemerModule from 'graphemer';
|
|
15
|
+
import GraphemeSplitter from 'grapheme-splitter';
|
|
16
|
+
import { runes } from 'runes2';
|
|
8
17
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
18
|
+
const Graphemer = graphemerModule.default ?? graphemerModule;
|
|
19
|
+
const intl = new Intl.Segmenter('en', { granularity: 'grapheme' });
|
|
20
|
+
const graphemer = new Graphemer();
|
|
21
|
+
const splitter = new GraphemeSplitter();
|
|
12
22
|
|
|
13
|
-
|
|
23
|
+
const impls = {
|
|
24
|
+
'Intl.Segmenter': (s) => [...intl.segment(s)].map((p) => p.segment),
|
|
25
|
+
'unicode-segmenter': (s) => [...splitGraphemes(s)],
|
|
26
|
+
graphemer: (s) => graphemer.splitGraphemes(s),
|
|
27
|
+
'grapheme-splitter': (s) => splitter.splitGraphemes(s),
|
|
28
|
+
runes2: (s) => runes(s),
|
|
29
|
+
};
|
|
14
30
|
|
|
15
|
-
|
|
31
|
+
for (const version of ['15.1.0', '16.0.0']) {
|
|
32
|
+
const vecs = vectors[version];
|
|
33
|
+
const row = Object.entries(impls)
|
|
34
|
+
.map(([name, fn]) => `${name}=${score(fn, vecs).passed}`)
|
|
35
|
+
.join(' ');
|
|
36
|
+
console.log(`${version}: ${row}`);
|
|
37
|
+
}
|
|
38
|
+
JS
|
|
39
|
+
node verify.mjs
|
|
40
|
+
```
|
|
16
41
|
|
|
17
|
-
|
|
18
|
-
|---|---|---|
|
|
19
|
-
| `Intl.Segmenter` | 1186 | 1092 |
|
|
20
|
-
| `unicode-segmenter` | 1186 | 1092 |
|
|
21
|
-
| `graphemer` | 1180 | 1086 |
|
|
22
|
-
| `grapheme-splitter` | 1175 | 1081 |
|
|
23
|
-
| `runes2` | 730 | 695 |
|
|
42
|
+
Expected output:
|
|
24
43
|
|
|
25
|
-
|
|
26
|
-
|
|
44
|
+
```text
|
|
45
|
+
15.1.0: Intl.Segmenter=1186 unicode-segmenter=1186 graphemer=1180 grapheme-splitter=1175 runes2=730
|
|
46
|
+
16.0.0: Intl.Segmenter=1092 unicode-segmenter=1092 graphemer=1086 grapheme-splitter=1081 runes2=695
|
|
47
|
+
```
|
|
27
48
|
|
|
28
49
|
## One caveat, and it is the only one
|
|
29
50
|
|
|
@@ -37,22 +58,29 @@ matrix:
|
|
|
37
58
|
| Node 20.x | 1186 | 1092 | split |
|
|
38
59
|
| Node 18.20.8 | 1187 | 1093 | not split |
|
|
39
60
|
|
|
40
|
-
Run
|
|
41
|
-
Node 18 the `Intl.Segmenter`
|
|
42
|
-
predates the deviation and passes every case. The other four
|
|
43
|
-
exact versions and hold identically on every Node.
|
|
61
|
+
Run the command above on Node 22 or 20 to get the expected output above. On
|
|
62
|
+
Node 18 the `Intl.Segmenter` value reads `1187` and `1093` instead: that ICU
|
|
63
|
+
predates the deviation and passes every case. The other four values are
|
|
64
|
+
pinned to exact library versions and hold identically on every Node.
|
|
44
65
|
|
|
45
66
|
The baseline was verified on Node 22.22.2 (ICU 78.2, Unicode 17.0).
|
|
46
67
|
|
|
47
|
-
##
|
|
68
|
+
## Reproducing SCOREBOARD.md and the rest of the gates
|
|
48
69
|
|
|
49
|
-
|
|
50
|
-
|
|
70
|
+
The committed `SCOREBOARD.md` (all four vendored Unicode versions, not just
|
|
71
|
+
the two above) is generated from this repository's own scripts, which are not
|
|
72
|
+
part of the published package, so reproducing it requires a checkout:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
git clone https://github.com/tamerkalla/grapheme-conformance.git && cd grapheme-conformance
|
|
76
|
+
npm ci
|
|
77
|
+
npm run scoreboard # regenerates SCOREBOARD.md and prints it
|
|
78
|
+
npm run scoreboard:check # asserts it is byte-identical to a fresh run
|
|
51
79
|
npm run typecheck
|
|
80
|
+
npm test # includes all ten integers above
|
|
52
81
|
npm run build
|
|
53
|
-
|
|
54
|
-
npm run scoreboard:check # asserts SCOREBOARD.md is byte-identical to a fresh run
|
|
82
|
+
node scripts/smoke.mjs # loads the ESM and CJS entry points
|
|
55
83
|
```
|
|
56
84
|
|
|
57
|
-
No network access is required by any of these. The vectors are committed
|
|
58
|
-
`vectors/` and are never fetched.
|
|
85
|
+
No network access is required by any of these. The vectors are committed
|
|
86
|
+
under `vectors/` and are never fetched.
|
package/dist/cli.cjs
CHANGED
|
@@ -212,7 +212,7 @@ async function main() {
|
|
|
212
212
|
console.log(USAGE);
|
|
213
213
|
process.exit(args.module ? 0 : 2);
|
|
214
214
|
}
|
|
215
|
-
const version = args.version ?? "
|
|
215
|
+
const version = args.version ?? "17.0.0";
|
|
216
216
|
const cases = vectors[version];
|
|
217
217
|
if (!cases) fail(`Unknown version '${version}'. Have: ${Object.keys(vectors).join(", ")}`);
|
|
218
218
|
const min = args.min === void 0 ? 1 : Number(args.min);
|
package/dist/cli.js
CHANGED
|
@@ -207,7 +207,7 @@ async function main() {
|
|
|
207
207
|
console.log(USAGE);
|
|
208
208
|
process.exit(args.module ? 0 : 2);
|
|
209
209
|
}
|
|
210
|
-
const version = args.version ?? "
|
|
210
|
+
const version = args.version ?? "17.0.0";
|
|
211
211
|
const cases = vectors[version];
|
|
212
212
|
if (!cases) fail(`Unknown version '${version}'. Have: ${Object.keys(vectors).join(", ")}`);
|
|
213
213
|
const min = args.min === void 0 ? 1 : Number(args.min);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "grapheme-conformance",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Score any JavaScript grapheme segmenter against Unicode's official GraphemeBreakTest.txt. Find out which string splitters are wrong, and about what.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"unicode",
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"scripts": {
|
|
53
53
|
"build": "tsup",
|
|
54
54
|
"typecheck": "tsc --noEmit",
|
|
55
|
+
"pretest": "npm run build",
|
|
55
56
|
"test": "vitest run",
|
|
56
57
|
"prescoreboard": "npm run build",
|
|
57
58
|
"scoreboard": "node scripts/scoreboard.mjs",
|
|
@@ -67,6 +68,7 @@
|
|
|
67
68
|
"tsup": "8.3.5",
|
|
68
69
|
"typescript": "5.7.2",
|
|
69
70
|
"unicode-segmenter": "0.17.3",
|
|
70
|
-
"vitest": "2.1.8"
|
|
71
|
+
"vitest": "2.1.8",
|
|
72
|
+
"yaml": "2.9.0"
|
|
71
73
|
}
|
|
72
74
|
}
|