@cantoo/fontkit 2.0.5 → 2.0.7

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 (153) hide show
  1. package/dist/browser-module.d.ts +239 -0
  2. package/dist/browser-module.mjs +4423 -1117
  3. package/dist/browser-module.mjs.map +1 -1
  4. package/dist/browser.cjs +4424 -1117
  5. package/dist/browser.cjs.map +1 -1
  6. package/dist/browser.d.ts +239 -0
  7. package/dist/fontkit.umd.d.ts +239 -0
  8. package/dist/fontkit.umd.js +22076 -0
  9. package/dist/fontkit.umd.js.map +7 -0
  10. package/dist/index.d.ts +239 -0
  11. package/dist/main.cjs +4424 -1117
  12. package/dist/main.cjs.map +1 -1
  13. package/dist/main.d.ts +239 -0
  14. package/dist/module.d.ts +239 -0
  15. package/dist/module.mjs +4423 -1117
  16. package/dist/module.mjs.map +1 -1
  17. package/package.json +22 -5
  18. package/src/CmapProcessor.js +59 -20
  19. package/src/DFont.js +114 -14
  20. package/src/TTFFont.js +259 -57
  21. package/src/TrueTypeCollection.js +54 -10
  22. package/src/WOFF2Font.js +128 -16
  23. package/src/WOFFFont.js +45 -8
  24. package/src/aat/AATFeatureMap.js +62 -18
  25. package/src/aat/AATLayoutEngine.js +33 -1
  26. package/src/aat/AATLookupTable.js +22 -4
  27. package/src/aat/AATMorxProcessor.js +189 -52
  28. package/src/aat/AATStateMachine.js +32 -0
  29. package/src/base.js +19 -0
  30. package/src/cff/CFFCharsets.js +3 -0
  31. package/src/cff/CFFDict.js +79 -9
  32. package/src/cff/CFFEncodings.js +2 -0
  33. package/src/cff/CFFFont.js +119 -23
  34. package/src/cff/CFFIndex.js +62 -5
  35. package/src/cff/CFFOperand.js +52 -17
  36. package/src/cff/CFFPointer.js +41 -2
  37. package/src/cff/CFFPrivateDict.js +14 -4
  38. package/src/cff/CFFStandardStrings.js +1 -0
  39. package/src/cff/CFFTop.js +103 -10
  40. package/src/decorators.js +32 -13
  41. package/src/encodings.js +22 -5
  42. package/src/fs.js +10 -0
  43. package/src/glyph/BBox.js +14 -0
  44. package/src/glyph/CFFGlyph.js +290 -148
  45. package/src/glyph/COLRGlyph.js +38 -3
  46. package/src/glyph/Glyph.js +68 -9
  47. package/src/glyph/GlyphVariationProcessor.js +135 -34
  48. package/src/glyph/Path.js +107 -20
  49. package/src/glyph/SBIXGlyph.js +57 -8
  50. package/src/glyph/StandardNames.js +1 -0
  51. package/src/glyph/TTFGlyph.js +176 -29
  52. package/src/glyph/TTFGlyphEncoder.js +32 -0
  53. package/src/glyph/WOFF2Glyph.js +23 -4
  54. package/src/layout/GlyphPosition.js +6 -0
  55. package/src/layout/GlyphRun.js +25 -7
  56. package/src/layout/KernProcessor.js +62 -3
  57. package/src/layout/LayoutEngine.js +86 -12
  58. package/src/layout/Script.js +27 -1
  59. package/src/layout/UnicodeLayoutEngine.js +29 -4
  60. package/src/opentype/GPOSProcessor.js +156 -45
  61. package/src/opentype/GSUBProcessor.js +92 -33
  62. package/src/opentype/GlyphInfo.js +52 -4
  63. package/src/opentype/GlyphIterator.js +51 -2
  64. package/src/opentype/OTLayoutEngine.js +66 -8
  65. package/src/opentype/OTProcessor.js +283 -70
  66. package/src/opentype/ShapingPlan.js +51 -3
  67. package/src/opentype/shapers/ArabicShaper.js +19 -0
  68. package/src/opentype/shapers/DefaultShaper.js +51 -7
  69. package/src/opentype/shapers/HangulShaper.js +85 -7
  70. package/src/opentype/shapers/HebrewShaper.js +45 -11
  71. package/src/opentype/shapers/IndicShaper.js +141 -26
  72. package/src/opentype/shapers/ThaiShaper.js +35 -4
  73. package/src/opentype/shapers/UniversalShaper.js +83 -17
  74. package/src/opentype/shapers/index.js +15 -2
  75. package/src/packages/unicode-properties/data-trie.js +1 -0
  76. package/src/packages/unicode-properties/data.js +12 -0
  77. package/src/packages/unicode-properties/dist/index.cjs +22 -9
  78. package/src/packages/unicode-properties/dist/index.cjs.map +2 -2
  79. package/src/packages/unicode-properties/dist/index.mjs +21 -9
  80. package/src/packages/unicode-properties/dist/index.mjs.map +2 -2
  81. package/src/packages/unicode-properties/index.js +65 -0
  82. package/src/packages/unicode-properties/package.json +1 -1
  83. package/src/packages/unicode-trie/builder.js +128 -7
  84. package/src/packages/unicode-trie/dist/builder.cjs +129 -16
  85. package/src/packages/unicode-trie/dist/builder.cjs.map +2 -2
  86. package/src/packages/unicode-trie/dist/builder.mjs +128 -16
  87. package/src/packages/unicode-trie/dist/builder.mjs.map +2 -2
  88. package/src/packages/unicode-trie/dist/index.cjs +22 -9
  89. package/src/packages/unicode-trie/dist/index.cjs.map +2 -2
  90. package/src/packages/unicode-trie/dist/index.mjs +21 -9
  91. package/src/packages/unicode-trie/dist/index.mjs.map +2 -2
  92. package/src/packages/unicode-trie/index.js +62 -10
  93. package/src/packages/unicode-trie/package.json +1 -1
  94. package/src/packages/unicode-trie/swap.js +13 -0
  95. package/src/subset/CFFSubset.js +112 -22
  96. package/src/subset/Subset.js +21 -1
  97. package/src/subset/TTFSubset.js +79 -36
  98. package/src/tables/BASE.js +1 -0
  99. package/src/tables/COLR.js +1 -0
  100. package/src/tables/CPAL.js +1 -0
  101. package/src/tables/DSIG.js +1 -0
  102. package/src/tables/EBDT.js +10 -3
  103. package/src/tables/EBLC.js +67 -7
  104. package/src/tables/GDEF.js +1 -0
  105. package/src/tables/GPOS.js +79 -16
  106. package/src/tables/GSUB.js +14 -3
  107. package/src/tables/HVAR.js +51 -5
  108. package/src/tables/JSTF.js +3 -2
  109. package/src/tables/LTSH.js +1 -0
  110. package/src/tables/OS2.js +1 -0
  111. package/src/tables/PCLT.js +1 -0
  112. package/src/tables/STAT.js +46 -6
  113. package/src/tables/VDMX.js +1 -0
  114. package/src/tables/VORG.js +1 -0
  115. package/src/tables/WOFF2Directory.js +56 -8
  116. package/src/tables/WOFFDirectory.js +14 -2
  117. package/src/tables/aat.js +160 -35
  118. package/src/tables/avar.js +1 -0
  119. package/src/tables/bsln.js +3 -2
  120. package/src/tables/cmap.js +26 -6
  121. package/src/tables/cvt.js +1 -0
  122. package/src/tables/directory.js +44 -16
  123. package/src/tables/feat.js +24 -2
  124. package/src/tables/fpgm.js +1 -0
  125. package/src/tables/fvar.js +32 -4
  126. package/src/tables/gasp.js +1 -0
  127. package/src/tables/glyf.js +1 -0
  128. package/src/tables/gvar.js +29 -2
  129. package/src/tables/hdmx.js +12 -1
  130. package/src/tables/head.js +1 -0
  131. package/src/tables/hhea.js +1 -0
  132. package/src/tables/hmtx.js +18 -2
  133. package/src/tables/index.js +1 -0
  134. package/src/tables/just.js +15 -4
  135. package/src/tables/kern.js +43 -6
  136. package/src/tables/loca.js +21 -8
  137. package/src/tables/maxp.js +1 -0
  138. package/src/tables/morx.js +14 -7
  139. package/src/tables/name.js +104 -23
  140. package/src/tables/opbd.js +2 -1
  141. package/src/tables/opentype.js +39 -4
  142. package/src/tables/post.js +8 -1
  143. package/src/tables/prep.js +1 -0
  144. package/src/tables/sbix.js +12 -1
  145. package/src/tables/variations.js +23 -3
  146. package/src/tables/vhea.js +1 -0
  147. package/src/tables/vmtx.js +18 -2
  148. package/src/utils.js +15 -0
  149. package/types/assets.d.ts +13 -0
  150. package/types/deps.d.ts +83 -0
  151. package/types/fontkit.d.ts +1324 -0
  152. package/types/index.d.ts +239 -0
  153. package/types/restructure.d.ts +354 -0
@@ -17,9 +17,24 @@ import {
17
17
  import { decodeBase64 } from '../../utils';
18
18
  import indicTrie from './indic.trie';
19
19
 
20
- const { decompositions } = useData;
20
+ /** @typedef {import('../../../types/fontkit').ShapingPlanLike} ShapingPlanLike */
21
+ /** @typedef {import('../../../types/fontkit').GlyphInfoLike} GlyphInfoLike */
22
+ /** @typedef {import('../../../types/fontkit').IndicGlyphInfo} IndicGlyphInfo */
23
+ /** @typedef {import('../../../types/fontkit').LayoutFont} LayoutFont */
24
+ /** @typedef {import('../../../types/fontkit').IndicConfig} IndicConfig */
25
+ /** @typedef {import('../../../types/fontkit').IndicShaperInfo} IndicShaperInfo */
26
+ /** @typedef {import('../../../types/fontkit').ShapingStageFn} ShapingStageFn */
27
+ /** @typedef {import('dfa').StateMachineDefinition} StateMachineDefinition */
28
+
29
+ /**
30
+ * @typedef {{ decompositions: Record<string, number[]> }} UseDataPartial
31
+ */
32
+
33
+ /** @type {UseDataPartial} */
34
+ const useDataTyped = /** @type {UseDataPartial} */ (useData);
35
+ const { decompositions } = useDataTyped;
21
36
  const trie = new UnicodeTrie(decodeBase64(indicTrie));
22
- const stateMachine = new StateMachine(indicMachine);
37
+ const stateMachine = new StateMachine(/** @type {StateMachineDefinition} */ (indicMachine));
23
38
 
24
39
  /**
25
40
  * The IndicShaper supports indic scripts e.g. Devanagari, Kannada, etc.
@@ -27,6 +42,10 @@ const stateMachine = new StateMachine(indicMachine);
27
42
  */
28
43
  export default class IndicShaper extends DefaultShaper {
29
44
  static zeroMarkWidths = 'NONE';
45
+
46
+ /**
47
+ * @param {ShapingPlanLike} plan
48
+ */
30
49
  static planFeatures(plan) {
31
50
  plan.addStage(setupSyllables);
32
51
 
@@ -55,24 +74,39 @@ export default class IndicShaper extends DefaultShaper {
55
74
  });
56
75
 
57
76
  // Setup the indic config for the selected script
58
- plan.unicodeScript = Script.fromOpenType(plan.script);
59
- plan.indicConfig = INDIC_CONFIGS[plan.unicodeScript] || INDIC_CONFIGS.Default;
60
- plan.isOldSpec = plan.indicConfig.hasOldSpec && plan.script[plan.script.length - 1] !== '2';
77
+ let scriptTag = Array.isArray(plan.script) ? plan.script[0] : plan.script;
78
+ plan.unicodeScript = scriptTag != null ? Script.fromOpenType(scriptTag) : undefined;
79
+ /** @type {Record<string, IndicConfig>} */
80
+ let configs = INDIC_CONFIGS;
81
+ plan.indicConfig = (plan.unicodeScript && configs[plan.unicodeScript]) || configs.Default;
82
+ let scriptKey = Array.isArray(plan.script)
83
+ ? plan.script[plan.script.length - 1]
84
+ : plan.script;
85
+ plan.isOldSpec = !!(plan.indicConfig.hasOldSpec && scriptKey != null && scriptKey[scriptKey.length - 1] !== '2');
61
86
 
62
87
  // TODO: turn off kern (Khmer) and liga features.
63
88
  }
64
89
 
90
+ /**
91
+ * @param {ShapingPlanLike} plan
92
+ * @param {GlyphInfoLike[]} glyphs
93
+ */
65
94
  static assignFeatures(plan, glyphs) {
66
95
  // Decompose split matras
67
96
  // TODO: do this in a more general unicode normalizer
68
97
  for (let i = glyphs.length - 1; i >= 0; i--) {
69
98
  let codepoint = glyphs[i].codePoints[0];
70
- let d = INDIC_DECOMPOSITIONS[codepoint] || decompositions[codepoint];
99
+ let d = /** @type {Record<number, number[]>} */ (INDIC_DECOMPOSITIONS)[codepoint]
100
+ || decompositions[String(codepoint)];
71
101
  if (d) {
72
- let decomposed = d.map((c) => {
102
+ /** @type {GlyphInfo[]} */
103
+ let decomposed = [];
104
+ for (let c of d) {
73
105
  let g = plan.font.glyphForCodePoint(c);
74
- return new GlyphInfo(plan.font, g.id, [c], glyphs[i].features);
75
- });
106
+ if (g) {
107
+ decomposed.push(new GlyphInfo(plan.font, g.id, [c], glyphs[i].features));
108
+ }
109
+ }
76
110
 
77
111
  glyphs.splice(i, 1, ...decomposed);
78
112
  }
@@ -80,23 +114,63 @@ export default class IndicShaper extends DefaultShaper {
80
114
  }
81
115
  }
82
116
 
117
+ /**
118
+ * @param {GlyphInfoLike} glyph
119
+ * @returns {number}
120
+ */
83
121
  function indicCategory(glyph) {
84
122
  return trie.get(glyph.codePoints[0]) >> 8;
85
123
  }
86
124
 
125
+ /**
126
+ * @param {GlyphInfoLike} glyph
127
+ * @returns {number}
128
+ */
87
129
  function indicPosition(glyph) {
88
130
  return 1 << (trie.get(glyph.codePoints[0]) & 0xff);
89
131
  }
90
132
 
91
133
  class IndicInfo {
134
+ /**
135
+ * @param {number} category
136
+ * @param {number} position
137
+ * @param {string} syllableType
138
+ * @param {number} syllable
139
+ */
92
140
  constructor(category, position, syllableType, syllable) {
141
+ /** @type {number} */
93
142
  this.category = category;
143
+ /** @type {number} */
94
144
  this.position = position;
145
+ /** @type {string} */
95
146
  this.syllableType = syllableType;
147
+ /** @type {number} */
96
148
  this.syllable = syllable;
97
149
  }
98
150
  }
99
151
 
152
+ /**
153
+ * @param {import('../../../types/fontkit').IndicShaperInfo | import('../../../types/fontkit').USEShaperInfo | null} info
154
+ * @returns {info is IndicShaperInfo}
155
+ */
156
+ function isIndicShaperInfo(info) {
157
+ // Discriminate on category kind: Indic uses numeric bitflags, USE uses strings.
158
+ return info != null && typeof info.category === 'number';
159
+ }
160
+
161
+ /**
162
+ * @param {GlyphInfoLike} glyph
163
+ * @returns {IndicShaperInfo}
164
+ */
165
+ function indicInfo(glyph) {
166
+ let info = glyph.shaperInfo;
167
+ if (!isIndicShaperInfo(info)) {
168
+ throw new Error('Expected Indic shaperInfo');
169
+ }
170
+ return info;
171
+ }
172
+
173
+ /** @type {ShapingStageFn} */
100
174
  function setupSyllables(font, glyphs) {
101
175
  let syllable = 0;
102
176
  let last = 0;
@@ -131,29 +205,55 @@ function setupSyllables(font, glyphs) {
131
205
  }
132
206
  }
133
207
 
208
+ /**
209
+ * @param {GlyphInfoLike} glyph
210
+ * @returns {number}
211
+ */
134
212
  function isConsonant(glyph) {
135
- return glyph.shaperInfo.category & CONSONANT_FLAGS;
213
+ return indicInfo(glyph).category & CONSONANT_FLAGS;
136
214
  }
137
215
 
216
+ /**
217
+ * @param {GlyphInfoLike} glyph
218
+ * @returns {number}
219
+ */
138
220
  function isJoiner(glyph) {
139
- return glyph.shaperInfo.category & JOINER_FLAGS;
221
+ return indicInfo(glyph).category & JOINER_FLAGS;
140
222
  }
141
223
 
224
+ /**
225
+ * @param {GlyphInfoLike} glyph
226
+ * @returns {number}
227
+ */
142
228
  function isHalantOrCoeng(glyph) {
143
- return glyph.shaperInfo.category & HALANT_OR_COENG_FLAGS;
229
+ return indicInfo(glyph).category & HALANT_OR_COENG_FLAGS;
144
230
  }
145
231
 
232
+ /**
233
+ * @param {GlyphInfoLike[]} glyphs
234
+ * @param {string} feature
235
+ * @returns {boolean}
236
+ */
146
237
  function wouldSubstitute(glyphs, feature) {
147
238
  for (let glyph of glyphs) {
148
239
  glyph.features = { [feature]: true };
149
240
  }
150
241
 
151
- let GSUB = glyphs[0]._font._layoutEngine.engine.GSUBProcessor;
152
- GSUB.applyFeatures([feature], glyphs);
242
+ let engine = glyphs[0]._font._layoutEngine.engine;
243
+ if (!engine?.GSUBProcessor) {
244
+ return false;
245
+ }
246
+ engine.GSUBProcessor.applyFeatures([feature], glyphs);
153
247
 
154
248
  return glyphs.length === 1;
155
249
  }
156
250
 
251
+ /**
252
+ * @param {LayoutFont} font
253
+ * @param {GlyphInfoLike} consonant
254
+ * @param {GlyphInfoLike} virama
255
+ * @returns {number}
256
+ */
157
257
  function consonantPosition(font, consonant, virama) {
158
258
  let glyphs = [virama, consonant, virama];
159
259
  if (wouldSubstitute(glyphs.slice(0, 2), 'blwf') || wouldSubstitute(glyphs.slice(1, 3), 'blwf')) {
@@ -167,12 +267,18 @@ function consonantPosition(font, consonant, virama) {
167
267
  return POSITIONS.Base_C;
168
268
  }
169
269
 
170
- function initialReordering(font, glyphs, plan) {
171
- let indicConfig = plan.indicConfig;
172
- let features = font._layoutEngine.engine.GSUBProcessor.features;
173
-
174
- let dottedCircle = font.glyphForCodePoint(0x25cc).id;
175
- let virama = font.glyphForCodePoint(indicConfig.virama).id;
270
+ /** @type {ShapingStageFn} */
271
+ function initialReordering(font, glyphsArg, plan) {
272
+ // setupSyllables always attaches IndicShaperInfo before this stage.
273
+ const glyphs = /** @type {IndicGlyphInfo[]} */ (glyphsArg);
274
+ let indicConfig = /** @type {IndicConfig} */ (plan.indicConfig);
275
+ let engine = font._layoutEngine.engine;
276
+ let features = engine?.GSUBProcessor ? engine.GSUBProcessor.features : {};
277
+
278
+ let dottedCircleGlyph = font.glyphForCodePoint(0x25cc);
279
+ let dottedCircle = dottedCircleGlyph ? dottedCircleGlyph.id : 0;
280
+ let viramaGlyph = font.glyphForCodePoint(indicConfig.virama);
281
+ let virama = viramaGlyph ? viramaGlyph.id : 0;
176
282
  if (virama) {
177
283
  let info = new GlyphInfo(font, virama, [indicConfig.virama]);
178
284
  for (let i = 0; i < glyphs.length; i++) {
@@ -190,7 +296,7 @@ function initialReordering(font, glyphs, plan) {
190
296
  }
191
297
 
192
298
  if (syllableType === 'broken_cluster' && dottedCircle) {
193
- let g = new GlyphInfo(font, dottedCircle, [0x25cc]);
299
+ let g = /** @type {IndicGlyphInfo} */ (new GlyphInfo(font, dottedCircle, [0x25cc]));
194
300
  g.shaperInfo = new IndicInfo(
195
301
  1 << indicCategory(g),
196
302
  indicPosition(g),
@@ -569,9 +675,13 @@ function initialReordering(font, glyphs, plan) {
569
675
  }
570
676
  }
571
677
 
572
- function finalReordering(font, glyphs, plan) {
573
- let indicConfig = plan.indicConfig;
574
- let features = font._layoutEngine.engine.GSUBProcessor.features;
678
+ /** @type {ShapingStageFn} */
679
+ function finalReordering(font, glyphsArg, plan) {
680
+ // setupSyllables / initialReordering always attach IndicShaperInfo before this stage.
681
+ const glyphs = /** @type {IndicGlyphInfo[]} */ (glyphsArg);
682
+ let indicConfig = /** @type {IndicConfig} */ (plan.indicConfig);
683
+ let engine = font._layoutEngine.engine;
684
+ let features = engine?.GSUBProcessor ? engine.GSUBProcessor.features : {};
575
685
 
576
686
  for (let start = 0, end = nextSyllable(glyphs, 0); start < glyphs.length; start = end, end = nextSyllable(glyphs, start)) {
577
687
  // 4. Final reordering:
@@ -597,7 +707,7 @@ function finalReordering(font, glyphs, plan) {
597
707
  while (base < end && isHalantOrCoeng(glyphs[base])) {
598
708
  base++;
599
709
  }
600
- glyphs[base].shaperInfo.position = POSITIONS.BASE_C;
710
+ glyphs[base].shaperInfo.position = POSITIONS.Base_C;
601
711
  tryPref = false;
602
712
  }
603
713
  break;
@@ -719,7 +829,7 @@ function finalReordering(font, glyphs, plan) {
719
829
  && glyphs[start].shaperInfo.position === POSITIONS.Ra_To_Become_Reph
720
830
  && (glyphs[start].shaperInfo.category === CATEGORIES.Repha) !== (glyphs[start].isLigated && !glyphs[start].isMultiplied)
721
831
  ) {
722
- let newRephPos;
832
+ let newRephPos = end - 1;
723
833
  let rephPos = indicConfig.rephPos;
724
834
  let found = false;
725
835
 
@@ -906,6 +1016,11 @@ function finalReordering(font, glyphs, plan) {
906
1016
  }
907
1017
  }
908
1018
 
1019
+ /**
1020
+ * @param {IndicGlyphInfo[]} glyphs
1021
+ * @param {number} start
1022
+ * @returns {number}
1023
+ */
909
1024
  function nextSyllable(glyphs, start) {
910
1025
  if (start >= glyphs.length) return start;
911
1026
  let syllable = glyphs[start].shaperInfo.syllable;
@@ -1,12 +1,24 @@
1
1
  import DefaultShaper from './DefaultShaper';
2
2
  import GlyphInfo from '../GlyphInfo';
3
3
 
4
+ /** @typedef {import('../../../types/fontkit').ShapingPlanLike} ShapingPlanLike */
5
+ /** @typedef {import('../../../types/fontkit').GlyphInfoLike} GlyphInfoLike */
6
+ /** @typedef {import('../../../types/fontkit').LayoutFont} LayoutFont */
7
+ /** @typedef {import('../../../types/fontkit').FeatureMap} FeatureMap */
8
+ /** @typedef {import('../../../types/fontkit').FeatureInput} FeatureInput */
9
+ /** @typedef {import('../../../types/fontkit').ScriptTag} ScriptTag */
10
+ /** @typedef {import('../../../types/fontkit').OTScriptRecord} OTScriptRecord */
11
+
4
12
  /**
5
13
  * Thai / Lao shaper (HarfBuzz hb-ot-shaper-thai.cc):
6
14
  * 1. Decompose SARA AM → NIKHAHIT + SARA AA and reorder NIKHAHIT past above marks
7
15
  * 2. PUA tone/vowel shift fallback for legacy fonts without Thai GSUB
8
16
  */
9
17
  export default class ThaiShaper extends DefaultShaper {
18
+ /**
19
+ * @param {ShapingPlanLike} plan
20
+ * @param {GlyphInfoLike[]} glyphs
21
+ */
10
22
  static assignFeatures(plan, glyphs) {
11
23
  super.assignFeatures(plan, glyphs);
12
24
  preprocessThai(glyphs, plan.font);
@@ -18,19 +30,23 @@ export default class ThaiShaper extends DefaultShaper {
18
30
  }
19
31
 
20
32
  // Thai/Lao SARA AM differ only by the 0x80 bit (U+0E33 / U+0EB3).
33
+ /** @param {number} u @returns {boolean} */
21
34
  function isSaraAm(u) {
22
35
  return (u & ~0x0080) === 0x0E33;
23
36
  }
24
37
 
38
+ /** @param {number} u @returns {number} */
25
39
  function nikhahitFromSaraAm(u) {
26
40
  return u - 0x0E33 + 0x0E4D;
27
41
  }
28
42
 
43
+ /** @param {number} u @returns {number} */
29
44
  function saraAaFromSaraAm(u) {
30
45
  return u - 1;
31
46
  }
32
47
 
33
48
  // Above-base marks (Thai; Lao is the same set with +0x80).
49
+ /** @param {number} u @returns {boolean} */
34
50
  function isAboveBaseMark(u) {
35
51
  const c = u & ~0x0080;
36
52
  return c === 0x0E31
@@ -39,6 +55,7 @@ function isAboveBaseMark(u) {
39
55
  || c === 0x0E3B;
40
56
  }
41
57
 
58
+ /** @param {GlyphInfoLike[]} glyphs @param {LayoutFont} font */
42
59
  function preprocessThai(glyphs, font) {
43
60
  let i = 0;
44
61
  while (i < glyphs.length) {
@@ -68,9 +85,11 @@ function preprocessThai(glyphs, font) {
68
85
  }
69
86
  }
70
87
 
88
+ /** @param {LayoutFont} font @param {number} codePoint @param {FeatureMap | FeatureInput | null | undefined} features @returns {GlyphInfo} */
71
89
  function makeGlyph(font, codePoint, features) {
72
- const id = font.glyphForCodePoint(codePoint).id;
73
- return new GlyphInfo(font, id, [codePoint], features);
90
+ const g = font.glyphForCodePoint(codePoint);
91
+ // .notdef when the mapped code point is somehow missing from the font.
92
+ return new GlyphInfo(font, g ? g.id : 0, [codePoint], features);
74
93
  }
75
94
 
76
95
  // PUA fallback: above/below state machines remap marks (and some bases) to
@@ -92,6 +111,7 @@ const BV = 1; // below-base vowel/mark
92
111
  const T = 2; // tone mark
93
112
  const NOT_MARK = 3;
94
113
 
114
+ /** @param {number} u @returns {number} */
95
115
  function getConsonantType(u) {
96
116
  if (u === 0x0E1B || u === 0x0E1D || u === 0x0E1F) return AC;
97
117
  if (u === 0x0E0D || u === 0x0E10) return RC;
@@ -100,6 +120,7 @@ function getConsonantType(u) {
100
120
  return NOT_CONSONANT;
101
121
  }
102
122
 
123
+ /** @param {number} u @returns {number} */
103
124
  function getMarkType(u) {
104
125
  if (
105
126
  u === 0x0E31
@@ -116,6 +137,7 @@ function getMarkType(u) {
116
137
 
117
138
  const T0 = 0, T1 = 1, T2 = 2, T3 = 3;
118
139
  const ABOVE_START_STATE = [T0, T1, T0, T0, T3]; // NC AC RC DC NOT_CONSONANT
140
+ /** @type {[number, number][][]} */
119
141
  const ABOVE_STATE_MACHINE = [
120
142
  // AV BV T
121
143
  [[NOP, T3], [NOP, T0], [SD, T3]], // T0
@@ -126,6 +148,7 @@ const ABOVE_STATE_MACHINE = [
126
148
 
127
149
  const B0 = 0, B1 = 1, B2 = 2;
128
150
  const BELOW_START_STATE = [B0, B0, B1, B2, B2];
151
+ /** @type {[number, number][][]} */
129
152
  const BELOW_STATE_MACHINE = [
130
153
  // AV BV T
131
154
  [[NOP, B0], [NOP, B2], [NOP, B0]], // B0
@@ -134,6 +157,7 @@ const BELOW_STATE_MACHINE = [
134
157
  ];
135
158
 
136
159
  // [original, Windows PUA, Mac PUA] per action
160
+ /** @type {Record<number, number[][]>} */
137
161
  const PUA_MAPPINGS = {
138
162
  [SD]: [
139
163
  [0x0E48, 0xF70A, 0xF88B], // MAI EK
@@ -172,6 +196,7 @@ const PUA_MAPPINGS = {
172
196
  ]
173
197
  };
174
198
 
199
+ /** @param {number} u @param {number} action @param {LayoutFont} font @returns {number} */
175
200
  function thaiPuaShape(u, action, font) {
176
201
  if (action === NOP) return u;
177
202
  const mappings = PUA_MAPPINGS[action];
@@ -185,12 +210,16 @@ function thaiPuaShape(u, action, font) {
185
210
  return u;
186
211
  }
187
212
 
213
+ /** @param {GlyphInfoLike[]} glyphs @param {number} index @param {number} newCp @param {LayoutFont} font */
188
214
  function replaceGlyphCodePoint(glyphs, index, newCp, font) {
189
215
  const prev = glyphs[index];
190
216
  if (prev.codePoints[0] === newCp) return;
191
- glyphs[index] = new GlyphInfo(font, font.glyphForCodePoint(newCp).id, [newCp], prev.features);
217
+ const g = font.glyphForCodePoint(newCp);
218
+ if (!g) return;
219
+ glyphs[index] = new GlyphInfo(font, g.id, [newCp], prev.features);
192
220
  }
193
221
 
222
+ /** @param {GlyphInfoLike[]} glyphs @param {LayoutFont} font */
194
223
  function applyThaiPuaShaping(glyphs, font) {
195
224
  let aboveState = ABOVE_START_STATE[NOT_CONSONANT];
196
225
  let belowState = BELOW_START_STATE[NOT_CONSONANT];
@@ -226,12 +255,14 @@ function applyThaiPuaShaping(glyphs, font) {
226
255
  }
227
256
 
228
257
  // Gate PUA shaping on absence of Thai GSUB (HB plan->map.found_script[0]).
258
+ /** @param {LayoutFont} font @returns {boolean} */
229
259
  function hasThaiGsub(font) {
230
260
  const gsub = font.GSUB;
231
261
  if (!gsub || !gsub.scriptList) return false;
232
- return gsub.scriptList.some(entry => entry.tag === 'thai' || entry.tag === 'tha2');
262
+ return gsub.scriptList.some(/** @param {OTScriptRecord} entry */ entry => entry.tag === 'thai' || entry.tag === 'tha2');
233
263
  }
234
264
 
265
+ /** @param {ScriptTag | string[] | null | undefined} script @returns {boolean} */
235
266
  function isThaiBufferScript(script) {
236
267
  if (Array.isArray(script)) return script.includes('thai');
237
268
  return script === 'thai';
@@ -6,9 +6,28 @@ import useData from './use.json';
6
6
  import { decodeBase64 } from '../../utils';
7
7
  import useTrie from './use.trie';
8
8
 
9
- const { categories, decompositions } = useData;
9
+ /** @typedef {import('../../../types/fontkit').ShapingPlanLike} ShapingPlanLike */
10
+ /** @typedef {import('../../../types/fontkit').GlyphInfoLike} GlyphInfoLike */
11
+ /** @typedef {import('../../../types/fontkit').LayoutFont} LayoutFont */
12
+ /** @typedef {import('../../../types/fontkit').USEShaperInfo} USEShaperInfo */
13
+ /** @typedef {import('dfa').StateMachineDefinition} StateMachineDefinition */
14
+
15
+ /**
16
+ * Generated USE payload: category tables plus a compiled `dfa` machine
17
+ * (`stateTable` / `accepting` / `tags`). Extra keys are fine — StateMachine
18
+ * only reads the three DFA fields.
19
+ *
20
+ * @typedef {StateMachineDefinition & {
21
+ * categories: string[],
22
+ * decompositions: Record<string, number[]>
23
+ * }} UseData
24
+ */
25
+
26
+ /** @type {UseData} */
27
+ const useDataTyped = /** @type {UseData} */ (useData);
28
+ const { categories, decompositions } = useDataTyped;
10
29
  const trie = new UnicodeTrie(decodeBase64(useTrie));
11
- const stateMachine = new StateMachine(useData);
30
+ const stateMachine = new StateMachine(useDataTyped);
12
31
 
13
32
  /**
14
33
  * This shaper is an implementation of the Universal Shaping Engine, which
@@ -17,6 +36,9 @@ const stateMachine = new StateMachine(useData);
17
36
  */
18
37
  export default class UniversalShaper extends DefaultShaper {
19
38
  static zeroMarkWidths = 'BEFORE_GPOS';
39
+ /**
40
+ * @param {ShapingPlanLike} plan
41
+ */
20
42
  static planFeatures(plan) {
21
43
  plan.addStage(setupSyllables);
22
44
 
@@ -43,16 +65,25 @@ export default class UniversalShaper extends DefaultShaper {
43
65
  plan.addStage(['abvs', 'blws', 'pres', 'psts', 'dist', 'abvm', 'blwm']);
44
66
  }
45
67
 
68
+ /**
69
+ * @param {ShapingPlanLike} plan
70
+ * @param {GlyphInfoLike[]} glyphs
71
+ */
46
72
  static assignFeatures(plan, glyphs) {
47
73
  // Decompose split vowels
48
74
  // TODO: do this in a more general unicode normalizer
49
75
  for (let i = glyphs.length - 1; i >= 0; i--) {
50
76
  let codepoint = glyphs[i].codePoints[0];
51
- if (decompositions[codepoint]) {
52
- let decomposed = decompositions[codepoint].map((c) => {
77
+ let decomp = decompositions[String(codepoint)] || decompositions[/** @type {keyof typeof decompositions} */ (/** @type {unknown} */ (codepoint))];
78
+ if (decomp) {
79
+ /** @type {GlyphInfo[]} */
80
+ let decomposed = [];
81
+ for (let c of decomp) {
53
82
  let g = plan.font.glyphForCodePoint(c);
54
- return new GlyphInfo(plan.font, g.id, [c], glyphs[i].features);
55
- });
83
+ if (g) {
84
+ decomposed.push(new GlyphInfo(plan.font, g.id, [c], glyphs[i].features));
85
+ }
86
+ }
56
87
 
57
88
  glyphs.splice(i, 1, ...decomposed);
58
89
  }
@@ -60,11 +91,17 @@ export default class UniversalShaper extends DefaultShaper {
60
91
  }
61
92
  }
62
93
 
94
+ /** @param {GlyphInfoLike} glyph @returns {number} */
63
95
  function useCategory(glyph) {
64
96
  return trie.get(glyph.codePoints[0]);
65
97
  }
66
98
 
67
99
  class USEInfo {
100
+ /**
101
+ * @param {string} category
102
+ * @param {string} syllableType
103
+ * @param {number} syllable
104
+ */
68
105
  constructor(category, syllableType, syllable) {
69
106
  this.category = category;
70
107
  this.syllableType = syllableType;
@@ -72,6 +109,27 @@ class USEInfo {
72
109
  }
73
110
  }
74
111
 
112
+ /**
113
+ * @param {import('../../../types/fontkit').IndicShaperInfo | import('../../../types/fontkit').USEShaperInfo | null} info
114
+ * @returns {info is USEShaperInfo}
115
+ */
116
+ function isUSEShaperInfo(info) {
117
+ return info != null && typeof info.category === 'string';
118
+ }
119
+
120
+ /**
121
+ * @param {GlyphInfoLike} glyph
122
+ * @returns {USEShaperInfo}
123
+ */
124
+ function useInfo(glyph) {
125
+ let info = glyph.shaperInfo;
126
+ if (!isUSEShaperInfo(info)) {
127
+ throw new Error('Expected USE shaperInfo');
128
+ }
129
+ return info;
130
+ }
131
+
132
+ /** @type {import('../../../types/fontkit').ShapingStageFn} */
75
133
  function setupSyllables(font, glyphs) {
76
134
  let syllable = 0;
77
135
  for (let [start, end, tags] of stateMachine.match(glyphs.map(useCategory))) {
@@ -83,43 +141,48 @@ function setupSyllables(font, glyphs) {
83
141
  }
84
142
 
85
143
  // Assign rphf feature
86
- let limit = glyphs[start].shaperInfo.category === 'R' ? 1 : Math.min(3, end - start);
144
+ let limit = useInfo(glyphs[start]).category === 'R' ? 1 : Math.min(3, end - start);
87
145
  for (let i = start; i < start + limit; i++) {
88
146
  glyphs[i].features.rphf = true;
89
147
  }
90
148
  }
91
149
  }
92
150
 
151
+ /** @type {import('../../../types/fontkit').ShapingStageFn} */
93
152
  function clearSubstitutionFlags(font, glyphs) {
94
153
  for (let glyph of glyphs) {
95
154
  glyph.substituted = false;
96
155
  }
97
156
  }
98
157
 
158
+ /** @type {import('../../../types/fontkit').ShapingStageFn} */
99
159
  function recordRphf(font, glyphs) {
100
160
  for (let glyph of glyphs) {
101
161
  if (glyph.substituted && glyph.features.rphf) {
102
162
  // Mark a substituted repha.
103
- glyph.shaperInfo.category = 'R';
163
+ useInfo(glyph).category = 'R';
104
164
  }
105
165
  }
106
166
  }
107
167
 
168
+ /** @type {import('../../../types/fontkit').ShapingStageFn} */
108
169
  function recordPref(font, glyphs) {
109
170
  for (let glyph of glyphs) {
110
171
  if (glyph.substituted) {
111
172
  // Mark a substituted pref as VPre, as they behave the same way.
112
- glyph.shaperInfo.category = 'VPre';
173
+ useInfo(glyph).category = 'VPre';
113
174
  }
114
175
  }
115
176
  }
116
177
 
178
+ /** @type {import('../../../types/fontkit').ShapingStageFn} */
117
179
  function reorder(font, glyphs) {
118
- let dottedCircle = font.glyphForCodePoint(0x25cc).id;
180
+ let dottedCircleGlyph = font.glyphForCodePoint(0x25cc);
181
+ let dottedCircle = dottedCircleGlyph ? dottedCircleGlyph.id : 0;
119
182
 
120
183
  for (let start = 0, end = nextSyllable(glyphs, 0); start < glyphs.length; start = end, end = nextSyllable(glyphs, start)) {
121
184
  let i, j;
122
- let info = glyphs[start].shaperInfo;
185
+ let info = useInfo(glyphs[start]);
123
186
  let type = info.syllableType;
124
187
 
125
188
  // Only a few syllable types need reordering.
@@ -133,7 +196,7 @@ function reorder(font, glyphs) {
133
196
  g.shaperInfo = info;
134
197
 
135
198
  // Insert after possible Repha.
136
- for (i = start; i < end && glyphs[i].shaperInfo.category === 'R'; i++);
199
+ for (i = start; i < end && useInfo(glyphs[i]).category === 'R'; i++);
137
200
  glyphs.splice(i + 1, 0, g);
138
201
  end++;
139
202
  }
@@ -142,7 +205,7 @@ function reorder(font, glyphs) {
142
205
  if (info.category === 'R' && end - start > 1) {
143
206
  // Got a repha. Reorder it to after first base, before first halant.
144
207
  for (i = start + 1; i < end; i++) {
145
- info = glyphs[i].shaperInfo;
208
+ info = useInfo(glyphs[i]);
146
209
  if (isBase(info) || isHalant(glyphs[i])) {
147
210
  // If we hit a halant, move before it; otherwise it's a base: move to it's
148
211
  // place, and shift things in between backward.
@@ -158,7 +221,7 @@ function reorder(font, glyphs) {
158
221
 
159
222
  // Move things back.
160
223
  for (i = start, j = end; i < end; i++) {
161
- info = glyphs[i].shaperInfo;
224
+ info = useInfo(glyphs[i]);
162
225
  if (isBase(info) || isHalant(glyphs[i])) {
163
226
  // If we hit a halant, move after it; otherwise it's a base: move to it's
164
227
  // place, and shift things in between backward.
@@ -170,17 +233,20 @@ function reorder(font, glyphs) {
170
233
  }
171
234
  }
172
235
 
236
+ /** @param {GlyphInfoLike[]} glyphs @param {number} start @returns {number} */
173
237
  function nextSyllable(glyphs, start) {
174
238
  if (start >= glyphs.length) return start;
175
- let syllable = glyphs[start].shaperInfo.syllable;
176
- while (++start < glyphs.length && glyphs[start].shaperInfo.syllable === syllable);
239
+ let syllable = useInfo(glyphs[start]).syllable;
240
+ while (++start < glyphs.length && useInfo(glyphs[start]).syllable === syllable);
177
241
  return start;
178
242
  }
179
243
 
244
+ /** @param {GlyphInfoLike} glyph @returns {boolean} */
180
245
  function isHalant(glyph) {
181
- return glyph.shaperInfo.category === 'H' && !glyph.isLigated;
246
+ return useInfo(glyph).category === 'H' && !glyph.isLigated;
182
247
  }
183
248
 
249
+ /** @param {USEShaperInfo} info @returns {boolean} */
184
250
  function isBase(info) {
185
251
  return info.category === 'B' || info.category === 'GB';
186
252
  }