@cantoo/fontkit 2.0.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/LICENSE +21 -0
- package/README.md +274 -0
- package/dist/browser-module.mjs +13660 -0
- package/dist/browser-module.mjs.map +1 -0
- package/dist/browser.cjs +13689 -0
- package/dist/browser.cjs.map +1 -0
- package/dist/main.cjs +13712 -0
- package/dist/main.cjs.map +1 -0
- package/dist/module.mjs +13673 -0
- package/dist/module.mjs.map +1 -0
- package/package.json +99 -0
- package/src/CmapProcessor.js +285 -0
- package/src/DFont.js +113 -0
- package/src/TTFFont.js +621 -0
- package/src/TrueTypeCollection.js +65 -0
- package/src/WOFF2Font.js +222 -0
- package/src/WOFFFont.js +56 -0
- package/src/aat/AATFeatureMap.js +545 -0
- package/src/aat/AATLayoutEngine.js +48 -0
- package/src/aat/AATLookupTable.js +125 -0
- package/src/aat/AATMorxProcessor.js +429 -0
- package/src/aat/AATStateMachine.js +96 -0
- package/src/base.js +29 -0
- package/src/cff/CFFCharsets.js +99 -0
- package/src/cff/CFFDict.js +166 -0
- package/src/cff/CFFEncodings.js +48 -0
- package/src/cff/CFFFont.js +158 -0
- package/src/cff/CFFIndex.js +150 -0
- package/src/cff/CFFOperand.js +135 -0
- package/src/cff/CFFPointer.js +50 -0
- package/src/cff/CFFPrivateDict.js +39 -0
- package/src/cff/CFFStandardStrings.js +71 -0
- package/src/cff/CFFTop.js +236 -0
- package/src/decorators.js +36 -0
- package/src/encodings.js +219 -0
- package/src/fs.js +12 -0
- package/src/glyph/BBox.js +72 -0
- package/src/glyph/CBDTGlyph.js +9 -0
- package/src/glyph/CFFGlyph.js +635 -0
- package/src/glyph/COLRGlyph.js +108 -0
- package/src/glyph/Glyph.js +270 -0
- package/src/glyph/GlyphVariationProcessor.js +487 -0
- package/src/glyph/Path.js +257 -0
- package/src/glyph/SBIXGlyph.js +54 -0
- package/src/glyph/StandardNames.js +27 -0
- package/src/glyph/TTFGlyph.js +408 -0
- package/src/glyph/TTFGlyphEncoder.js +158 -0
- package/src/glyph/WOFF2Glyph.js +70 -0
- package/src/index.js +15 -0
- package/src/layout/GlyphPosition.js +30 -0
- package/src/layout/GlyphRun.js +108 -0
- package/src/layout/KernProcessor.js +97 -0
- package/src/layout/LayoutEngine.js +190 -0
- package/src/layout/Script.js +231 -0
- package/src/layout/UnicodeLayoutEngine.js +250 -0
- package/src/node.js +16 -0
- package/src/opentype/GPOSProcessor.js +311 -0
- package/src/opentype/GSUBProcessor.js +208 -0
- package/src/opentype/GlyphInfo.js +57 -0
- package/src/opentype/GlyphIterator.js +82 -0
- package/src/opentype/OTLayoutEngine.js +117 -0
- package/src/opentype/OTProcessor.js +472 -0
- package/src/opentype/ShapingPlan.js +121 -0
- package/src/opentype/shapers/ArabicShaper.js +124 -0
- package/src/opentype/shapers/DefaultShaper.js +116 -0
- package/src/opentype/shapers/HangulShaper.js +285 -0
- package/src/opentype/shapers/HebrewShaper.js +105 -0
- package/src/opentype/shapers/IndicShaper.js +914 -0
- package/src/opentype/shapers/ThaiShaper.js +238 -0
- package/src/opentype/shapers/UniversalShaper.js +186 -0
- package/src/opentype/shapers/data.trie +0 -0
- package/src/opentype/shapers/gen-indic.js +208 -0
- package/src/opentype/shapers/gen-use.js +287 -0
- package/src/opentype/shapers/generate-data.js +33 -0
- package/src/opentype/shapers/index.js +109 -0
- package/src/opentype/shapers/indic-data.js +175 -0
- package/src/opentype/shapers/indic.json +1 -0
- package/src/opentype/shapers/indic.machine +31 -0
- package/src/opentype/shapers/indic.trie +0 -0
- package/src/opentype/shapers/use.json +1 -0
- package/src/opentype/shapers/use.machine +44 -0
- package/src/opentype/shapers/use.trie +0 -0
- package/src/packages/unicode-properties/LICENSE +8 -0
- package/src/packages/unicode-properties/README.md +12 -0
- package/src/packages/unicode-properties/data-trie.js +1 -0
- package/src/packages/unicode-properties/data.js +1 -0
- package/src/packages/unicode-properties/data.json +1 -0
- package/src/packages/unicode-properties/data.trie +0 -0
- package/src/packages/unicode-properties/dist/index.cjs +257 -0
- package/src/packages/unicode-properties/dist/index.cjs.map +7 -0
- package/src/packages/unicode-properties/dist/index.mjs +234 -0
- package/src/packages/unicode-properties/dist/index.mjs.map +7 -0
- package/src/packages/unicode-properties/generate.js +107 -0
- package/src/packages/unicode-properties/index.js +184 -0
- package/src/packages/unicode-properties/package.json +50 -0
- package/src/packages/unicode-trie/LICENSE +8 -0
- package/src/packages/unicode-trie/README.md +13 -0
- package/src/packages/unicode-trie/builder.js +965 -0
- package/src/packages/unicode-trie/dist/builder.cjs +711 -0
- package/src/packages/unicode-trie/dist/builder.cjs.map +7 -0
- package/src/packages/unicode-trie/dist/builder.mjs +690 -0
- package/src/packages/unicode-trie/dist/builder.mjs.map +7 -0
- package/src/packages/unicode-trie/dist/index.cjs +112 -0
- package/src/packages/unicode-trie/dist/index.cjs.map +7 -0
- package/src/packages/unicode-trie/dist/index.mjs +91 -0
- package/src/packages/unicode-trie/dist/index.mjs.map +7 -0
- package/src/packages/unicode-trie/index.js +135 -0
- package/src/packages/unicode-trie/package.json +49 -0
- package/src/packages/unicode-trie/swap.js +21 -0
- package/src/subset/CFFSubset.js +176 -0
- package/src/subset/Subset.js +63 -0
- package/src/subset/TTFSubset.js +130 -0
- package/src/tables/BASE.js +78 -0
- package/src/tables/COLR.js +25 -0
- package/src/tables/CPAL.js +24 -0
- package/src/tables/DSIG.js +21 -0
- package/src/tables/EBDT.js +91 -0
- package/src/tables/EBLC.js +80 -0
- package/src/tables/GDEF.js +57 -0
- package/src/tables/GPOS.js +209 -0
- package/src/tables/GSUB.js +85 -0
- package/src/tables/HVAR.js +44 -0
- package/src/tables/JSTF.js +43 -0
- package/src/tables/LTSH.js +10 -0
- package/src/tables/OS2.js +84 -0
- package/src/tables/PCLT.js +21 -0
- package/src/tables/STAT.js +99 -0
- package/src/tables/VDMX.js +33 -0
- package/src/tables/VORG.js +14 -0
- package/src/tables/WOFF2Directory.js +73 -0
- package/src/tables/WOFFDirectory.js +37 -0
- package/src/tables/aat.js +157 -0
- package/src/tables/avar.js +19 -0
- package/src/tables/bsln.js +31 -0
- package/src/tables/cmap.js +127 -0
- package/src/tables/cvt.js +6 -0
- package/src/tables/directory.js +58 -0
- package/src/tables/feat.js +28 -0
- package/src/tables/fpgm.js +8 -0
- package/src/tables/fvar.js +31 -0
- package/src/tables/gasp.js +15 -0
- package/src/tables/glyf.js +4 -0
- package/src/tables/gvar.js +27 -0
- package/src/tables/hdmx.js +15 -0
- package/src/tables/head.js +25 -0
- package/src/tables/hhea.js +19 -0
- package/src/tables/hmtx.js +11 -0
- package/src/tables/index.js +119 -0
- package/src/tables/just.js +81 -0
- package/src/tables/kern.js +91 -0
- package/src/tables/loca.js +30 -0
- package/src/tables/maxp.js +20 -0
- package/src/tables/morx.js +79 -0
- package/src/tables/name.js +157 -0
- package/src/tables/opbd.js +15 -0
- package/src/tables/opentype.js +217 -0
- package/src/tables/post.js +34 -0
- package/src/tables/prep.js +6 -0
- package/src/tables/sbix.js +17 -0
- package/src/tables/variations.js +80 -0
- package/src/tables/vhea.js +19 -0
- package/src/tables/vmtx.js +12 -0
- package/src/utils.js +62 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import DefaultShaper from './DefaultShaper';
|
|
2
|
+
import GlyphInfo from '../GlyphInfo';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Thai / Lao shaper (HarfBuzz hb-ot-shaper-thai.cc):
|
|
6
|
+
* 1. Decompose SARA AM → NIKHAHIT + SARA AA and reorder NIKHAHIT past above marks
|
|
7
|
+
* 2. PUA tone/vowel shift fallback for legacy fonts without Thai GSUB
|
|
8
|
+
*/
|
|
9
|
+
export default class ThaiShaper extends DefaultShaper {
|
|
10
|
+
static assignFeatures(plan, glyphs) {
|
|
11
|
+
super.assignFeatures(plan, glyphs);
|
|
12
|
+
preprocessThai(glyphs, plan.font);
|
|
13
|
+
// HB: PUA only when buffer script is Thai and the font has no Thai GSUB.
|
|
14
|
+
if (isThaiBufferScript(plan.bufferScript || plan.script) && !hasThaiGsub(plan.font)) {
|
|
15
|
+
applyThaiPuaShaping(glyphs, plan.font);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Thai/Lao SARA AM differ only by the 0x80 bit (U+0E33 / U+0EB3).
|
|
21
|
+
function isSaraAm(u) {
|
|
22
|
+
return (u & ~0x0080) === 0x0E33;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function nikhahitFromSaraAm(u) {
|
|
26
|
+
return u - 0x0E33 + 0x0E4D;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function saraAaFromSaraAm(u) {
|
|
30
|
+
return u - 1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Above-base marks (Thai; Lao is the same set with +0x80).
|
|
34
|
+
function isAboveBaseMark(u) {
|
|
35
|
+
const c = u & ~0x0080;
|
|
36
|
+
return c === 0x0E31
|
|
37
|
+
|| (c >= 0x0E34 && c <= 0x0E37)
|
|
38
|
+
|| (c >= 0x0E47 && c <= 0x0E4E)
|
|
39
|
+
|| c === 0x0E3B;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function preprocessThai(glyphs, font) {
|
|
43
|
+
let i = 0;
|
|
44
|
+
while (i < glyphs.length) {
|
|
45
|
+
const u = glyphs[i].codePoints[0];
|
|
46
|
+
if (!isSaraAm(u)) {
|
|
47
|
+
i++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const features = glyphs[i].features;
|
|
52
|
+
const nikhahit = makeGlyph(font, nikhahitFromSaraAm(u), features);
|
|
53
|
+
const saraAa = makeGlyph(font, saraAaFromSaraAm(u), features);
|
|
54
|
+
glyphs.splice(i, 1, nikhahit, saraAa);
|
|
55
|
+
|
|
56
|
+
// Walk NIKHAHIT backward over above-base marks toward the base.
|
|
57
|
+
let nikhahitIndex = i;
|
|
58
|
+
let target = nikhahitIndex;
|
|
59
|
+
while (target > 0 && isAboveBaseMark(glyphs[target - 1].codePoints[0])) {
|
|
60
|
+
target--;
|
|
61
|
+
}
|
|
62
|
+
if (target !== nikhahitIndex) {
|
|
63
|
+
const moved = glyphs.splice(nikhahitIndex, 1)[0];
|
|
64
|
+
glyphs.splice(target, 0, moved);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
i += 2;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function makeGlyph(font, codePoint, features) {
|
|
72
|
+
const id = font.glyphForCodePoint(codePoint).id;
|
|
73
|
+
return new GlyphInfo(font, id, [codePoint], features);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// PUA fallback: above/below state machines remap marks (and some bases) to
|
|
77
|
+
// Windows/Mac private-use codepoints when the font ships those glyphs.
|
|
78
|
+
const NOP = 0;
|
|
79
|
+
const SD = 1;
|
|
80
|
+
const SL = 2;
|
|
81
|
+
const SDL = 3;
|
|
82
|
+
const RD = 4;
|
|
83
|
+
|
|
84
|
+
const NC = 0; // normal consonant
|
|
85
|
+
const AC = 1; // ascender (1B/1D/1F)
|
|
86
|
+
const RC = 2; // removable descender (0D/10)
|
|
87
|
+
const DC = 3; // strict descender (0E/0F)
|
|
88
|
+
const NOT_CONSONANT = 4;
|
|
89
|
+
|
|
90
|
+
const AV = 0; // above-base vowel/mark
|
|
91
|
+
const BV = 1; // below-base vowel/mark
|
|
92
|
+
const T = 2; // tone mark
|
|
93
|
+
const NOT_MARK = 3;
|
|
94
|
+
|
|
95
|
+
function getConsonantType(u) {
|
|
96
|
+
if (u === 0x0E1B || u === 0x0E1D || u === 0x0E1F) return AC;
|
|
97
|
+
if (u === 0x0E0D || u === 0x0E10) return RC;
|
|
98
|
+
if (u === 0x0E0E || u === 0x0E0F) return DC;
|
|
99
|
+
if (u >= 0x0E01 && u <= 0x0E2E) return NC;
|
|
100
|
+
return NOT_CONSONANT;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getMarkType(u) {
|
|
104
|
+
if (
|
|
105
|
+
u === 0x0E31 ||
|
|
106
|
+
(u >= 0x0E34 && u <= 0x0E37) ||
|
|
107
|
+
u === 0x0E47 ||
|
|
108
|
+
(u >= 0x0E4D && u <= 0x0E4E)
|
|
109
|
+
) {
|
|
110
|
+
return AV;
|
|
111
|
+
}
|
|
112
|
+
if (u >= 0x0E38 && u <= 0x0E3A) return BV;
|
|
113
|
+
if (u >= 0x0E48 && u <= 0x0E4C) return T;
|
|
114
|
+
return NOT_MARK;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const T0 = 0, T1 = 1, T2 = 2, T3 = 3;
|
|
118
|
+
const ABOVE_START_STATE = [T0, T1, T0, T0, T3]; // NC AC RC DC NOT_CONSONANT
|
|
119
|
+
const ABOVE_STATE_MACHINE = [
|
|
120
|
+
// AV BV T
|
|
121
|
+
[[NOP, T3], [NOP, T0], [SD, T3]], // T0
|
|
122
|
+
[[SL, T2], [NOP, T1], [SDL, T2]], // T1
|
|
123
|
+
[[NOP, T3], [NOP, T2], [SL, T3]], // T2
|
|
124
|
+
[[NOP, T3], [NOP, T3], [NOP, T3]] // T3
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
const B0 = 0, B1 = 1, B2 = 2;
|
|
128
|
+
const BELOW_START_STATE = [B0, B0, B1, B2, B2];
|
|
129
|
+
const BELOW_STATE_MACHINE = [
|
|
130
|
+
// AV BV T
|
|
131
|
+
[[NOP, B0], [NOP, B2], [NOP, B0]], // B0
|
|
132
|
+
[[NOP, B1], [RD, B2], [NOP, B1]], // B1
|
|
133
|
+
[[NOP, B2], [SD, B2], [NOP, B2]] // B2
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
// [original, Windows PUA, Mac PUA] per action
|
|
137
|
+
const PUA_MAPPINGS = {
|
|
138
|
+
[SD]: [
|
|
139
|
+
[0x0E48, 0xF70A, 0xF88B], // MAI EK
|
|
140
|
+
[0x0E49, 0xF70B, 0xF88E], // MAI THO
|
|
141
|
+
[0x0E4A, 0xF70C, 0xF891], // MAI TRI
|
|
142
|
+
[0x0E4B, 0xF70D, 0xF894], // MAI CHATTAWA
|
|
143
|
+
[0x0E4C, 0xF70E, 0xF897], // THANTHAKHAT
|
|
144
|
+
[0x0E38, 0xF718, 0xF89B], // SARA U
|
|
145
|
+
[0x0E39, 0xF719, 0xF89C], // SARA UU
|
|
146
|
+
[0x0E3A, 0xF71A, 0xF89D] // PHINTHU
|
|
147
|
+
],
|
|
148
|
+
[SDL]: [
|
|
149
|
+
[0x0E48, 0xF705, 0xF88C],
|
|
150
|
+
[0x0E49, 0xF706, 0xF88F],
|
|
151
|
+
[0x0E4A, 0xF707, 0xF892],
|
|
152
|
+
[0x0E4B, 0xF708, 0xF895],
|
|
153
|
+
[0x0E4C, 0xF709, 0xF898]
|
|
154
|
+
],
|
|
155
|
+
[SL]: [
|
|
156
|
+
[0x0E48, 0xF713, 0xF88A],
|
|
157
|
+
[0x0E49, 0xF714, 0xF88D],
|
|
158
|
+
[0x0E4A, 0xF715, 0xF890],
|
|
159
|
+
[0x0E4B, 0xF716, 0xF893],
|
|
160
|
+
[0x0E4C, 0xF717, 0xF896],
|
|
161
|
+
[0x0E31, 0xF710, 0xF884], // MAI HAN-AKAT
|
|
162
|
+
[0x0E34, 0xF701, 0xF885], // SARA I
|
|
163
|
+
[0x0E35, 0xF702, 0xF886], // SARA II
|
|
164
|
+
[0x0E36, 0xF703, 0xF887], // SARA UE
|
|
165
|
+
[0x0E37, 0xF704, 0xF888], // SARA UEE
|
|
166
|
+
[0x0E47, 0xF712, 0xF889], // MAITAIKHU
|
|
167
|
+
[0x0E4D, 0xF711, 0xF899] // NIKHAHIT
|
|
168
|
+
],
|
|
169
|
+
[RD]: [
|
|
170
|
+
[0x0E0D, 0xF70F, 0xF89A], // YO YING
|
|
171
|
+
[0x0E10, 0xF700, 0xF89E] // THO THAN
|
|
172
|
+
]
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
function thaiPuaShape(u, action, font) {
|
|
176
|
+
if (action === NOP) return u;
|
|
177
|
+
const mappings = PUA_MAPPINGS[action];
|
|
178
|
+
if (!mappings) return u;
|
|
179
|
+
for (const [orig, winPua, macPua] of mappings) {
|
|
180
|
+
if (orig !== u) continue;
|
|
181
|
+
if (font.hasGlyphForCodePoint(winPua)) return winPua;
|
|
182
|
+
if (font.hasGlyphForCodePoint(macPua)) return macPua;
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
return u;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function replaceGlyphCodePoint(glyphs, index, newCp, font) {
|
|
189
|
+
const prev = glyphs[index];
|
|
190
|
+
if (prev.codePoints[0] === newCp) return;
|
|
191
|
+
glyphs[index] = new GlyphInfo(font, font.glyphForCodePoint(newCp).id, [newCp], prev.features);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function applyThaiPuaShaping(glyphs, font) {
|
|
195
|
+
let aboveState = ABOVE_START_STATE[NOT_CONSONANT];
|
|
196
|
+
let belowState = BELOW_START_STATE[NOT_CONSONANT];
|
|
197
|
+
let baseIndex = 0;
|
|
198
|
+
|
|
199
|
+
for (let i = 0; i < glyphs.length; i++) {
|
|
200
|
+
const u = glyphs[i].codePoints[0];
|
|
201
|
+
const mt = getMarkType(u);
|
|
202
|
+
|
|
203
|
+
if (mt === NOT_MARK) {
|
|
204
|
+
const ct = getConsonantType(u);
|
|
205
|
+
aboveState = ABOVE_START_STATE[ct];
|
|
206
|
+
belowState = BELOW_START_STATE[ct];
|
|
207
|
+
baseIndex = i;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const [aboveAction, aboveNext] = ABOVE_STATE_MACHINE[aboveState][mt];
|
|
212
|
+
const [belowAction, belowNext] = BELOW_STATE_MACHINE[belowState][mt];
|
|
213
|
+
aboveState = aboveNext;
|
|
214
|
+
belowState = belowNext;
|
|
215
|
+
|
|
216
|
+
// At most one action is non-NOP.
|
|
217
|
+
const action = aboveAction !== NOP ? aboveAction : belowAction;
|
|
218
|
+
if (action === NOP) continue;
|
|
219
|
+
|
|
220
|
+
if (action === RD) {
|
|
221
|
+
replaceGlyphCodePoint(glyphs, baseIndex, thaiPuaShape(glyphs[baseIndex].codePoints[0], action, font), font);
|
|
222
|
+
} else {
|
|
223
|
+
replaceGlyphCodePoint(glyphs, i, thaiPuaShape(u, action, font), font);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Gate PUA shaping on absence of Thai GSUB (HB plan->map.found_script[0]).
|
|
229
|
+
function hasThaiGsub(font) {
|
|
230
|
+
const gsub = font.GSUB;
|
|
231
|
+
if (!gsub || !gsub.scriptList) return false;
|
|
232
|
+
return gsub.scriptList.some(entry => entry.tag === 'thai' || entry.tag === 'tha2');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function isThaiBufferScript(script) {
|
|
236
|
+
if (Array.isArray(script)) return script.includes('thai');
|
|
237
|
+
return script === 'thai';
|
|
238
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import DefaultShaper from './DefaultShaper';
|
|
2
|
+
import StateMachine from 'dfa';
|
|
3
|
+
import UnicodeTrie from '../../packages/unicode-trie/index.js';
|
|
4
|
+
import GlyphInfo from '../GlyphInfo';
|
|
5
|
+
import useData from './use.json';
|
|
6
|
+
import { decodeBase64 } from '../../utils';
|
|
7
|
+
import useTrie from './use.trie';
|
|
8
|
+
|
|
9
|
+
const {categories, decompositions} = useData;
|
|
10
|
+
const trie = new UnicodeTrie(decodeBase64(useTrie));
|
|
11
|
+
const stateMachine = new StateMachine(useData);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* This shaper is an implementation of the Universal Shaping Engine, which
|
|
15
|
+
* uses Unicode data to shape a number of scripts without a dedicated shaping engine.
|
|
16
|
+
* See https://www.microsoft.com/typography/OpenTypeDev/USE/intro.htm.
|
|
17
|
+
*/
|
|
18
|
+
export default class UniversalShaper extends DefaultShaper {
|
|
19
|
+
static zeroMarkWidths = 'BEFORE_GPOS';
|
|
20
|
+
static planFeatures(plan) {
|
|
21
|
+
plan.addStage(setupSyllables);
|
|
22
|
+
|
|
23
|
+
// Default glyph pre-processing group
|
|
24
|
+
plan.addStage(['locl', 'ccmp', 'nukt', 'akhn']);
|
|
25
|
+
|
|
26
|
+
// Reordering group
|
|
27
|
+
plan.addStage(clearSubstitutionFlags);
|
|
28
|
+
plan.addStage(['rphf'], false);
|
|
29
|
+
plan.addStage(recordRphf);
|
|
30
|
+
plan.addStage(clearSubstitutionFlags);
|
|
31
|
+
plan.addStage(['pref']);
|
|
32
|
+
plan.addStage(recordPref);
|
|
33
|
+
|
|
34
|
+
// Orthographic unit shaping group
|
|
35
|
+
plan.addStage(['rkrf', 'abvf', 'blwf', 'half', 'pstf', 'vatu', 'cjct']);
|
|
36
|
+
plan.addStage(reorder);
|
|
37
|
+
|
|
38
|
+
// Topographical features
|
|
39
|
+
// Scripts that need this are handled by the Arabic shaper, not implemented here for now.
|
|
40
|
+
// plan.addStage(['isol', 'init', 'medi', 'fina', 'med2', 'fin2', 'fin3'], false);
|
|
41
|
+
|
|
42
|
+
// Standard topographic presentation and positional feature application
|
|
43
|
+
plan.addStage(['abvs', 'blws', 'pres', 'psts', 'dist', 'abvm', 'blwm']);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static assignFeatures(plan, glyphs) {
|
|
47
|
+
// Decompose split vowels
|
|
48
|
+
// TODO: do this in a more general unicode normalizer
|
|
49
|
+
for (let i = glyphs.length - 1; i >= 0; i--) {
|
|
50
|
+
let codepoint = glyphs[i].codePoints[0];
|
|
51
|
+
if (decompositions[codepoint]) {
|
|
52
|
+
let decomposed = decompositions[codepoint].map(c => {
|
|
53
|
+
let g = plan.font.glyphForCodePoint(c);
|
|
54
|
+
return new GlyphInfo(plan.font, g.id, [c], glyphs[i].features);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
glyphs.splice(i, 1, ...decomposed);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function useCategory(glyph) {
|
|
64
|
+
return trie.get(glyph.codePoints[0]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
class USEInfo {
|
|
68
|
+
constructor(category, syllableType, syllable) {
|
|
69
|
+
this.category = category;
|
|
70
|
+
this.syllableType = syllableType;
|
|
71
|
+
this.syllable = syllable;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function setupSyllables(font, glyphs) {
|
|
76
|
+
let syllable = 0;
|
|
77
|
+
for (let [start, end, tags] of stateMachine.match(glyphs.map(useCategory))) {
|
|
78
|
+
++syllable;
|
|
79
|
+
|
|
80
|
+
// Create shaper info
|
|
81
|
+
for (let i = start; i <= end; i++) {
|
|
82
|
+
glyphs[i].shaperInfo = new USEInfo(categories[useCategory(glyphs[i])], tags[0], syllable);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Assign rphf feature
|
|
86
|
+
let limit = glyphs[start].shaperInfo.category === 'R' ? 1 : Math.min(3, end - start);
|
|
87
|
+
for (let i = start; i < start + limit; i++) {
|
|
88
|
+
glyphs[i].features.rphf = true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function clearSubstitutionFlags(font, glyphs) {
|
|
94
|
+
for (let glyph of glyphs) {
|
|
95
|
+
glyph.substituted = false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function recordRphf(font, glyphs) {
|
|
100
|
+
for (let glyph of glyphs) {
|
|
101
|
+
if (glyph.substituted && glyph.features.rphf) {
|
|
102
|
+
// Mark a substituted repha.
|
|
103
|
+
glyph.shaperInfo.category = 'R';
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function recordPref(font, glyphs) {
|
|
109
|
+
for (let glyph of glyphs) {
|
|
110
|
+
if (glyph.substituted) {
|
|
111
|
+
// Mark a substituted pref as VPre, as they behave the same way.
|
|
112
|
+
glyph.shaperInfo.category = 'VPre';
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function reorder(font, glyphs) {
|
|
118
|
+
let dottedCircle = font.glyphForCodePoint(0x25cc).id;
|
|
119
|
+
|
|
120
|
+
for (let start = 0, end = nextSyllable(glyphs, 0); start < glyphs.length; start = end, end = nextSyllable(glyphs, start)) {
|
|
121
|
+
let i, j;
|
|
122
|
+
let info = glyphs[start].shaperInfo;
|
|
123
|
+
let type = info.syllableType;
|
|
124
|
+
|
|
125
|
+
// Only a few syllable types need reordering.
|
|
126
|
+
if (type !== 'virama_terminated_cluster' && type !== 'standard_cluster' && type !== 'broken_cluster') {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Insert a dotted circle glyph in broken clusters.
|
|
131
|
+
if (type === 'broken_cluster' && dottedCircle) {
|
|
132
|
+
let g = new GlyphInfo(font, dottedCircle, [0x25cc]);
|
|
133
|
+
g.shaperInfo = info;
|
|
134
|
+
|
|
135
|
+
// Insert after possible Repha.
|
|
136
|
+
for (i = start; i < end && glyphs[i].shaperInfo.category === 'R'; i++);
|
|
137
|
+
glyphs.splice(i + 1, 0, g);
|
|
138
|
+
end++;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Move things forward.
|
|
142
|
+
if (info.category === 'R' && end - start > 1) {
|
|
143
|
+
// Got a repha. Reorder it to after first base, before first halant.
|
|
144
|
+
for (i = start + 1; i < end; i++) {
|
|
145
|
+
info = glyphs[i].shaperInfo;
|
|
146
|
+
if (isBase(info) || isHalant(glyphs[i])) {
|
|
147
|
+
// If we hit a halant, move before it; otherwise it's a base: move to it's
|
|
148
|
+
// place, and shift things in between backward.
|
|
149
|
+
if (isHalant(glyphs[i])) {
|
|
150
|
+
i--;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
glyphs.splice(start, 0, ...glyphs.splice(start + 1, i - start), glyphs[i]);
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Move things back.
|
|
160
|
+
for (i = start, j = end; i < end; i++) {
|
|
161
|
+
info = glyphs[i].shaperInfo;
|
|
162
|
+
if (isBase(info) || isHalant(glyphs[i])) {
|
|
163
|
+
// If we hit a halant, move after it; otherwise it's a base: move to it's
|
|
164
|
+
// place, and shift things in between backward.
|
|
165
|
+
j = isHalant(glyphs[i]) ? i + 1 : i;
|
|
166
|
+
} else if ((info.category === 'VPre' || info.category === 'VMPre') && j < i) {
|
|
167
|
+
glyphs.splice(j, 1, glyphs[i], ...glyphs.splice(j, i - j));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function nextSyllable(glyphs, start) {
|
|
174
|
+
if (start >= glyphs.length) return start;
|
|
175
|
+
let syllable = glyphs[start].shaperInfo.syllable;
|
|
176
|
+
while (++start < glyphs.length && glyphs[start].shaperInfo.syllable === syllable);
|
|
177
|
+
return start;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isHalant(glyph) {
|
|
181
|
+
return glyph.shaperInfo.category === 'H' && !glyph.isLigated;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function isBase(info) {
|
|
185
|
+
return info.category === 'B' || info.category === 'GB';
|
|
186
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import codepoints from 'codepoints';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import UnicodeTrieBuilder from '../../packages/unicode-trie/builder.js';
|
|
4
|
+
import dfa from 'dfa/compile.js';
|
|
5
|
+
import { CATEGORIES, POSITIONS, CONSONANT_FLAGS } from './indic-data.js';
|
|
6
|
+
|
|
7
|
+
const compile = dfa.default;
|
|
8
|
+
|
|
9
|
+
const CATEGORY_MAP = {
|
|
10
|
+
Avagraha: 'Symbol',
|
|
11
|
+
Bindu: 'SM',
|
|
12
|
+
Brahmi_Joining_Number: 'Placeholder',
|
|
13
|
+
Cantillation_Mark: 'A',
|
|
14
|
+
Consonant: 'C',
|
|
15
|
+
Consonant_Dead: 'C',
|
|
16
|
+
Consonant_Final: 'CM',
|
|
17
|
+
Consonant_Head_Letter: 'C',
|
|
18
|
+
Consonant_Killer: 'M',
|
|
19
|
+
Consonant_Medial: 'CM',
|
|
20
|
+
Consonant_Placeholder: 'Placeholder',
|
|
21
|
+
Consonant_Preceding_Repha: 'Repha',
|
|
22
|
+
Consonant_Prefixed: 'X',
|
|
23
|
+
Consonant_Subjoined: 'CM',
|
|
24
|
+
Consonant_Succeeding_Repha: 'N',
|
|
25
|
+
Consonant_With_Stacker: 'Repha',
|
|
26
|
+
Gemination_Mark: 'SM',
|
|
27
|
+
Invisible_Stacker: 'Coeng',
|
|
28
|
+
Joiner: 'ZWJ',
|
|
29
|
+
Modifying_Letter: 'X',
|
|
30
|
+
Non_Joiner: 'ZWNJ',
|
|
31
|
+
Nukta: 'N',
|
|
32
|
+
Number: 'Placeholder',
|
|
33
|
+
Number_Joiner: 'Placeholder',
|
|
34
|
+
Pure_Killer: 'M',
|
|
35
|
+
Register_Shifter: 'RS',
|
|
36
|
+
Syllable_Modifier: 'M',
|
|
37
|
+
Tone_Letter: 'X',
|
|
38
|
+
Tone_Mark: 'N',
|
|
39
|
+
Virama: 'H',
|
|
40
|
+
Visarga: 'SM',
|
|
41
|
+
Vowel: 'V',
|
|
42
|
+
Vowel_Dependent: 'M',
|
|
43
|
+
Vowel_Independent: 'V'
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const OVERRIDES = {
|
|
47
|
+
0x0953: 'SM',
|
|
48
|
+
0x0954: 'SM',
|
|
49
|
+
0x0A72: 'C',
|
|
50
|
+
0x0A73: 'C',
|
|
51
|
+
0x1CF5: 'C',
|
|
52
|
+
0x1CF6: 'C',
|
|
53
|
+
0x1CE2: 'A',
|
|
54
|
+
0x1CE3: 'A',
|
|
55
|
+
0x1CE4: 'A',
|
|
56
|
+
0x1CE5: 'A',
|
|
57
|
+
0x1CE6: 'A',
|
|
58
|
+
0x1CE7: 'A',
|
|
59
|
+
0x1CE8: 'A',
|
|
60
|
+
0x1CED: 'A',
|
|
61
|
+
0xA8F2: 'Symbol',
|
|
62
|
+
0xA8F3: 'Symbol',
|
|
63
|
+
0xA8F4: 'Symbol',
|
|
64
|
+
0xA8F5: 'Symbol',
|
|
65
|
+
0xA8F6: 'Symbol',
|
|
66
|
+
0xA8F7: 'Symbol',
|
|
67
|
+
0x1CE9: 'Symbol',
|
|
68
|
+
0x1CEA: 'Symbol',
|
|
69
|
+
0x1CEB: 'Symbol',
|
|
70
|
+
0x1CEC: 'Symbol',
|
|
71
|
+
0x1CEE: 'Symbol',
|
|
72
|
+
0x1CEF: 'Symbol',
|
|
73
|
+
0x1CF0: 'Symbol',
|
|
74
|
+
0x1CF1: 'Symbol',
|
|
75
|
+
0x17C6: 'N',
|
|
76
|
+
0x2010: 'Placeholder',
|
|
77
|
+
0x2011: 'Placeholder',
|
|
78
|
+
0x25CC: 'Dotted_Circle',
|
|
79
|
+
|
|
80
|
+
// Ra
|
|
81
|
+
0x0930: 'Ra', // Devanagari
|
|
82
|
+
0x09B0: 'Ra', // Bengali
|
|
83
|
+
0x09F0: 'Ra', // Bengali
|
|
84
|
+
0x0A30: 'Ra', // Gurmukhi - No Reph
|
|
85
|
+
0x0AB0: 'Ra', // Gujarati
|
|
86
|
+
0x0B30: 'Ra', // Oriya
|
|
87
|
+
0x0BB0: 'Ra', // Tamil - No Reph
|
|
88
|
+
0x0C30: 'Ra', // Telugu - Reph formed only with ZWJ
|
|
89
|
+
0x0CB0: 'Ra', // Kannada
|
|
90
|
+
0x0D30: 'Ra', // Malayalam - No Reph, Logical Repha
|
|
91
|
+
0x0DBB: 'Ra', // Sinhala - Reph formed only with ZWJ
|
|
92
|
+
0x179A: 'Ra', // Khmer - No Reph, Visual Repha
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const POSITION_MAP = {
|
|
96
|
+
Left: 'Pre_C',
|
|
97
|
+
Top: 'Above_C',
|
|
98
|
+
Bottom: 'Below_C',
|
|
99
|
+
Right: 'Post_C',
|
|
100
|
+
|
|
101
|
+
// These should resolve to the position of the last part of the split sequence.
|
|
102
|
+
Bottom_And_Right: 'Post_C',
|
|
103
|
+
Left_And_Right: 'Post_C',
|
|
104
|
+
Top_And_Bottom: 'Below_C',
|
|
105
|
+
Top_And_Bottom_And_Right: 'Post_C',
|
|
106
|
+
Top_And_Left: 'Above_C',
|
|
107
|
+
Top_And_Left_And_Right: 'Post_C',
|
|
108
|
+
Top_And_Right: 'Post_C',
|
|
109
|
+
|
|
110
|
+
Overstruck: 'After_Main',
|
|
111
|
+
Visual_Order_Left: 'Pre_M'
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
function matraPosition(c, pos) {
|
|
115
|
+
switch (pos) {
|
|
116
|
+
case 'Pre_C':
|
|
117
|
+
return 'Pre_M';
|
|
118
|
+
|
|
119
|
+
case 'Post_C':
|
|
120
|
+
switch (c.block) {
|
|
121
|
+
case 'Devanagari': return 'After_Sub';
|
|
122
|
+
case 'Bengali': return 'After_Post';
|
|
123
|
+
case 'Gurmukhi': return 'After_Post';
|
|
124
|
+
case 'Gujarati': return 'After_Post';
|
|
125
|
+
case 'Oriya': return 'After_Post';
|
|
126
|
+
case 'Tamil': return 'After_Post';
|
|
127
|
+
case 'Telugu': return c.code <= 0x0C42 ? 'Before_Sub' : 'After_Sub';
|
|
128
|
+
case 'Kannada': return c.code < 0x0CC3 || c.code > 0xCD6 ? 'Before_Sub' : 'After_Sub';
|
|
129
|
+
case 'Malayalam': return 'After_Post';
|
|
130
|
+
case 'Sinhala': return 'After_Sub';
|
|
131
|
+
case 'Khmer': return 'After_Post';
|
|
132
|
+
default: return 'After_Sub';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
case 'Above_C':
|
|
136
|
+
switch (c.block) {
|
|
137
|
+
case 'Devanagari': return 'After_Sub';
|
|
138
|
+
case 'Gurmukhi': return 'After_Post'; // Deviate from spec
|
|
139
|
+
case 'Gujarati': return 'After_Sub';
|
|
140
|
+
case 'Oriya': return 'After_Main';
|
|
141
|
+
case 'Tamil': return 'After_Sub';
|
|
142
|
+
case 'Telugu': return 'Before_Sub';
|
|
143
|
+
case 'Kannada': return 'Before_Sub';
|
|
144
|
+
case 'Sinhala': return 'After_Sub';
|
|
145
|
+
case 'Khmer': return 'After_Post';
|
|
146
|
+
default: return 'After_Sub';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
case 'Below_C':
|
|
150
|
+
switch (c.block) {
|
|
151
|
+
case 'Devanagari': return 'After_Sub';
|
|
152
|
+
case 'Bengali': return 'After_Sub';
|
|
153
|
+
case 'Gurmukhi': return 'After_Post';
|
|
154
|
+
case 'Gujarati': return 'After_Post';
|
|
155
|
+
case 'Oriya': return 'After_Sub';
|
|
156
|
+
case 'Tamil': return 'After_Post';
|
|
157
|
+
case 'Telugu': return 'Before_Sub';
|
|
158
|
+
case 'Kannada': return 'Before_Sub';
|
|
159
|
+
case 'Malayalam': return 'After_Post';
|
|
160
|
+
case 'Sinhala': return 'After_Sub';
|
|
161
|
+
case 'Khmer': return 'After_Post';
|
|
162
|
+
default: return 'After_Sub';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
default:
|
|
166
|
+
return pos;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getPosition(codepoint, category) {
|
|
171
|
+
let position = POSITION_MAP[codepoint.indicPositionalCategory] || 'End';
|
|
172
|
+
|
|
173
|
+
if (CATEGORIES[category] & CONSONANT_FLAGS) {
|
|
174
|
+
position = 'Base_C';
|
|
175
|
+
} else if (category === 'M') {
|
|
176
|
+
position = matraPosition(codepoint, position);
|
|
177
|
+
} else if (category === 'SM' || category === 'VD' || category === 'A' || category === 'Symbol') {
|
|
178
|
+
position = 'SMVD';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Oriya Bindu is Before_Sub in the spec.
|
|
182
|
+
if (codepoint.code === 0x0B01) {
|
|
183
|
+
position = 'Before_Sub';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return Math.log2(POSITIONS[position]);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let symbols = {};
|
|
190
|
+
for (let c in CATEGORIES) {
|
|
191
|
+
symbols[c] = Math.log2(CATEGORIES[c]);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let trie = new UnicodeTrieBuilder;
|
|
195
|
+
for (let i = 0; i < codepoints.length; i++) {
|
|
196
|
+
let codepoint = codepoints[i];
|
|
197
|
+
if (codepoint) {
|
|
198
|
+
let category = OVERRIDES[codepoint.code] || CATEGORY_MAP[codepoint.indicSyllabicCategory] || 'X';
|
|
199
|
+
let position = getPosition(codepoint, category);
|
|
200
|
+
|
|
201
|
+
trie.set(codepoint.code, (symbols[category] << 8) | position);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
fs.writeFileSync(new URL('indic.trie', import.meta.url), trie.toBuffer());
|
|
206
|
+
|
|
207
|
+
let stateMachine = compile(fs.readFileSync(new URL('indic.machine', import.meta.url), 'utf8'), symbols);
|
|
208
|
+
fs.writeFileSync(new URL('indic.json', import.meta.url), JSON.stringify(stateMachine));
|