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 CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  **Your string splitter is probably wrong about Hindi.**
4
4
 
5
+ [![build](https://github.com/tamerkalla/grapheme-conformance/actions/workflows/release.yml/badge.svg)](https://github.com/tamerkalla/grapheme-conformance/actions/workflows/release.yml)
6
+ [![npm](https://img.shields.io/npm/v/grapheme-conformance.svg)](https://www.npmjs.com/package/grapheme-conformance)
7
+ [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
8
+ [![provenance](https://img.shields.io/badge/provenance-attested-brightgreen.svg)](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`'s single failure is the input `2701 200D 2701`, a known ICU
44
- deviation and one that arrived with a later ICU rather than an earlier one.
45
- Node 18.20.8 ships an ICU without it and passes every case (`1187` and `1093`);
46
- Node 20 and 22 split that input and score `1186` and `1092`.
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 --version 16.0.0
77
+ --export splitGraphemes
61
78
  ```
62
79
 
63
80
  ```
64
- unicode-segmenter/grapheme (splitGraphemes) GraphemeBreakTest 16.0.0
65
- passed 1092/1093 99.91%
66
- failed 1
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` | `16.0.0` | vendored vectors: `15.0.0`, `15.1.0`, `16.0.0`, `17.0.0` |
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['16.0.0'],
106
+ vectors['17.0.0'],
93
107
  );
94
108
 
95
- report.passed; // 1092
96
- report.total; // 1093
97
- report.rate; // 0.999085...
98
- report.failures[0].inputHex; // '2701 200D 2701'
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, and not monotonically: Node 18.20.8 scores higher than
148
- Node 22 because the `2701 200D 2701` deviation is a newer ICU behaviour. The
149
- pure-JS libraries are pinned to exact versions and score identically everywhere.
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
- Run one command:
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
- npm run scoreboard
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
- It builds the package, scores every library against every vendored copy of
10
- Unicode's `GraphemeBreakTest.txt`, writes `SCOREBOARD.md`, and prints the table
11
- below to stdout.
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
- ## Expected output
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
- Compare ten integers. These two columns are the specified baseline.
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
- | implementation | U15.1.0 (1187) | U16.0.0 (1093) |
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
- The printed table shows each cell as `passed/total (rate%)`; the integer to
26
- check is the one before the slash. If all ten match, the build is correct.
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 `npm run scoreboard` on Node 22 or 20 to get the two baseline numbers. On
41
- Node 18 the `Intl.Segmenter` row reads `1187` and `1093` instead: that ICU
42
- predates the deviation and passes every case. The other four rows are pinned to
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
- ## Everything else
68
+ ## Reproducing SCOREBOARD.md and the rest of the gates
48
69
 
49
- ```
50
- npm test # 104 assertions, offline, including all ten integers above
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
- npm run smoke # loads the ESM and CJS entry points and scores Intl.Segmenter
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 under
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 ?? "16.0.0";
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 ?? "16.0.0";
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.2",
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
  }