Skip to content

Commit 62d5371

Browse files
JLHwungmathiasbynens
authored andcommitted
perf: derive mapping symbols.mjs from code-points.mjs
For Case_Folding, Simple_Case_Mapping, Special_Casing and Decomposition_Mapping, `symbols.mjs` shipped a second compressed copy of the same data as `code-points.mjs`, differing only in that every code point was replaced by its string. It now imports `code-points.mjs` and derives the map through the new `mapping-to-symbols.mjs`. Small mappings (fewer than 10 entries) keep their inline literals. The resulting maps are identical, including entry order; a new test checks this for every mapping directory. For Unicode 18.0.0 this shrinks the package from 444 KB to 410 KB packed (1.47 MB to 1.43 MB unpacked). Assisted-By: Claude Fable 5.1 Issue: #109
1 parent 484858a commit 62d5371

5 files changed

Lines changed: 89 additions & 11 deletions

File tree

‎index.mjs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,8 @@ const generateData = async (version) => {
351351
'decode-property-map.d.mts',
352352
'decode-ranges.mjs',
353353
'decode-ranges.d.mts',
354+
'mapping-to-symbols.mjs',
355+
'mapping-to-symbols.d.mts',
354356
];
355357
for (const file of staticFiles) {
356358
fs.copyFileSync(

‎scripts/utils.mjs‎

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -215,22 +215,24 @@ const writeFiles = (options) => {
215215
symbolsFileContent = `export default ${jsesc(codePointsAsArray.map((cp) => String.fromCodePoint(cp)))};\n`;
216216
}
217217
} else {
218-
const symbols = new Map();
219-
for (let [from, to] of codePoints) {
220-
from = String.fromCodePoint(from);
221-
if (Array.isArray(to)) {
222-
to = String.fromCodePoint.apply(null, to);
223-
} else {
224-
to = String.fromCodePoint(to);
225-
}
226-
symbols.set(from, to);
227-
}
228218
if (codePoints.size < 10) {
219+
const symbols = new Map();
220+
for (let [from, to] of codePoints) {
221+
from = String.fromCodePoint(from);
222+
if (Array.isArray(to)) {
223+
to = String.fromCodePoint.apply(null, to);
224+
} else {
225+
to = String.fromCodePoint(to);
226+
}
227+
symbols.set(from, to);
228+
}
229229
codePointsFileContent = `export default ${jsesc(codePoints)};\n`;
230230
symbolsFileContent = `export default ${jsesc(symbols)};\n`;
231231
} else {
232232
codePointsFileContent = `import { brotliDecompressSync } from 'node:zlib';\n\nexport default ${brotliInline(codePoints)};\n`;
233-
symbolsFileContent = `import { brotliDecompressSync } from 'node:zlib';\n\nexport default ${brotliInline(symbols)};\n`;
233+
// The symbols map is derived from the code points map at runtime
234+
// rather than shipped as a second copy of the same data.
235+
symbolsFileContent = `import codePoints from './code-points.mjs';\nimport mappingToSymbols from '../../mapping-to-symbols.mjs';\n\nexport default mappingToSymbols(codePoints);\n`;
234236
}
235237
if (
236238
(type === 'Case_Folding' && item === 'F') ||

‎static/mapping-to-symbols.d.mts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* Derive the symbol-to-symbol(s) map of a case mapping, case folding, or
3+
* decomposition mapping from its code-point-to-code-point(s) map.
4+
*/
5+
declare function mappingToSymbols(
6+
codePoints: Map<number, number | number[]>,
7+
): Map<string, string>;
8+
9+
export default mappingToSymbols;

‎static/mapping-to-symbols.mjs‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* This file ships to end users.
3+
*
4+
* Derive the symbol-to-symbol(s) map of a case mapping, case folding, or
5+
* decomposition mapping from its code-point-to-code-point(s) map.
6+
*/
7+
8+
const fromCodePoint = String.fromCodePoint;
9+
10+
/**
11+
* @param {Map<number, number | number[]>} codePoints
12+
* @returns {Map<string, string>}
13+
*/
14+
const mappingToSymbols = (codePoints) => {
15+
const symbols = new Map();
16+
for (const [from, to] of codePoints) {
17+
symbols.set(
18+
fromCodePoint(from),
19+
Array.isArray(to) ? fromCodePoint(...to) : fromCodePoint(to),
20+
);
21+
}
22+
return symbols;
23+
};
24+
25+
export default mappingToSymbols;

‎tests/tests.mjs‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1+
import fs from 'node:fs';
2+
import path from 'node:path';
3+
import { fileURLToPath } from 'node:url';
14
import { suite, test } from 'node:test';
25
import resources from '../data/resources.mjs';
36

7+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
8+
49
const oldest = resources[0].version;
510
const newest = resources.at(-1).version;
611

@@ -11,6 +16,38 @@ if (!process.env.CI) {
1116
await generateData(newest);
1217
}
1318

19+
// Every mapping `symbols.mjs` must equal its `code-points.mjs` with each code
20+
// point replaced by the corresponding symbol.
21+
const assertSymbolsMatchCodePoints = async (t, version) => {
22+
const root = path.resolve(__dirname, '..', 'output', `unicode-${version}`);
23+
let count = 0;
24+
for (const type of [
25+
'Case_Folding',
26+
'Simple_Case_Mapping',
27+
'Special_Casing',
28+
'Decomposition_Mapping',
29+
]) {
30+
const typeDir = path.join(root, type);
31+
if (!fs.existsSync(typeDir)) {
32+
continue;
33+
}
34+
for (const name of fs.readdirSync(typeDir)) {
35+
const dir = path.join(typeDir, name);
36+
const { default: codePoints } = await import(
37+
path.join(dir, 'code-points.mjs')
38+
);
39+
const { default: symbols } = await import(path.join(dir, 'symbols.mjs'));
40+
const expected = [...codePoints].map(([from, to]) => [
41+
String.fromCodePoint(from),
42+
String.fromCodePoint(...(Array.isArray(to) ? to : [to])),
43+
]);
44+
t.assert.deepStrictEqual([...symbols], expected, `${type}/${name}`);
45+
count++;
46+
}
47+
}
48+
t.assert.ok(count > 0, 'found mapping directories');
49+
};
50+
1451
suite(`The generated Unicode ${oldest} js`, () => {
1552
test('General_Category/Modifier_Letter should match the snapshot', async (t) => {
1653
const { default: data } = await import(
@@ -106,4 +143,7 @@ suite(`The generated latest Unicode js`, () => {
106143
);
107144
t.assert.snapshot([...map.entries()]);
108145
});
146+
test('every mapping symbols.mjs should match code-points.mjs', async (t) => {
147+
await assertSymbolsMatchCodePoints(t, newest);
148+
});
109149
});

0 commit comments

Comments
 (0)