Skip to content

Commit 7c6c20a

Browse files
committed
Apply LB30 East Asian exception for opening punctuation
Rule LB30 prohibits a break before opening punctuation only when the bracket is not East Asian wide/full/halfwidth, but all OP was treated the same, so a break before a wide bracket such as U+2329 or U+FF08 was wrongly prohibited. generate_data.js now also reads EastAsianWidth.txt and puts East Asian opening punctuation in its own class (OP_EA) so LB30's exception applies. Un-skips the 15 LineBreakTest.txt rows this fixes.
1 parent dc9603e commit 7c6c20a

5 files changed

Lines changed: 146 additions & 87 deletions

File tree

src/classes-trie-data.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/classes.js

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,18 @@ export const EB = 29; // Emoji Base
3232
export const EM = 30; // Emoji Modifier
3333
export const ZWJ = 31; // Zero Width Joiner
3434
export const CB = 32; // Contingent break
35+
// East Asian (wide/full/halfwidth) opening punctuation is split out so Rule
36+
// LB30 does not treat it like its narrow form.
37+
export const OP_EA = 33; // Opening punctuation, East Asian
3538

3639
// The following break classes are not handled by the pair table
37-
export const AI = 33; // Ambiguous (Alphabetic or Ideograph)
38-
export const BK = 34; // Break (mandatory)
39-
export const CJ = 35; // Conditional Japanese Starter
40-
export const CR = 36; // Carriage return
41-
export const LF = 37; // Line feed
42-
export const NL = 38; // Next line
43-
export const SA = 39; // South-East Asian
44-
export const SG = 40; // Surrogates
45-
export const SP = 41; // Space
46-
export const XX = 42; // Unknown
40+
export const AI = 34; // Ambiguous (Alphabetic or Ideograph)
41+
export const BK = 35; // Break (mandatory)
42+
export const CJ = 36; // Conditional Japanese Starter
43+
export const CR = 37; // Carriage return
44+
export const LF = 38; // Line feed
45+
export const NL = 39; // Next line
46+
export const SA = 40; // South-East Asian
47+
export const SG = 41; // Surrogates
48+
export const SP = 42; // Space
49+
export const XX = 43; // Unknown

src/generate_data.js

Lines changed: 93 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,48 +3,104 @@ import request from 'request';
33
import * as classes from './classes.js';
44
import UnicodeTrieBuilder from 'unicode-trie/builder.js';
55

6-
// this loads the LineBreak.txt file for Unicode and parses it to
7-
// combine ranges and generate JavaScript
8-
request('https://www.unicode.org/Public/15.0.0/ucd/LineBreak.txt', function (err, res, data) {
9-
const matches = data.match(/^[0-9A-F]+(\.\.[0-9A-F]+)?;[A-Z][A-Z0-9]([A-Z])?/gm);
10-
11-
let start = null;
12-
let end = null;
13-
let type = null;
14-
const trie = new UnicodeTrieBuilder(classes.XX);
15-
16-
// collect entries in the linebreaking table into ranges
17-
// to keep things smaller.
18-
for (let match of matches) {
19-
var rangeEnd, rangeType;
20-
match = match.split(/;|\.\./);
21-
const rangeStart = match[0];
22-
23-
if (match.length === 3) {
24-
rangeEnd = match[1];
25-
rangeType = match[2];
26-
} else {
27-
rangeEnd = rangeStart;
28-
rangeType = match[1];
29-
}
6+
const BASE = 'https://www.unicode.org/Public/15.0.0/ucd/';
307

31-
if ((type != null) && (rangeType !== type)) {
32-
trie.setRange(parseInt(start, 16), parseInt(end, 16), classes[type], true);
33-
type = null;
8+
// Build a predicate for East Asian Wide/Full/Halfwidth code points. Rule LB30
9+
// treats opening punctuation and closing parens differently when they are East
10+
// Asian, so those need to be split from the OP/CP classes.
11+
function parseEastAsianWide(data) {
12+
const ranges = [];
13+
const re = /^([0-9A-F]+)(?:\.\.([0-9A-F]+))?\s*;\s*([WFH])\b/gm;
14+
let match;
15+
while ((match = re.exec(data)) !== null) {
16+
const start = parseInt(match[1], 16);
17+
const end = match[2] ? parseInt(match[2], 16) : start;
18+
ranges.push([start, end]);
19+
}
20+
ranges.sort((a, b) => a[0] - b[0]);
21+
return function isEastAsianWide(cp) {
22+
let lo = 0;
23+
let hi = ranges.length - 1;
24+
while (lo <= hi) {
25+
const mid = (lo + hi) >> 1;
26+
if (cp < ranges[mid][0]) {
27+
hi = mid - 1;
28+
} else if (cp > ranges[mid][1]) {
29+
lo = mid + 1;
30+
} else {
31+
return true;
32+
}
3433
}
34+
return false;
35+
};
36+
}
3537

36-
if (type == null) {
37-
start = rangeStart;
38-
type = rangeType;
38+
function setTypedRange(trie, start, end, type, isEastAsianWide) {
39+
// Opening punctuation is set one code point at a time so the East Asian ones
40+
// can be assigned OP_EA for Rule LB30's East Asian exception.
41+
if (type === 'OP') {
42+
for (let cp = start; cp <= end; cp++) {
43+
trie.setRange(cp, cp, isEastAsianWide(cp) ? classes.OP_EA : classes.OP, true);
3944
}
40-
41-
end = rangeEnd;
45+
} else {
46+
trie.setRange(start, end, classes[type], true);
4247
}
48+
}
49+
50+
function fail(name, err, res, body) {
51+
if (err) throw err;
52+
if (!res || res.statusCode !== 200) throw new Error(`${name}: unexpected status ${res && res.statusCode}`);
53+
if (!body) throw new Error(`${name}: empty response`);
54+
}
55+
56+
request(`${BASE}EastAsianWidth.txt`, function (eawErr, eawRes, eawData) {
57+
fail('EastAsianWidth.txt', eawErr, eawRes, eawData);
58+
const isEastAsianWide = parseEastAsianWide(eawData);
59+
60+
// this loads the LineBreak.txt file for Unicode and parses it to
61+
// combine ranges and generate JavaScript
62+
request(`${BASE}LineBreak.txt`, function (err, res, data) {
63+
fail('LineBreak.txt', err, res, data);
64+
const matches = data.match(/^[0-9A-F]+(\.\.[0-9A-F]+)?;[A-Z][A-Z0-9]([A-Z])?/gm);
65+
66+
let start = null;
67+
let end = null;
68+
let type = null;
69+
const trie = new UnicodeTrieBuilder(classes.XX);
70+
71+
// collect entries in the linebreaking table into ranges
72+
// to keep things smaller.
73+
for (let match of matches) {
74+
var rangeEnd, rangeType;
75+
match = match.split(/;|\.\./);
76+
const rangeStart = match[0];
77+
78+
if (match.length === 3) {
79+
rangeEnd = match[1];
80+
rangeType = match[2];
81+
} else {
82+
rangeEnd = rangeStart;
83+
rangeType = match[1];
84+
}
85+
86+
if ((type != null) && (rangeType !== type)) {
87+
setTypedRange(trie, parseInt(start, 16), parseInt(end, 16), type, isEastAsianWide);
88+
type = null;
89+
}
90+
91+
if (type == null) {
92+
start = rangeStart;
93+
type = rangeType;
94+
}
95+
96+
end = rangeEnd;
97+
}
4398

44-
trie.setRange(parseInt(start, 16), parseInt(end, 16), classes[type], true);
99+
setTypedRange(trie, parseInt(start, 16), parseInt(end, 16), type, isEastAsianWide);
45100

46-
// write the trie Uint8Array to a file
47-
const trieBuffer = trie.toBuffer();
48-
const output = `export default new Uint8Array([${[...trieBuffer].join(',')}]);\n`;
49-
fs.writeFileSync(new URL('classes-trie-data.js', import.meta.url), output);
101+
// write the trie Uint8Array to a file
102+
const trieBuffer = trie.toBuffer();
103+
const output = `export default new Uint8Array([${[...trieBuffer].join(',')}]);\n`;
104+
fs.writeFileSync(new URL('classes-trie-data.js', import.meta.url), output);
105+
});
50106
});

0 commit comments

Comments
 (0)