@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.
Files changed (163) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +274 -0
  3. package/dist/browser-module.mjs +13660 -0
  4. package/dist/browser-module.mjs.map +1 -0
  5. package/dist/browser.cjs +13689 -0
  6. package/dist/browser.cjs.map +1 -0
  7. package/dist/main.cjs +13712 -0
  8. package/dist/main.cjs.map +1 -0
  9. package/dist/module.mjs +13673 -0
  10. package/dist/module.mjs.map +1 -0
  11. package/package.json +99 -0
  12. package/src/CmapProcessor.js +285 -0
  13. package/src/DFont.js +113 -0
  14. package/src/TTFFont.js +621 -0
  15. package/src/TrueTypeCollection.js +65 -0
  16. package/src/WOFF2Font.js +222 -0
  17. package/src/WOFFFont.js +56 -0
  18. package/src/aat/AATFeatureMap.js +545 -0
  19. package/src/aat/AATLayoutEngine.js +48 -0
  20. package/src/aat/AATLookupTable.js +125 -0
  21. package/src/aat/AATMorxProcessor.js +429 -0
  22. package/src/aat/AATStateMachine.js +96 -0
  23. package/src/base.js +29 -0
  24. package/src/cff/CFFCharsets.js +99 -0
  25. package/src/cff/CFFDict.js +166 -0
  26. package/src/cff/CFFEncodings.js +48 -0
  27. package/src/cff/CFFFont.js +158 -0
  28. package/src/cff/CFFIndex.js +150 -0
  29. package/src/cff/CFFOperand.js +135 -0
  30. package/src/cff/CFFPointer.js +50 -0
  31. package/src/cff/CFFPrivateDict.js +39 -0
  32. package/src/cff/CFFStandardStrings.js +71 -0
  33. package/src/cff/CFFTop.js +236 -0
  34. package/src/decorators.js +36 -0
  35. package/src/encodings.js +219 -0
  36. package/src/fs.js +12 -0
  37. package/src/glyph/BBox.js +72 -0
  38. package/src/glyph/CBDTGlyph.js +9 -0
  39. package/src/glyph/CFFGlyph.js +635 -0
  40. package/src/glyph/COLRGlyph.js +108 -0
  41. package/src/glyph/Glyph.js +270 -0
  42. package/src/glyph/GlyphVariationProcessor.js +487 -0
  43. package/src/glyph/Path.js +257 -0
  44. package/src/glyph/SBIXGlyph.js +54 -0
  45. package/src/glyph/StandardNames.js +27 -0
  46. package/src/glyph/TTFGlyph.js +408 -0
  47. package/src/glyph/TTFGlyphEncoder.js +158 -0
  48. package/src/glyph/WOFF2Glyph.js +70 -0
  49. package/src/index.js +15 -0
  50. package/src/layout/GlyphPosition.js +30 -0
  51. package/src/layout/GlyphRun.js +108 -0
  52. package/src/layout/KernProcessor.js +97 -0
  53. package/src/layout/LayoutEngine.js +190 -0
  54. package/src/layout/Script.js +231 -0
  55. package/src/layout/UnicodeLayoutEngine.js +250 -0
  56. package/src/node.js +16 -0
  57. package/src/opentype/GPOSProcessor.js +311 -0
  58. package/src/opentype/GSUBProcessor.js +208 -0
  59. package/src/opentype/GlyphInfo.js +57 -0
  60. package/src/opentype/GlyphIterator.js +82 -0
  61. package/src/opentype/OTLayoutEngine.js +117 -0
  62. package/src/opentype/OTProcessor.js +472 -0
  63. package/src/opentype/ShapingPlan.js +121 -0
  64. package/src/opentype/shapers/ArabicShaper.js +124 -0
  65. package/src/opentype/shapers/DefaultShaper.js +116 -0
  66. package/src/opentype/shapers/HangulShaper.js +285 -0
  67. package/src/opentype/shapers/HebrewShaper.js +105 -0
  68. package/src/opentype/shapers/IndicShaper.js +914 -0
  69. package/src/opentype/shapers/ThaiShaper.js +238 -0
  70. package/src/opentype/shapers/UniversalShaper.js +186 -0
  71. package/src/opentype/shapers/data.trie +0 -0
  72. package/src/opentype/shapers/gen-indic.js +208 -0
  73. package/src/opentype/shapers/gen-use.js +287 -0
  74. package/src/opentype/shapers/generate-data.js +33 -0
  75. package/src/opentype/shapers/index.js +109 -0
  76. package/src/opentype/shapers/indic-data.js +175 -0
  77. package/src/opentype/shapers/indic.json +1 -0
  78. package/src/opentype/shapers/indic.machine +31 -0
  79. package/src/opentype/shapers/indic.trie +0 -0
  80. package/src/opentype/shapers/use.json +1 -0
  81. package/src/opentype/shapers/use.machine +44 -0
  82. package/src/opentype/shapers/use.trie +0 -0
  83. package/src/packages/unicode-properties/LICENSE +8 -0
  84. package/src/packages/unicode-properties/README.md +12 -0
  85. package/src/packages/unicode-properties/data-trie.js +1 -0
  86. package/src/packages/unicode-properties/data.js +1 -0
  87. package/src/packages/unicode-properties/data.json +1 -0
  88. package/src/packages/unicode-properties/data.trie +0 -0
  89. package/src/packages/unicode-properties/dist/index.cjs +257 -0
  90. package/src/packages/unicode-properties/dist/index.cjs.map +7 -0
  91. package/src/packages/unicode-properties/dist/index.mjs +234 -0
  92. package/src/packages/unicode-properties/dist/index.mjs.map +7 -0
  93. package/src/packages/unicode-properties/generate.js +107 -0
  94. package/src/packages/unicode-properties/index.js +184 -0
  95. package/src/packages/unicode-properties/package.json +50 -0
  96. package/src/packages/unicode-trie/LICENSE +8 -0
  97. package/src/packages/unicode-trie/README.md +13 -0
  98. package/src/packages/unicode-trie/builder.js +965 -0
  99. package/src/packages/unicode-trie/dist/builder.cjs +711 -0
  100. package/src/packages/unicode-trie/dist/builder.cjs.map +7 -0
  101. package/src/packages/unicode-trie/dist/builder.mjs +690 -0
  102. package/src/packages/unicode-trie/dist/builder.mjs.map +7 -0
  103. package/src/packages/unicode-trie/dist/index.cjs +112 -0
  104. package/src/packages/unicode-trie/dist/index.cjs.map +7 -0
  105. package/src/packages/unicode-trie/dist/index.mjs +91 -0
  106. package/src/packages/unicode-trie/dist/index.mjs.map +7 -0
  107. package/src/packages/unicode-trie/index.js +135 -0
  108. package/src/packages/unicode-trie/package.json +49 -0
  109. package/src/packages/unicode-trie/swap.js +21 -0
  110. package/src/subset/CFFSubset.js +176 -0
  111. package/src/subset/Subset.js +63 -0
  112. package/src/subset/TTFSubset.js +130 -0
  113. package/src/tables/BASE.js +78 -0
  114. package/src/tables/COLR.js +25 -0
  115. package/src/tables/CPAL.js +24 -0
  116. package/src/tables/DSIG.js +21 -0
  117. package/src/tables/EBDT.js +91 -0
  118. package/src/tables/EBLC.js +80 -0
  119. package/src/tables/GDEF.js +57 -0
  120. package/src/tables/GPOS.js +209 -0
  121. package/src/tables/GSUB.js +85 -0
  122. package/src/tables/HVAR.js +44 -0
  123. package/src/tables/JSTF.js +43 -0
  124. package/src/tables/LTSH.js +10 -0
  125. package/src/tables/OS2.js +84 -0
  126. package/src/tables/PCLT.js +21 -0
  127. package/src/tables/STAT.js +99 -0
  128. package/src/tables/VDMX.js +33 -0
  129. package/src/tables/VORG.js +14 -0
  130. package/src/tables/WOFF2Directory.js +73 -0
  131. package/src/tables/WOFFDirectory.js +37 -0
  132. package/src/tables/aat.js +157 -0
  133. package/src/tables/avar.js +19 -0
  134. package/src/tables/bsln.js +31 -0
  135. package/src/tables/cmap.js +127 -0
  136. package/src/tables/cvt.js +6 -0
  137. package/src/tables/directory.js +58 -0
  138. package/src/tables/feat.js +28 -0
  139. package/src/tables/fpgm.js +8 -0
  140. package/src/tables/fvar.js +31 -0
  141. package/src/tables/gasp.js +15 -0
  142. package/src/tables/glyf.js +4 -0
  143. package/src/tables/gvar.js +27 -0
  144. package/src/tables/hdmx.js +15 -0
  145. package/src/tables/head.js +25 -0
  146. package/src/tables/hhea.js +19 -0
  147. package/src/tables/hmtx.js +11 -0
  148. package/src/tables/index.js +119 -0
  149. package/src/tables/just.js +81 -0
  150. package/src/tables/kern.js +91 -0
  151. package/src/tables/loca.js +30 -0
  152. package/src/tables/maxp.js +20 -0
  153. package/src/tables/morx.js +79 -0
  154. package/src/tables/name.js +157 -0
  155. package/src/tables/opbd.js +15 -0
  156. package/src/tables/opentype.js +217 -0
  157. package/src/tables/post.js +34 -0
  158. package/src/tables/prep.js +6 -0
  159. package/src/tables/sbix.js +17 -0
  160. package/src/tables/variations.js +80 -0
  161. package/src/tables/vhea.js +19 -0
  162. package/src/tables/vmtx.js +12 -0
  163. package/src/utils.js +62 -0
@@ -0,0 +1,116 @@
1
+ import {isDigit, isMark} from '../../packages/unicode-properties/index.js';
2
+ import GlyphInfo from '../GlyphInfo';
3
+
4
+ const VARIATION_FEATURES = ['rvrn'];
5
+ const COMMON_FEATURES = ['ccmp', 'locl', 'rlig', 'mark', 'mkmk'];
6
+ const FRACTIONAL_FEATURES = ['frac', 'numr', 'dnom'];
7
+ const HORIZONTAL_FEATURES = ['calt', 'clig', 'liga', 'rclt', 'curs', 'kern'];
8
+ const _VERTICAL_FEATURES = ['vert'];
9
+ const DIRECTIONAL_FEATURES = {
10
+ ltr: ['ltra', 'ltrm'],
11
+ rtl: ['rtla', 'rtlm']
12
+ };
13
+
14
+ export default class DefaultShaper {
15
+ static zeroMarkWidths = 'AFTER_GPOS';
16
+ static plan(plan, glyphs, features) {
17
+ // Plan the features we want to apply
18
+ this.planPreprocessing(plan);
19
+ this.planFeatures(plan);
20
+ this.planPostprocessing(plan, features);
21
+
22
+ // Assign the global features to all the glyphs
23
+ plan.assignGlobalFeatures(glyphs);
24
+
25
+ // Assign local features to glyphs
26
+ this.assignFeatures(plan, glyphs);
27
+ }
28
+
29
+ static planPreprocessing(plan) {
30
+ plan.add({
31
+ global: [...VARIATION_FEATURES, ...DIRECTIONAL_FEATURES[plan.direction]],
32
+ local: FRACTIONAL_FEATURES
33
+ });
34
+ }
35
+
36
+ static planFeatures(_plan) {
37
+ // Do nothing by default. Let subclasses override this.
38
+ }
39
+
40
+ static planPostprocessing(plan, userFeatures) {
41
+ plan.add([...COMMON_FEATURES, ...HORIZONTAL_FEATURES]);
42
+ plan.setFeatureOverrides(userFeatures);
43
+ }
44
+
45
+ static assignFeatures(plan, glyphs) {
46
+ // Font-aware NFC before GSUB (HarfBuzz default-shaper behavior).
47
+ composeGlyphs(plan.font, glyphs);
48
+
49
+ // Enable contextual fractions
50
+ for (let i = 0; i < glyphs.length; i++) {
51
+ let glyph = glyphs[i];
52
+ if (glyph.codePoints[0] === 0x2044) { // fraction slash
53
+ let start = i;
54
+ let end = i + 1;
55
+
56
+ // Apply numerator
57
+ while (start > 0 && isDigit(glyphs[start - 1].codePoints[0])) {
58
+ glyphs[start - 1].features.numr = true;
59
+ glyphs[start - 1].features.frac = true;
60
+ start--;
61
+ }
62
+
63
+ // Apply denominator
64
+ while (end < glyphs.length && isDigit(glyphs[end].codePoints[0])) {
65
+ glyphs[end].features.dnom = true;
66
+ glyphs[end].features.frac = true;
67
+ end++;
68
+ }
69
+
70
+ // Apply fraction slash
71
+ glyph.features.frac = true;
72
+ i = end - 1;
73
+ }
74
+ }
75
+ }
76
+ }
77
+
78
+ // Compose base+mark clusters when the font has the precomposed glyph.
79
+ // Skip pure mark reorders (same length) so Arabic calt etc. keep expected order.
80
+ function composeGlyphs(font, glyphs) {
81
+ let singleMark = g => g.codePoints.length === 1 && isMark(g.codePoints[0]);
82
+
83
+ for (let i = 0; i < glyphs.length; ) {
84
+ let base = glyphs[i];
85
+ if (base.codePoints.length !== 1 || isMark(base.codePoints[0])) {
86
+ i++;
87
+ continue;
88
+ }
89
+
90
+ let end = i + 1;
91
+ while (end < glyphs.length && singleMark(glyphs[end])) end++;
92
+ if (end === i + 1) {
93
+ i++;
94
+ continue;
95
+ }
96
+
97
+ let input = glyphs.slice(i, end).map(g => g.codePoints[0]);
98
+ let composed = Array.from(String.fromCodePoint(...input).normalize('NFC')).flatMap(char => {
99
+ let cp = char.codePointAt(0);
100
+ return font.hasGlyphForCodePoint(cp)
101
+ ? [cp]
102
+ : Array.from(char.normalize('NFD'), c => c.codePointAt(0));
103
+ });
104
+
105
+ if (composed.length === input.length) {
106
+ i = end;
107
+ continue;
108
+ }
109
+
110
+ let replacement = composed.map(
111
+ cp => new GlyphInfo(font, font.glyphForCodePoint(cp).id, [cp], base.features)
112
+ );
113
+ glyphs.splice(i, end - i, ...replacement);
114
+ i += replacement.length;
115
+ }
116
+ }
@@ -0,0 +1,285 @@
1
+ import DefaultShaper from './DefaultShaper';
2
+ import GlyphInfo from '../GlyphInfo';
3
+
4
+ /**
5
+ * This is a shaper for the Hangul script, used by the Korean language.
6
+ * It does the following:
7
+ * - decompose if unsupported by the font:
8
+ * <LV> -> <L,V>
9
+ * <LVT> -> <L,V,T>
10
+ * <LV,T> -> <L,V,T>
11
+ *
12
+ * - compose if supported by the font:
13
+ * <L,V> -> <LV>
14
+ * <L,V,T> -> <LVT>
15
+ * <LV,T> -> <LVT>
16
+ *
17
+ * - reorder tone marks (S is any valid syllable):
18
+ * <S, M> -> <M, S>
19
+ *
20
+ * - apply ljmo, vjmo, and tjmo OpenType features to decomposed Jamo sequences.
21
+ *
22
+ * This logic is based on the following documents:
23
+ * - http://www.microsoft.com/typography/OpenTypeDev/hangul/intro.htm
24
+ * - http://ktug.org/~nomos/harfbuzz-hangul/hangulshaper.pdf
25
+ */
26
+ export default class HangulShaper extends DefaultShaper {
27
+ static zeroMarkWidths = 'NONE';
28
+ static planFeatures(plan) {
29
+ plan.add(['ljmo', 'vjmo', 'tjmo'], false);
30
+ }
31
+
32
+ static assignFeatures(plan, glyphs) {
33
+ let state = 0;
34
+ let i = 0;
35
+ while (i < glyphs.length) {
36
+ let action;
37
+ let glyph = glyphs[i];
38
+ let code = glyph.codePoints[0];
39
+ let type = getType(code);
40
+
41
+ [ action, state ] = STATE_TABLE[state][type];
42
+
43
+ switch (action) {
44
+ case DECOMPOSE:
45
+ // Decompose the composed syllable if it is not supported by the font.
46
+ if (!plan.font.hasGlyphForCodePoint(code)) {
47
+ i = decompose(glyphs, i, plan.font);
48
+ }
49
+ break;
50
+
51
+ case COMPOSE:
52
+ // Found a decomposed syllable. Try to compose if supported by the font.
53
+ i = compose(glyphs, i, plan.font);
54
+ break;
55
+
56
+ case TONE_MARK:
57
+ // Got a valid syllable, followed by a tone mark. Move the tone mark to the beginning of the syllable.
58
+ reorderToneMark(glyphs, i, plan.font);
59
+ break;
60
+
61
+ case INVALID:
62
+ // Tone mark has no valid syllable to attach to, so insert a dotted circle
63
+ i = insertDottedCircle(glyphs, i, plan.font);
64
+ break;
65
+ }
66
+
67
+ i++;
68
+ }
69
+ }
70
+ }
71
+
72
+ const HANGUL_BASE = 0xac00;
73
+ const HANGUL_END = 0xd7a4;
74
+ const HANGUL_COUNT = HANGUL_END - HANGUL_BASE + 1;
75
+ const L_BASE = 0x1100; // lead
76
+ const V_BASE = 0x1161; // vowel
77
+ const T_BASE = 0x11a7; // trail
78
+ const L_COUNT = 19;
79
+ const V_COUNT = 21;
80
+ const T_COUNT = 28;
81
+ const L_END = L_BASE + L_COUNT - 1;
82
+ const V_END = V_BASE + V_COUNT - 1;
83
+ const T_END = T_BASE + T_COUNT - 1;
84
+ const DOTTED_CIRCLE = 0x25cc;
85
+
86
+ const isL = code => 0x1100 <= code && code <= 0x115f || 0xa960 <= code && code <= 0xa97c;
87
+ const isV = code => 0x1160 <= code && code <= 0x11a7 || 0xd7b0 <= code && code <= 0xd7c6;
88
+ const isT = code => 0x11a8 <= code && code <= 0x11ff || 0xd7cb <= code && code <= 0xd7fb;
89
+ const isTone = code => 0x302e <= code && code <= 0x302f;
90
+ const isLVT = code => HANGUL_BASE <= code && code <= HANGUL_END;
91
+ const isLV = code => (code - HANGUL_BASE) < HANGUL_COUNT && (code - HANGUL_BASE) % T_COUNT === 0;
92
+ const isCombiningL = code => L_BASE <= code && code <= L_END;
93
+ const isCombiningV = code => V_BASE <= code && code <= V_END;
94
+ const isCombiningT = code => T_BASE + 1 && 1 <= code && code <= T_END;
95
+
96
+ // Character categories
97
+ const X = 0; // Other character
98
+ const L = 1; // Leading consonant
99
+ const V = 2; // Medial vowel
100
+ const T = 3; // Trailing consonant
101
+ const LV = 4; // Composed <LV> syllable
102
+ const LVT = 5; // Composed <LVT> syllable
103
+ const M = 6; // Tone mark
104
+
105
+ // This function classifies a character using the above categories.
106
+ function getType(code) {
107
+ if (isL(code)) { return L; }
108
+ if (isV(code)) { return V; }
109
+ if (isT(code)) { return T; }
110
+ if (isLV(code)) { return LV; }
111
+ if (isLVT(code)) { return LVT; }
112
+ if (isTone(code)) { return M; }
113
+ return X;
114
+ }
115
+
116
+ // State machine actions
117
+ const NO_ACTION = 0;
118
+ const DECOMPOSE = 1;
119
+ const COMPOSE = 2;
120
+ const TONE_MARK = 4;
121
+ const INVALID = 5;
122
+
123
+ // Build a state machine that accepts valid syllables, and applies actions along the way.
124
+ // The logic this is implementing is documented at the top of the file.
125
+ const STATE_TABLE = [
126
+ // X L V T LV LVT M
127
+ // State 0: start state
128
+ [ [ NO_ACTION, 0 ], [ NO_ACTION, 1 ], [ NO_ACTION, 0 ], [ NO_ACTION, 0 ], [ DECOMPOSE, 2 ], [ DECOMPOSE, 3 ], [ INVALID, 0 ] ],
129
+
130
+ // State 1: <L>
131
+ [ [ NO_ACTION, 0 ], [ NO_ACTION, 1 ], [ COMPOSE, 2 ], [ NO_ACTION, 0 ], [ DECOMPOSE, 2 ], [ DECOMPOSE, 3 ], [ INVALID, 0 ] ],
132
+
133
+ // State 2: <L,V> or <LV>
134
+ [ [ NO_ACTION, 0 ], [ NO_ACTION, 1 ], [ NO_ACTION, 0 ], [ COMPOSE, 3 ], [ DECOMPOSE, 2 ], [ DECOMPOSE, 3 ], [ TONE_MARK, 0 ] ],
135
+
136
+ // State 3: <L,V,T> or <LVT>
137
+ [ [ NO_ACTION, 0 ], [ NO_ACTION, 1 ], [ NO_ACTION, 0 ], [ NO_ACTION, 0 ], [ DECOMPOSE, 2 ], [ DECOMPOSE, 3 ], [ TONE_MARK, 0 ] ]
138
+ ];
139
+
140
+ function getGlyph(font, code, features) {
141
+ return new GlyphInfo(font, font.glyphForCodePoint(code).id, [code], features);
142
+ }
143
+
144
+ function decompose(glyphs, i, font) {
145
+ let glyph = glyphs[i];
146
+ let code = glyph.codePoints[0];
147
+
148
+ let s = code - HANGUL_BASE;
149
+ let t = T_BASE + s % T_COUNT;
150
+ s = s / T_COUNT | 0;
151
+ let l = L_BASE + s / V_COUNT | 0;
152
+ let v = V_BASE + s % V_COUNT;
153
+
154
+ // Don't decompose if all of the components are not available
155
+ if (!font.hasGlyphForCodePoint(l) ||
156
+ !font.hasGlyphForCodePoint(v) ||
157
+ (t !== T_BASE && !font.hasGlyphForCodePoint(t))) {
158
+ return i;
159
+ }
160
+
161
+ // Replace the current glyph with decomposed L, V, and T glyphs,
162
+ // and apply the proper OpenType features to each component.
163
+ let ljmo = getGlyph(font, l, glyph.features);
164
+ ljmo.features.ljmo = true;
165
+
166
+ let vjmo = getGlyph(font, v, glyph.features);
167
+ vjmo.features.vjmo = true;
168
+
169
+ let insert = [ ljmo, vjmo ];
170
+
171
+ if (t > T_BASE) {
172
+ let tjmo = getGlyph(font, t, glyph.features);
173
+ tjmo.features.tjmo = true;
174
+ insert.push(tjmo);
175
+ }
176
+
177
+ glyphs.splice(i, 1, ...insert);
178
+ return i + insert.length - 1;
179
+ }
180
+
181
+ function compose(glyphs, i, font) {
182
+ let glyph = glyphs[i];
183
+ let code = glyphs[i].codePoints[0];
184
+ let type = getType(code);
185
+
186
+ let prev = glyphs[i - 1].codePoints[0];
187
+ let prevType = getType(prev);
188
+
189
+ // Figure out what type of syllable we're dealing with
190
+ let lv, ljmo, vjmo, tjmo;
191
+ if (prevType === LV && type === T) {
192
+ // <LV,T>
193
+ lv = prev;
194
+ tjmo = glyph;
195
+ } else {
196
+ if (type === V) {
197
+ // <L,V>
198
+ ljmo = glyphs[i - 1];
199
+ vjmo = glyph;
200
+ } else {
201
+ // <L,V,T>
202
+ ljmo = glyphs[i - 2];
203
+ vjmo = glyphs[i - 1];
204
+ tjmo = glyph;
205
+ }
206
+
207
+ let l = ljmo.codePoints[0];
208
+ let v = vjmo.codePoints[0];
209
+
210
+ // Make sure L and V are combining characters
211
+ if (isCombiningL(l) && isCombiningV(v)) {
212
+ lv = HANGUL_BASE + ((l - L_BASE) * V_COUNT + (v - V_BASE)) * T_COUNT;
213
+ }
214
+ }
215
+
216
+ let t = (tjmo && tjmo.codePoints[0]) || T_BASE;
217
+ if ((lv != null) && (t === T_BASE || isCombiningT(t))) {
218
+ let s = lv + (t - T_BASE);
219
+
220
+ // Replace with a composed glyph if supported by the font,
221
+ // otherwise apply the proper OpenType features to each component.
222
+ if (font.hasGlyphForCodePoint(s)) {
223
+ let del = prevType === V ? 3 : 2;
224
+ glyphs.splice(i - del + 1, del, getGlyph(font, s, glyph.features));
225
+ return i - del + 1;
226
+ }
227
+ }
228
+
229
+ // Didn't compose (either a non-combining component or unsupported by font).
230
+ if (ljmo) { ljmo.features.ljmo = true; }
231
+ if (vjmo) { vjmo.features.vjmo = true; }
232
+ if (tjmo) { tjmo.features.tjmo = true; }
233
+
234
+ if (prevType === LV) {
235
+ // Sequence was originally <L,V>, which got combined earlier.
236
+ // Either the T was non-combining, or the LVT glyph wasn't supported.
237
+ // Decompose the glyph again and apply OT features.
238
+ decompose(glyphs, i - 1, font);
239
+ return i + 1;
240
+ }
241
+
242
+ return i;
243
+ }
244
+
245
+ function getLength(code) {
246
+ switch (getType(code)) {
247
+ case LV:
248
+ case LVT:
249
+ return 1;
250
+ case V:
251
+ return 2;
252
+ case T:
253
+ return 3;
254
+ }
255
+ }
256
+
257
+ function reorderToneMark(glyphs, i, font) {
258
+ let glyph = glyphs[i];
259
+ let code = glyphs[i].codePoints[0];
260
+
261
+ // Move tone mark to the beginning of the previous syllable, unless it is zero width
262
+ if (font.glyphForCodePoint(code).advanceWidth === 0) { return; }
263
+
264
+ let prev = glyphs[i - 1].codePoints[0];
265
+ let len = getLength(prev);
266
+
267
+ glyphs.splice(i, 1);
268
+ return glyphs.splice(i - len, 0, glyph);
269
+ }
270
+
271
+ function insertDottedCircle(glyphs, i, font) {
272
+ let glyph = glyphs[i];
273
+ let code = glyphs[i].codePoints[0];
274
+
275
+ if (font.hasGlyphForCodePoint(DOTTED_CIRCLE)) {
276
+ let dottedCircle = getGlyph(font, DOTTED_CIRCLE, glyph.features);
277
+
278
+ // If the tone mark is zero width, insert the dotted circle before, otherwise after
279
+ let idx = font.glyphForCodePoint(code).advanceWidth === 0 ? i : i + 1;
280
+ glyphs.splice(idx, 0, dottedCircle);
281
+ i++;
282
+ }
283
+
284
+ return i;
285
+ }
@@ -0,0 +1,105 @@
1
+ import {getCombiningClass} from '../../packages/unicode-properties/index.js';
2
+ import DefaultShaper from './DefaultShaper';
3
+ import GlyphInfo from '../GlyphInfo';
4
+
5
+ /**
6
+ * Hebrew shaper (HarfBuzz hb-ot-shaper-hebrew.cc):
7
+ * - Compose presentation forms (FBxx) when the font lacks GPOS mark for `hebr`
8
+ * - Reorder patah/qamats + sheva/hiriq + meteg/below so meteg sits next to the base
9
+ */
10
+ export default class HebrewShaper extends DefaultShaper {
11
+ static assignFeatures(plan, glyphs) {
12
+ super.assignFeatures(plan, glyphs);
13
+
14
+ if (!hasGposMark(plan.font)) {
15
+ composeHebrew(glyphs, plan.font);
16
+ }
17
+ reorderMarksHebrew(glyphs);
18
+ }
19
+ }
20
+
21
+ // Dagesh forms for U+05D0..U+05EA (0 = no precomposed form).
22
+ const DAGESH_FORMS = [
23
+ 0xFB30, 0xFB31, 0xFB32, 0xFB33, 0xFB34, 0xFB35, 0xFB36, 0,
24
+ 0xFB38, 0xFB39, 0xFB3A, 0xFB3B, 0xFB3C, 0, 0xFB3E, 0,
25
+ 0xFB40, 0xFB41, 0, 0xFB43, 0xFB44, 0, 0xFB46, 0xFB47,
26
+ 0xFB48, 0xFB49, 0xFB4A
27
+ ];
28
+
29
+ // Precomposed Hebrew presentation form for `a + b`, or null (HB compose_hebrew).
30
+ function composeHebrewPair(a, b) {
31
+ switch (b) {
32
+ case 0x05B4: // HIRIQ
33
+ return a === 0x05D9 ? 0xFB1D : null;
34
+ case 0x05B7: // PATAH
35
+ return a === 0x05F2 ? 0xFB1F : a === 0x05D0 ? 0xFB2E : null;
36
+ case 0x05B8: // QAMATS
37
+ return a === 0x05D0 ? 0xFB2F : null;
38
+ case 0x05B9: // HOLAM
39
+ return a === 0x05D5 ? 0xFB4B : null;
40
+ case 0x05BC: // DAGESH
41
+ if (a >= 0x05D0 && a <= 0x05EA) return DAGESH_FORMS[a - 0x05D0] || null;
42
+ if (a === 0xFB2A) return 0xFB2C;
43
+ if (a === 0xFB2B) return 0xFB2D;
44
+ return null;
45
+ case 0x05BF: // RAFE
46
+ return a === 0x05D1 ? 0xFB4C : a === 0x05DB ? 0xFB4D : a === 0x05E4 ? 0xFB4E : null;
47
+ case 0x05C1: // SHIN DOT
48
+ return a === 0x05E9 ? 0xFB2A : a === 0xFB49 ? 0xFB2C : null;
49
+ case 0x05C2: // SIN DOT
50
+ return a === 0x05E9 ? 0xFB2B : a === 0xFB49 ? 0xFB2D : null;
51
+ default:
52
+ return null;
53
+ }
54
+ }
55
+
56
+ // Greedily compose adjacent pairs when the font has the glyph
57
+ // (e.g. SHIN + SHIN_DOT → FB2A, then + DAGESH → FB2C).
58
+ function composeHebrew(glyphs, font) {
59
+ for (let i = 0; i + 1 < glyphs.length; ) {
60
+ let composed = glyphs[i].codePoints[0];
61
+ let consumed = 1;
62
+ while (i + consumed < glyphs.length) {
63
+ let ab = composeHebrewPair(composed, glyphs[i + consumed].codePoints[0]);
64
+ if (ab == null || !font.hasGlyphForCodePoint(ab)) break;
65
+ composed = ab;
66
+ consumed++;
67
+ }
68
+ if (consumed > 1) {
69
+ let cps = [];
70
+ for (let j = 0; j < consumed; j++) cps.push(...glyphs[i + j].codePoints);
71
+ glyphs[i] = new GlyphInfo(font, font.glyphForCodePoint(composed).id, cps, glyphs[i].features);
72
+ glyphs.splice(i + 1, consumed - 1);
73
+ }
74
+ i++;
75
+ }
76
+ }
77
+
78
+ // HB reorder_marks_hebrew: [patah/qamats, sheva/hiriq, meteg/below] → swap last two.
79
+ // unicode-properties already exposes Hebrew modified CCCs (CCC10/14/17/18/22).
80
+ function reorderMarksHebrew(glyphs) {
81
+ for (let i = 2; i < glyphs.length; i++) {
82
+ let c0 = getCombiningClass(glyphs[i - 2].codePoints[0]);
83
+ let c1 = getCombiningClass(glyphs[i - 1].codePoints[0]);
84
+ let c2 = getCombiningClass(glyphs[i].codePoints[0]);
85
+ if (
86
+ (c0 === 'CCC17' || c0 === 'CCC18') &&
87
+ (c1 === 'CCC10' || c1 === 'CCC14') &&
88
+ (c2 === 'CCC22' || c2 === 'Below')
89
+ ) {
90
+ [glyphs[i - 1], glyphs[i]] = [glyphs[i], glyphs[i - 1]];
91
+ break;
92
+ }
93
+ }
94
+ }
95
+
96
+ // Gate fallback composition on Hebrew-script GPOS `mark` (not the global featureList).
97
+ function hasGposMark(font) {
98
+ let hebr = font.GPOS?.scriptList?.find(e => e.tag === 'hebr')?.script;
99
+ if (!hebr) return false;
100
+
101
+ let langs = [hebr.defaultLangSys, ...(hebr.langSysRecords || []).map(l => l.langSys)].filter(Boolean);
102
+ return langs.some(ls =>
103
+ (ls.featureIndexes || []).some(i => font.GPOS.featureList[i]?.tag === 'mark')
104
+ );
105
+ }