Skip to content

Commit 4b4fc25

Browse files
committed
Preserve ZERO WIDTH JOINER in legacy joining defaults
ArabicShaping files before Unicode 3.1.1 omit joining controls. The fallback excluded ZWJ from Transparent but placed it in Non_Joining, despite its join-causing semantics. Seed Join_Causing with U+200D and allow explicit shaping records to override the defaults. Cover both joining controls and neighboring joining types across historical and derived-data releases. Only U+200D changes in the eight releases from Unicode 2.0.14 through 3.1.0. Assisted-By: GPT-6
1 parent d061bbc commit 4b4fc25

3 files changed

Lines changed: 43 additions & 1 deletion

File tree

‎scripts/parse-joining-type.mjs‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,13 @@ const parseArabicShaping = (source, categories) => {
5656
.remove(0x200d);
5757
const map = {
5858
Transparent: transparent,
59-
Non_Joining: regenerate().addRange(0, 0x10ffff).remove(transparent),
59+
// Before Unicode 3.1.1, ArabicShaping omits the joining controls.
60+
// ZWJ still causes cursive joining; ZWNJ remains Non_Joining.
61+
Join_Causing: regenerate(0x200d),
62+
Non_Joining: regenerate()
63+
.addRange(0, 0x10ffff)
64+
.remove(transparent)
65+
.remove(0x200d),
6066
};
6167
const lines = source.split('\n');
6268
for (const line of lines) {
@@ -71,6 +77,7 @@ const parseArabicShaping = (source, categories) => {
7177
const codePoint = parseInt(data[0].trim(), 16);
7278
const joiningType = findCanonicalName(data[2].trim());
7379
map.Transparent.remove(codePoint);
80+
map.Join_Causing.remove(codePoint);
7481
map.Non_Joining.remove(codePoint);
7582
map[joiningType] ??= regenerate();
7683
map[joiningType].add(codePoint);

‎tests/legacy-joining.mjs‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { test } from 'node:test';
2+
import parseJoiningType from '../scripts/parse-joining-type.mjs';
3+
4+
test('Legacy joining data preserves both joining controls', async (t) => {
5+
for (const version of [
6+
'2.0.14',
7+
'2.1.2',
8+
'2.1.5',
9+
'2.1.8',
10+
'2.1.9',
11+
'3.0.0',
12+
'3.0.1',
13+
'3.1.0',
14+
'3.1.1',
15+
'3.2.0',
16+
'4.0.1',
17+
]) {
18+
const map = await parseJoiningType(version);
19+
for (const [cp, expected] of [
20+
[0x200c, 'Non_Joining'],
21+
[0x200d, 'Join_Causing'],
22+
[0x300, 'Transparent'],
23+
[0x640, 'Join_Causing'],
24+
]) {
25+
t.assert.deepEqual(
26+
Object.entries(map)
27+
.filter(([, set]) => set.contains(cp))
28+
.map(([value]) => value),
29+
[expected],
30+
`${version}: U+${cp.toString(16).toUpperCase()}`,
31+
);
32+
}
33+
}
34+
});

‎tests/tests.mjs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { suite, test } from 'node:test';
22
import fs from 'node:fs';
33
import './parsers.mjs';
44
import './legacy-names.mjs';
5+
import './legacy-joining.mjs';
56
import './line-endings.mjs';
67
import resources from '../data/resources.mjs';
78

0 commit comments

Comments
 (0)