maplibre-gl 6.8.0 → 6.9.1
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/build/generate-unicode-data.ts +321 -110
- package/dist/maplibre-gl-dev.mjs +274 -149
- package/dist/maplibre-gl-dev.mjs.map +1 -1
- package/dist/maplibre-gl-shared-dev.mjs +1960 -376
- package/dist/maplibre-gl-shared-dev.mjs.map +1 -1
- package/dist/maplibre-gl-shared.mjs +2 -2
- package/dist/maplibre-gl-shared.mjs.map +1 -1
- package/dist/maplibre-gl-worker-dev.mjs +9 -2
- package/dist/maplibre-gl-worker-dev.mjs.map +1 -1
- package/dist/maplibre-gl-worker.mjs +2 -2
- package/dist/maplibre-gl-worker.mjs.map +1 -1
- package/dist/maplibre-gl.d.ts +84 -26
- package/dist/maplibre-gl.mjs +39 -39
- package/dist/maplibre-gl.mjs.map +1 -1
- package/package.json +16 -15
- package/src/data/bucket/fill_extrusion_bucket.ts +5 -8
- package/src/data/bucket/round_polygon_corners.test.ts +16 -15
- package/src/data/bucket/round_polygon_corners.ts +18 -13
- package/src/data/bucket/symbol_bucket.test.ts +2 -9
- package/src/data/bucket/symbol_bucket.ts +4 -15
- package/src/geo/projection/globe_transform.test.ts +28 -0
- package/src/geo/projection/mercator_transform.test.ts +80 -0
- package/src/geo/projection/mercator_transform.ts +32 -4
- package/src/geo/projection/vertical_perspective_transform.ts +0 -4
- package/src/geo/transform_helper.test.ts +11 -0
- package/src/geo/transform_helper.ts +18 -16
- package/src/index.ts +5 -1
- package/src/render/glyph_manager.test.ts +22 -2
- package/src/render/glyph_manager.ts +20 -14
- package/src/render/painter.test.ts +20 -0
- package/src/render/painter.ts +13 -2
- package/src/render/render_to_texture_interface.ts +2 -1
- package/src/render/terrain.test.ts +23 -7
- package/src/render/terrain.ts +21 -10
- package/src/shaders/glsl/_prelude.vertex.glsl +5 -3
- package/src/shaders/glsl/_prelude.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/fill_extrusion.vertex.glsl +6 -1
- package/src/shaders/glsl/fill_extrusion.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/fill_extrusion_pattern.vertex.glsl +6 -1
- package/src/shaders/glsl/fill_extrusion_pattern.vertex.glsl.g.ts +1 -1
- package/src/source/geojson_source_diff.test.ts +59 -0
- package/src/source/geojson_source_diff.ts +3 -5
- package/src/source/rtl_text_plugin_status.ts +12 -0
- package/src/source/rtl_text_plugin_worker.ts +6 -0
- package/src/source/worker.ts +7 -1
- package/src/style/evaluation_parameters.ts +0 -12
- package/src/style/light.test.ts +10 -0
- package/src/style/light.ts +2 -9
- package/src/style/properties.test.ts +16 -0
- package/src/style/properties.ts +15 -1
- package/src/style/sky.ts +2 -9
- package/src/style/style.test.ts +46 -1
- package/src/style/style.ts +0 -3
- package/src/style/style_image.ts +1 -1
- package/src/symbol/arabic_shaping.test.ts +61 -0
- package/src/symbol/arabic_shaping.ts +207 -0
- package/src/symbol/bidi.test.ts +88 -0
- package/src/symbol/bidi.ts +210 -0
- package/src/symbol/merge_lines.test.ts +24 -0
- package/src/symbol/merge_lines.ts +83 -44
- package/src/symbol/shaping.ts +81 -52
- package/src/symbol/transform_text.test.ts +46 -0
- package/src/symbol/transform_text.ts +2 -5
- package/src/tile/tile.test.ts +42 -1
- package/src/tile/tile.ts +5 -1
- package/src/ui/camera.test.ts +69 -0
- package/src/ui/camera.ts +25 -17
- package/src/ui/control/scale_control.ts +6 -4
- package/src/ui/map.ts +33 -8
- package/src/ui/map_tests/map_render.test.ts +24 -0
- package/src/ui/map_tests/map_style.test.ts +53 -1
- package/src/ui/map_tests/map_terrain.test.ts +47 -0
- package/src/ui/marker.test.ts +22 -0
- package/src/ui/marker.ts +6 -0
- package/src/ui/popup.test.ts +17 -0
- package/src/util/browser.test.ts +67 -0
- package/src/util/browser.ts +18 -5
- package/src/util/dom.test.ts +18 -0
- package/src/util/dom.ts +4 -3
- package/src/util/offscreen_canvas_supported.test.ts +22 -0
- package/src/util/offscreen_canvas_supported.ts +8 -1
- package/src/util/script_detection.test.ts +53 -1
- package/src/util/script_detection.ts +7 -100
- package/src/util/unicode_properties.g.ts +128 -9
- package/src/webgl/index_buffer.ts +2 -2
- package/src/webgl/program.ts +3 -0
- package/src/webgl/render_to_texture.test.ts +122 -7
- package/src/webgl/render_to_texture.ts +41 -15
- package/src/webgl/rtt_fingerprint.test.ts +28 -13
- package/src/webgl/rtt_fingerprint.ts +24 -15
- package/src/webgl/uniform_buffer.test.ts +3 -2
- package/src/webgl/uniform_buffer.ts +9 -4
- package/src/webgl/vertex_array_object.ts +0 -17
|
@@ -4,7 +4,7 @@ import * as regenerate from 'regenerate';
|
|
|
4
4
|
/**
|
|
5
5
|
* The heuristics in the functions below are based on this version of the
|
|
6
6
|
* Unicode Standard. This constant should match the `@unicode/unicode-*` package
|
|
7
|
-
* in package.json
|
|
7
|
+
* in package.json, and the vendored extracts in `build/unicode`.
|
|
8
8
|
*
|
|
9
9
|
* When upgrading to a new version of the standard, consider any new scripts,
|
|
10
10
|
* blocks, and characters that may require different script detection.
|
|
@@ -27,16 +27,65 @@ async function createSet(blocks: string[], scripts: string[]): Promise<regenerat
|
|
|
27
27
|
return set;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Returns a character class matching the scripts that are written cursively, and so cannot have
|
|
32
|
+
* their letters spaced apart without coming apart.
|
|
33
|
+
*
|
|
34
|
+
* A script is one of them when its letters have a joining type, which says they are drawn joined to
|
|
35
|
+
* what stands beside them. Duployan is written joined up as well, by a rule the character database
|
|
36
|
+
* does not state as a joining type, so it is named here.
|
|
37
|
+
*/
|
|
38
|
+
async function isInCursiveScript(): Promise<string> {
|
|
39
|
+
const joiningTypes = await readJoiningTypes();
|
|
40
|
+
const joins = (codePoint: number) => ['D', 'L', 'R', 'C'].includes(joiningTypes.get(codePoint));
|
|
41
|
+
|
|
42
|
+
const set = await createSet([], ['Duployan']);
|
|
43
|
+
const scripts = (await import(`@unicode/unicode-${unicodeVersion}/index.js`)).default.Script;
|
|
44
|
+
for (const script of scripts) {
|
|
45
|
+
if (script === 'Common' || script === 'Inherited') continue;
|
|
46
|
+
const codePoints = (await import(`@unicode/unicode-${unicodeVersion}/Script/${script}/code-points.js`)).default;
|
|
47
|
+
if (codePoints.some(joins)) set.add(codePoints);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return set.toString();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Returns a character class matching the scripts that are written horizontally from right to left.
|
|
55
|
+
*
|
|
56
|
+
* A script is one of them when the bidirectional algorithm reads its letters from right to left. The
|
|
57
|
+
* whole script is taken, the marks and digits written with it included, so that a word keeps all its
|
|
58
|
+
* parts. `Common` is left out: it holds the characters every script shares, a handful of which are
|
|
59
|
+
* read right to left while the rest of it is not.
|
|
60
|
+
*/
|
|
61
|
+
async function isInRTLScript(): Promise<string> {
|
|
62
|
+
const readRightToLeft = new Set<number>();
|
|
63
|
+
for (const bidiClass of ['Right_To_Left', 'Arabic_Letter']) {
|
|
64
|
+
const codePoints = (await import(`@unicode/unicode-${unicodeVersion}/Bidi_Class/${bidiClass}/code-points.js`)).default;
|
|
65
|
+
for (const codePoint of codePoints) readRightToLeft.add(codePoint);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const scripts = (await import(`@unicode/unicode-${unicodeVersion}/index.js`)).default.Script;
|
|
69
|
+
const set = regenerate.default();
|
|
70
|
+
for (const script of scripts) {
|
|
71
|
+
if (script === 'Common') continue;
|
|
72
|
+
const codePoints = (await import(`@unicode/unicode-${unicodeVersion}/Script/${script}/code-points.js`)).default;
|
|
73
|
+
if (codePoints.some((codePoint: number) => readRightToLeft.has(codePoint))) set.add(codePoints);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return set.toString();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Returns a character class matching the blocks whose glyphs are drawn locally by TinySDF.
|
|
81
|
+
*
|
|
82
|
+
* These are the writing systems TinySDF draws well and cheaply: in general, any system typically set
|
|
83
|
+
* in a monospaced font. Hanzi, Kanji and Hanja, from the CJK Unified Ideographs blocks, are the
|
|
84
|
+
* clearest case, with more than 99,000 codepoints reached essentially at random, which is a great
|
|
85
|
+
* deal of bandwidth to spend on a glyph server. The smaller CJKV and other siniform blocks are drawn
|
|
86
|
+
* locally too, so that text mixing them looks of a piece.
|
|
87
|
+
*/
|
|
30
88
|
async function usesLocalIdeographFontFamily(): Promise<string> {
|
|
31
|
-
// Local rendering is preferred for Unicode code blocks that represent
|
|
32
|
-
// writing systems for which TinySDF produces optimal results and greatly
|
|
33
|
-
// reduces bandwidth consumption. In general, TinySDF is best for any
|
|
34
|
-
// writing system typically set in a monospaced font. With more than 99,000
|
|
35
|
-
// codepoints accessed essentially at random, Hanzi/Kanji/Hanja (from the
|
|
36
|
-
// CJK Unified Ideographs blocks) is the canonical example of wasteful
|
|
37
|
-
// bandwidth consumption when rendered remotely. For visual consistency
|
|
38
|
-
// within CJKV text, even relatively small CJKV and other siniform code
|
|
39
|
-
// blocks prefer local rendering.
|
|
40
89
|
const set = await createSet([
|
|
41
90
|
'CJK Compatibility Forms',
|
|
42
91
|
'CJK Compatibility',
|
|
@@ -112,67 +161,28 @@ async function allowsIdeographicBreaking(): Promise<string> {
|
|
|
112
161
|
return set.toString();
|
|
113
162
|
}
|
|
114
163
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
164
|
+
/**
|
|
165
|
+
* Returns a character class matching the characters drawn upright in vertical text.
|
|
166
|
+
*
|
|
167
|
+
* `Vertical_Orientation` names every character that may be drawn upright, without saying which of
|
|
168
|
+
* them stand upright in their own right and which only follow the characters beside them. Those that
|
|
169
|
+
* follow are taken out, being the ones {@link neutralVerticalOrientationSet} names, and the blocks
|
|
170
|
+
* added back below are the ones read both ways: upright standing alone, neutral in company. What is
|
|
171
|
+
* removed after that is a character that block reads the other way.
|
|
172
|
+
*/
|
|
123
173
|
async function hasUprightVerticalOrientation(): Promise<string> {
|
|
124
|
-
const set =
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
174
|
+
const set = regenerate.default();
|
|
175
|
+
for (const orientation of ['U', 'Tu']) {
|
|
176
|
+
set.add((await import(`@unicode/unicode-${unicodeVersion}/Vertical_Orientation/${orientation}/code-points.js`)).default);
|
|
177
|
+
}
|
|
178
|
+
set.remove(await neutralVerticalOrientationSet());
|
|
179
|
+
set.add(await createSet([
|
|
129
180
|
'CJK Compatibility Forms',
|
|
130
|
-
'CJK Compatibility',
|
|
131
|
-
'CJK Strokes',
|
|
132
181
|
'CJK Symbols And Punctuation',
|
|
133
|
-
'Counting Rod Numerals',
|
|
134
|
-
'Domino Tiles',
|
|
135
|
-
'Emoticons',
|
|
136
|
-
'Enclosed Alphanumeric Supplement',
|
|
137
|
-
'Enclosed CJK Letters And Months',
|
|
138
|
-
'Geometric Shapes Extended',
|
|
139
182
|
'Halfwidth And Fullwidth Forms',
|
|
140
|
-
'Ideographic Description Characters',
|
|
141
|
-
'Kanbun',
|
|
142
183
|
'Katakana',
|
|
143
|
-
'Mahjong Tiles',
|
|
144
|
-
'Mayan Numerals',
|
|
145
|
-
'Meroitic Hieroglyphs',
|
|
146
|
-
'Miscellaneous Symbols And Pictographs',
|
|
147
|
-
'Miscellaneous Symbols Supplement',
|
|
148
|
-
'Musical Symbols',
|
|
149
|
-
'Ornamental Dingbats',
|
|
150
|
-
'Playing Cards',
|
|
151
|
-
'Siddham',
|
|
152
184
|
'Small Form Variants',
|
|
153
|
-
|
|
154
|
-
'Soyombo',
|
|
155
|
-
'Supplemental Symbols And Pictographs',
|
|
156
|
-
'Sutton SignWriting',
|
|
157
|
-
'Symbols And Pictographs Extended-A',
|
|
158
|
-
'Tai Xuan Jing Symbols',
|
|
159
|
-
'Transport And Map Symbols',
|
|
160
|
-
'Vertical Forms',
|
|
161
|
-
'Yijing Hexagram Symbols',
|
|
162
|
-
'Zanabazar Square',
|
|
163
|
-
'Znamenny Musical Notation',
|
|
164
|
-
], [
|
|
165
|
-
'Bopomofo',
|
|
166
|
-
'Canadian Aboriginal',
|
|
167
|
-
'Han',
|
|
168
|
-
'Hangul',
|
|
169
|
-
'Hiragana',
|
|
170
|
-
'Katakana',
|
|
171
|
-
'Khitan Small Script',
|
|
172
|
-
'Nushu',
|
|
173
|
-
'Tangut',
|
|
174
|
-
'Yi',
|
|
175
|
-
]);
|
|
185
|
+
], []));
|
|
176
186
|
|
|
177
187
|
set.add(0x02EA /* modifier letter yin departing tone mark */);
|
|
178
188
|
set.add(0x02EB /* modifier letter yang departing tone mark */);
|
|
@@ -208,6 +218,16 @@ async function hasUprightVerticalOrientation(): Promise<string> {
|
|
|
208
218
|
}
|
|
209
219
|
|
|
210
220
|
async function hasNeutralVerticalOrientation(): Promise<string> {
|
|
221
|
+
return (await neutralVerticalOrientationSet()).toString();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The characters drawn upright in vertical text only because the characters beside them are.
|
|
226
|
+
*
|
|
227
|
+
* `Vertical_Orientation` does not draw this distinction, naming both these and the characters that
|
|
228
|
+
* stand upright in their own right, so which of them merely follow their neighbours is settled here.
|
|
229
|
+
*/
|
|
230
|
+
async function neutralVerticalOrientationSet(): Promise<regenerate.regenerate> {
|
|
211
231
|
const set = await createSet([
|
|
212
232
|
'CJK Compatibility Forms',
|
|
213
233
|
'CJK Symbols And Punctuation',
|
|
@@ -281,36 +301,7 @@ async function hasNeutralVerticalOrientation(): Promise<string> {
|
|
|
281
301
|
set.add(0xFFFC /* object replacement character */);
|
|
282
302
|
set.add(0xFFFD /* replacement character */);
|
|
283
303
|
|
|
284
|
-
return set
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
async function requiresComplexTextShaping(): Promise<string> {
|
|
288
|
-
// This is a rough heuristic: whether we "can render" a script
|
|
289
|
-
// actually depends on the properties of the font being used
|
|
290
|
-
// and whether differences from the ideal rendering are considered
|
|
291
|
-
// semantically significant.
|
|
292
|
-
|
|
293
|
-
// These blocks cover common scripts that require
|
|
294
|
-
// complex text shaping, based on unicode script metadata:
|
|
295
|
-
// https://www.unicode.org/repos/cldr/trunk/common/properties/scriptMetadata.txt
|
|
296
|
-
// where "Web Rank <= 32" "Shaping Required = YES"
|
|
297
|
-
const set = await createSet([
|
|
298
|
-
'Bengali',
|
|
299
|
-
'Devanagari',
|
|
300
|
-
'Gujarati',
|
|
301
|
-
'Gurmukhi',
|
|
302
|
-
'Kannada',
|
|
303
|
-
'Khmer',
|
|
304
|
-
'Malayalam',
|
|
305
|
-
'Myanmar',
|
|
306
|
-
'Oriya',
|
|
307
|
-
'Tamil',
|
|
308
|
-
'Telugu',
|
|
309
|
-
'Tibetan',
|
|
310
|
-
'Sinhala',
|
|
311
|
-
], []);
|
|
312
|
-
|
|
313
|
-
return set.toString();
|
|
304
|
+
return set;
|
|
314
305
|
}
|
|
315
306
|
|
|
316
307
|
/**
|
|
@@ -337,19 +328,20 @@ async function canFormGraphemeCluster(): Promise<string> {
|
|
|
337
328
|
* Text in these has no punctuation to break a line at, so the only way to wrap it is to ask the
|
|
338
329
|
* browser's word segmenter where the words are. Elsewhere the segmenter is the wrong tool: it
|
|
339
330
|
* isolates a comma as a word of its own, and a line must not begin with one.
|
|
331
|
+
*
|
|
332
|
+
* `Line_Break` names the South East Asian scripts that need a dictionary to be broken into lines.
|
|
333
|
+
* The three scripts added to them are written without spaces as well, and are broken by the same
|
|
334
|
+
* means, but the standard gives them a line breaking class of their own.
|
|
340
335
|
*/
|
|
341
336
|
async function isWrittenWithoutSpaces(): Promise<string> {
|
|
342
|
-
const set =
|
|
337
|
+
const set = regenerate.default();
|
|
338
|
+
set.add((await import(`@unicode/unicode-${unicodeVersion}/Line_Break/Complex_Context/code-points.js`)).default);
|
|
339
|
+
|
|
340
|
+
return set.add(await createSet([], [
|
|
343
341
|
'Balinese',
|
|
344
342
|
'Javanese',
|
|
345
|
-
'Khmer',
|
|
346
|
-
'Lao',
|
|
347
|
-
'Myanmar',
|
|
348
|
-
'Thai',
|
|
349
343
|
'Tibetan',
|
|
350
|
-
]);
|
|
351
|
-
|
|
352
|
-
return set.toString();
|
|
344
|
+
])).toString();
|
|
353
345
|
}
|
|
354
346
|
|
|
355
347
|
/**
|
|
@@ -368,9 +360,214 @@ async function joinsToTheFollowingGrapheme(): Promise<string> {
|
|
|
368
360
|
return set.toString();
|
|
369
361
|
}
|
|
370
362
|
|
|
363
|
+
const downloadedRows = new Map<string, Promise<string[][]>>();
|
|
364
|
+
/**
|
|
365
|
+
* The rows of one file of the Unicode Character Database, each split into its fields, with the
|
|
366
|
+
* comments and blank lines dropped. A file is downloaded from unicode.org and parsed once, and kept
|
|
367
|
+
* for the rest of the run.
|
|
368
|
+
*/
|
|
369
|
+
function unicodeDataRows(file: string): Promise<string[][]> {
|
|
370
|
+
if (!downloadedRows.has(file)) {
|
|
371
|
+
downloadedRows.set(file, (async () => {
|
|
372
|
+
const url = `https://www.unicode.org/Public/${unicodeVersion}/ucd/${file}`;
|
|
373
|
+
const response = await fetch(url);
|
|
374
|
+
if (!response.ok) {
|
|
375
|
+
throw new Error(`Failed to download ${url}: ${response.status} ${response.statusText}`);
|
|
376
|
+
}
|
|
377
|
+
return (await response.text())
|
|
378
|
+
.split('\n')
|
|
379
|
+
.map(line => line.split('#')[0])
|
|
380
|
+
.filter(line => line.trim())
|
|
381
|
+
.map(line => line.split(';').map(field => field.trim()));
|
|
382
|
+
})());
|
|
383
|
+
}
|
|
384
|
+
return downloadedRows.get(file);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* The blocks the Arabic script is written from, plus the two joiners that steer it.
|
|
389
|
+
*
|
|
390
|
+
* Only Arabic is tabulated. A cursive script outside these blocks has no presentation forms, and so
|
|
391
|
+
* no code point by which MapLibre could name a shaped glyph.
|
|
392
|
+
*/
|
|
393
|
+
const arabicBlocks: Array<[number, number]> = [
|
|
394
|
+
[0x0600, 0x06ff],
|
|
395
|
+
[0x0750, 0x077f],
|
|
396
|
+
[0x0870, 0x089f],
|
|
397
|
+
[0x08a0, 0x08ff],
|
|
398
|
+
[0xfb50, 0xfdff],
|
|
399
|
+
[0xfe70, 0xfeff],
|
|
400
|
+
[0x200c, 0x200d],
|
|
401
|
+
];
|
|
402
|
+
|
|
403
|
+
function isArabic(codePoint: number): boolean {
|
|
404
|
+
return arabicBlocks.some(([start, end]) => codePoint >= start && codePoint <= end);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* How a character joins to the ones beside it: `R` to the character before it, `L` to the one after
|
|
409
|
+
* it, `D` to both, `C` without being written itself, `T` not at all while letting the two around it
|
|
410
|
+
* join through it, and `U` not at all.
|
|
411
|
+
*/
|
|
412
|
+
type JoiningType = 'R' | 'L' | 'D' | 'C' | 'U' | 'T';
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* The joining type of every character: which side, if any, it joins the letters beside it on.
|
|
416
|
+
*
|
|
417
|
+
* `DerivedJoiningType.txt` states the property for every code point, the combining marks and format
|
|
418
|
+
* characters that join through included.
|
|
419
|
+
*/
|
|
420
|
+
async function readJoiningTypes(): Promise<Map<number, JoiningType>> {
|
|
421
|
+
const joiningTypes = new Map<number, JoiningType>();
|
|
422
|
+
|
|
423
|
+
for (const [codePoints, type] of await unicodeDataRows('extracted/DerivedJoiningType.txt')) {
|
|
424
|
+
const [first, last] = codePoints.split('..').map(hex => parseInt(hex, 16));
|
|
425
|
+
for (let codePoint = first; codePoint <= (last ?? first); codePoint++) {
|
|
426
|
+
joiningTypes.set(codePoint, type as JoiningType);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return joiningTypes;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** The four shapes a cursive script writes a letter in. */
|
|
434
|
+
type PresentationForms = {isolated: number; final: number; initial: number; medial: number};
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Tatweel, the stroke a word is stretched along, and the space a mark is shown over on its own.
|
|
438
|
+
*
|
|
439
|
+
* A mark has no shape without a letter under it, so the database gives its presentation forms as
|
|
440
|
+
* decompositions onto one of these: `<medial> 0640 064B` is "fathatan, as written over a letter
|
|
441
|
+
* mid-word", and `<isolated> 0020 064B` is the same mark standing by itself.
|
|
442
|
+
*/
|
|
443
|
+
const markCarriers = new Set([0x0020, 0x0640]);
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* The Presentation Forms shape of each Arabic letter, and the two shapes of each lam-alef ligature.
|
|
447
|
+
*
|
|
448
|
+
* The shapes come from the compatibility decompositions of the presentation blocks: `<final> 0628`
|
|
449
|
+
* is what says U+FE90 is the final form of beh.
|
|
450
|
+
*
|
|
451
|
+
* Of the two-character decompositions only lam-alef is taken. It is the one ligature Arabic shaping
|
|
452
|
+
* is required to form; the rest of the presentation blocks are typographic ligatures, which a font
|
|
453
|
+
* offers and the text is free to do without.
|
|
454
|
+
*/
|
|
455
|
+
async function readPresentationForms(): Promise<{
|
|
456
|
+
forms: Map<number, PresentationForms>;
|
|
457
|
+
ligatures: Map<string, {isolated: number; final: number}>;
|
|
458
|
+
}> {
|
|
459
|
+
const forms = new Map<number, PresentationForms>();
|
|
460
|
+
const ligatures = new Map<string, {isolated: number; final: number}>();
|
|
461
|
+
|
|
462
|
+
for (const row of await unicodeDataRows('UnicodeData.txt')) {
|
|
463
|
+
const codePoint = parseInt(row[0], 16);
|
|
464
|
+
if (codePoint < 0xfb50 || codePoint > 0xfeff) continue;
|
|
465
|
+
|
|
466
|
+
const match = /^<(isolated|final|initial|medial)>\s+(.+)$/.exec(row[5]);
|
|
467
|
+
if (!match) continue;
|
|
468
|
+
|
|
469
|
+
const shape = match[1] as keyof PresentationForms;
|
|
470
|
+
const bases = match[2].split(/\s+/).map(base => parseInt(base, 16));
|
|
471
|
+
const carried = bases.length === 2 && markCarriers.has(bases[0]);
|
|
472
|
+
|
|
473
|
+
if (bases.length === 1 || carried) {
|
|
474
|
+
const base = carried ? bases[1] : bases[0];
|
|
475
|
+
const existing = forms.get(base) ?? {isolated: 0, final: 0, initial: 0, medial: 0};
|
|
476
|
+
existing[shape] = codePoint;
|
|
477
|
+
forms.set(base, existing);
|
|
478
|
+
} else if (codePoint >= 0xfef5 && codePoint <= 0xfefc) {
|
|
479
|
+
const pair = String.fromCodePoint(...bases);
|
|
480
|
+
const existing = ligatures.get(pair) ?? {isolated: 0, final: 0};
|
|
481
|
+
existing[shape as 'isolated' | 'final'] = codePoint;
|
|
482
|
+
ligatures.set(pair, existing);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return {forms, ligatures};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Packs a sorted list of code points into `start,length` pairs, delta-encoded in base 36.
|
|
491
|
+
*
|
|
492
|
+
* The joining types run in long unbroken stretches, so the ranges take a fraction of the space the
|
|
493
|
+
* code points would.
|
|
494
|
+
*/
|
|
495
|
+
function encodeCodePointRanges(codePoints: number[]): string {
|
|
496
|
+
const ranges: number[][] = [];
|
|
497
|
+
for (const codePoint of codePoints.sort((a, b) => a - b)) {
|
|
498
|
+
const last = ranges[ranges.length - 1];
|
|
499
|
+
if (last && last[0] + last[1] === codePoint) {
|
|
500
|
+
last[1]++;
|
|
501
|
+
} else {
|
|
502
|
+
ranges.push([codePoint, 1]);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
let previousEnd = 0;
|
|
507
|
+
return ranges
|
|
508
|
+
.map(([start, length]) => {
|
|
509
|
+
const encoded = `${(start - previousEnd).toString(36)},${length.toString(36)}`;
|
|
510
|
+
previousEnd = start + length;
|
|
511
|
+
return encoded;
|
|
512
|
+
})
|
|
513
|
+
.join(';');
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** The joining types, as one encoded set of ranges per type, ready to print as an object literal. */
|
|
517
|
+
async function encodedJoiningTypes(): Promise<string> {
|
|
518
|
+
const byType = new Map<JoiningType, number[]>();
|
|
519
|
+
for (const [codePoint, type] of await readJoiningTypes()) {
|
|
520
|
+
if (type === 'U' || !isArabic(codePoint)) continue;
|
|
521
|
+
byType.set(type, (byType.get(type) ?? []).concat(codePoint));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return [...byType]
|
|
525
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
526
|
+
.map(([type, codePoints]) => ` ${type}: '${encodeCodePointRanges(codePoints)}',`)
|
|
527
|
+
.join('\n');
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** Each Arabic letter's four presentation forms, ready to print as an object literal. */
|
|
531
|
+
async function encodedPresentationForms(): Promise<string> {
|
|
532
|
+
return [...(await readPresentationForms()).forms]
|
|
533
|
+
.sort(([a], [b]) => a - b)
|
|
534
|
+
.map(([base, {isolated, final, initial, medial}]) =>
|
|
535
|
+
` ${base}: [${isolated}, ${final}, ${initial}, ${medial}],`)
|
|
536
|
+
.join('\n');
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** The lam-alef ligatures, ready to print as an object literal. */
|
|
540
|
+
async function encodedLigatures(): Promise<string> {
|
|
541
|
+
return [...(await readPresentationForms()).ligatures]
|
|
542
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
543
|
+
.map(([pair, {isolated, final}]) => {
|
|
544
|
+
const escaped = [...pair]
|
|
545
|
+
.map(character => `\\u${character.codePointAt(0).toString(16).padStart(4, '0')}`)
|
|
546
|
+
.join('');
|
|
547
|
+
return ` '${escaped}': [${isolated}, ${final}],`;
|
|
548
|
+
})
|
|
549
|
+
.join('\n');
|
|
550
|
+
}
|
|
551
|
+
|
|
371
552
|
fs.writeFileSync('src/util/unicode_properties.g.ts',
|
|
372
553
|
`// This file is generated. Edit build/generate-unicode-data.ts, then run \`npm run generate-unicode-data\`.
|
|
373
554
|
|
|
555
|
+
/**
|
|
556
|
+
* Returns whether the given codepoint belongs to a script that is written cursively, whose letters
|
|
557
|
+
* therefore cannot be spaced apart.
|
|
558
|
+
*/
|
|
559
|
+
export function codePointIsInCursiveScript(codePoint: number): boolean {
|
|
560
|
+
return /${await isInCursiveScript()}/gim.test(String.fromCodePoint(codePoint));
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Returns whether the given codepoint belongs to a script that is written horizontally from right
|
|
565
|
+
* to left.
|
|
566
|
+
*/
|
|
567
|
+
export function codePointIsInRTLScript(codePoint: number): boolean {
|
|
568
|
+
return /${await isInRTLScript()}/gim.test(String.fromCodePoint(codePoint));
|
|
569
|
+
}
|
|
570
|
+
|
|
374
571
|
/**
|
|
375
572
|
* Returns whether the fallback fonts specified by the
|
|
376
573
|
* \`localIdeographFontFamily\` map option apply to the given codepoint.
|
|
@@ -416,13 +613,6 @@ export function codePointHasNeutralVerticalOrientation(codePoint: number): boole
|
|
|
416
613
|
return /${await hasNeutralVerticalOrientation()}/gim.test(String.fromCodePoint(codePoint));
|
|
417
614
|
}
|
|
418
615
|
|
|
419
|
-
/**
|
|
420
|
-
* Returns whether the give codepoint is likely to require complex text shaping.
|
|
421
|
-
*/
|
|
422
|
-
export function codePointRequiresComplexTextShaping(codePoint: number): boolean {
|
|
423
|
-
return /${await requiresComplexTextShaping()}/gim.test(String.fromCodePoint(codePoint));
|
|
424
|
-
}
|
|
425
|
-
|
|
426
616
|
/**
|
|
427
617
|
* Returns whether the text could hold a grapheme cluster of more than one codepoint, and so is worth
|
|
428
618
|
* segmenting. A negative answer means every codepoint of it stands alone.
|
|
@@ -450,4 +640,25 @@ export function codePointIsWrittenWithoutSpaces(codePoint: number): boolean {
|
|
|
450
640
|
export function canCombineGraphemes(former: string, latter: string): boolean {
|
|
451
641
|
return /(?:${await joinsToTheFollowingGrapheme()})$/.test(former) || /^\\p{gc=Mc}/u.test(latter);
|
|
452
642
|
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* The joining type of each Arabic character, as \`start,length\` code point ranges delta-encoded in
|
|
646
|
+
* base 36, one entry per type. Anything absent from all of them is non-joining.
|
|
647
|
+
*/
|
|
648
|
+
export const ENCODED_JOINING_TYPES: Record<string, string> = {
|
|
649
|
+
${await encodedJoiningTypes()}
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Each Arabic letter's Presentation Forms code points, as \`[isolated, final, initial, medial]\`.
|
|
654
|
+
* A shape the letter is not written in is 0.
|
|
655
|
+
*/
|
|
656
|
+
export const PRESENTATION_FORMS: Record<number, [number, number, number, number]> = {
|
|
657
|
+
${await encodedPresentationForms()}
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
/** The lam-alef ligatures, keyed by the pair of letters they replace, as \`[isolated, final]\`. */
|
|
661
|
+
export const LIGATURES: Record<string, [number, number]> = {
|
|
662
|
+
${await encodedLigatures()}
|
|
663
|
+
};
|
|
453
664
|
`);
|