@xterm/addon-ligatures 0.11.0-beta.9 → 0.11.0-beta.90

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.
@@ -0,0 +1,393 @@
1
+ import { ILookupTree, ILookupTreeEntry } from './types';
2
+
3
+ /**
4
+ * Merges the provided trees into a single lookup tree. When conflicting lookups
5
+ * are encountered between two trees, the one with the lower index, then the
6
+ * lower subindex is chosen.
7
+ *
8
+ * @param trees Array of trees to merge. Entries in earlier trees are favored
9
+ * over those in later trees when there is a choice.
10
+ */
11
+ export default function mergeTrees(trees: ILookupTree[]): ILookupTree {
12
+ const result: ILookupTree = {
13
+ individual: {},
14
+ range: []
15
+ };
16
+
17
+ const mergedEntries = new WeakMap<ILookupTreeEntry, Set<ILookupTreeEntry>>();
18
+ for (const tree of trees) {
19
+ mergeSubtree(result, tree, mergedEntries);
20
+ }
21
+
22
+ return result;
23
+ }
24
+
25
+ /**
26
+ * Recursively merges the data for the mergeTree into the mainTree.
27
+ *
28
+ * @param mainTree The tree where the values should be merged
29
+ * @param mergeTree The tree to be merged into the mainTree
30
+ * @param mergedEntries WeakMap to track already merged entry pairs
31
+ */
32
+ function mergeSubtree(mainTree: ILookupTree, mergeTree: ILookupTree, mergedEntries: WeakMap<ILookupTreeEntry, Set<ILookupTreeEntry>>): void {
33
+ // Need to fix this recursively (and handle lookups)
34
+ for (const [glyphId, value] of Object.entries(mergeTree.individual)) {
35
+ // The main tree is guaranteed to have no overlaps between the
36
+ // individual and range values, so if we match an invididual, there
37
+ // must not be a range
38
+ if (mainTree.individual[glyphId]) {
39
+ mergeTreeEntry(mainTree.individual[glyphId], value, mergedEntries);
40
+ } else {
41
+ let matched = false;
42
+ for (const [index, { range, entry }] of mainTree.range.entries()) {
43
+ const overlap = getIndividualOverlap(Number(glyphId), range);
44
+
45
+ // Don't overlap
46
+ if (overlap.both === null) {
47
+ continue;
48
+ }
49
+
50
+ matched = true;
51
+
52
+ // If they overlap, we have to split the range and then
53
+ // merge the overlap
54
+ mainTree.individual[glyphId] = value;
55
+ mergeTreeEntry(mainTree.individual[glyphId], cloneEntry(entry), mergedEntries);
56
+
57
+ // When there's an overlap, we also have to fix up the range
58
+ // that we had already processed
59
+ mainTree.range.splice(index, 1);
60
+ for (const glyph of overlap.second) {
61
+ if (Array.isArray(glyph)) {
62
+ mainTree.range.push({
63
+ range: glyph,
64
+ entry: cloneEntry(entry)
65
+ });
66
+ } else {
67
+ mainTree.individual[glyph] = cloneEntry(entry);
68
+ }
69
+ }
70
+ }
71
+
72
+ if (!matched) {
73
+ mainTree.individual[glyphId] = value;
74
+ }
75
+ }
76
+ }
77
+
78
+ for (const { range, entry } of mergeTree.range) {
79
+ // Ranges are more complicated, because they can overlap with
80
+ // multiple things, individual and range alike. We start by
81
+ // eliminating ranges that are already present in another range
82
+ let remainingRanges: (number | [number, number])[] = [range];
83
+
84
+ for (let index = 0; index < mainTree.range.length; index++) {
85
+ const { range, entry: resultEntry } = mainTree.range[index];
86
+ for (const [remainingIndex, remainingRange] of remainingRanges.entries()) {
87
+ if (Array.isArray(remainingRange)) {
88
+ const overlap = getRangeOverlap(remainingRange, range);
89
+ if (overlap.both === null) {
90
+ continue;
91
+ }
92
+
93
+ mainTree.range.splice(index, 1);
94
+ index--;
95
+
96
+ const entryToMerge: ILookupTreeEntry = cloneEntry(resultEntry);
97
+ if (Array.isArray(overlap.both)) {
98
+ mainTree.range.push({
99
+ range: overlap.both,
100
+ entry: entryToMerge
101
+ });
102
+ } else {
103
+ mainTree.individual[overlap.both] = entryToMerge;
104
+ }
105
+
106
+ mergeTreeEntry(entryToMerge, cloneEntry(entry), mergedEntries);
107
+
108
+ for (const second of overlap.second) {
109
+ if (Array.isArray(second)) {
110
+ mainTree.range.push({
111
+ range: second,
112
+ entry: cloneEntry(resultEntry)
113
+ });
114
+ } else {
115
+ mainTree.individual[second] = cloneEntry(resultEntry);
116
+ }
117
+ }
118
+
119
+ remainingRanges = overlap.first;
120
+ } else {
121
+ const overlap = getIndividualOverlap(remainingRange, range);
122
+ if (overlap.both === null) {
123
+ continue;
124
+ }
125
+
126
+ // If they overlap, we have to split the range and then
127
+ // merge the overlap
128
+ mainTree.individual[remainingRange] = cloneEntry(entry);
129
+ mergeTreeEntry(mainTree.individual[remainingRange], cloneEntry(resultEntry), mergedEntries);
130
+
131
+ // When there's an overlap, we also have to fix up the range
132
+ // that we had already processed
133
+ mainTree.range.splice(index, 1);
134
+ index--;
135
+
136
+ for (const glyph of overlap.second) {
137
+ if (Array.isArray(glyph)) {
138
+ mainTree.range.push({
139
+ range: glyph,
140
+ entry: cloneEntry(resultEntry)
141
+ });
142
+ } else {
143
+ mainTree.individual[glyph] = cloneEntry(resultEntry);
144
+ }
145
+ }
146
+
147
+ remainingRanges.splice(remainingIndex, 1, ...overlap.first);
148
+ break;
149
+ }
150
+ }
151
+ }
152
+
153
+ // Next, we run the same against any individual glyphs
154
+ for (const glyphId of Object.keys(mainTree.individual)) {
155
+ for (const [remainingIndex, remainingRange] of remainingRanges.entries()) {
156
+ if (Array.isArray(remainingRange)) {
157
+ const overlap = getIndividualOverlap(Number(glyphId), remainingRange);
158
+ if (overlap.both === null) {
159
+ continue;
160
+ }
161
+
162
+ // If they overlap, we have to merge the overlap
163
+ mergeTreeEntry(mainTree.individual[glyphId], cloneEntry(entry), mergedEntries);
164
+
165
+ // Update the remaining ranges
166
+ remainingRanges.splice(remainingIndex, 1, ...overlap.second);
167
+ break;
168
+ } else {
169
+ if (Number(glyphId) === remainingRange) {
170
+ mergeTreeEntry(mainTree.individual[glyphId], cloneEntry(entry), mergedEntries);
171
+ break;
172
+ }
173
+ }
174
+ }
175
+ }
176
+
177
+ // Any remaining ranges should just be added directly
178
+ for (const remainingRange of remainingRanges) {
179
+ if (Array.isArray(remainingRange)) {
180
+ mainTree.range.push({
181
+ range: remainingRange,
182
+ entry: cloneEntry(entry)
183
+ });
184
+ } else {
185
+ mainTree.individual[remainingRange] = cloneEntry(entry);
186
+ }
187
+ }
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Recursively merges the entry forr the mergeTree into the mainTree
193
+ *
194
+ * @param mainTree The entry where the values should be merged
195
+ * @param mergeTree The entry to merge into the mainTree
196
+ * @param mergedEntries WeakMap to track already merged entry pairs
197
+ */
198
+ function mergeTreeEntry(mainTree: ILookupTreeEntry, mergeTree: ILookupTreeEntry, mergedEntries: WeakMap<ILookupTreeEntry, Set<ILookupTreeEntry>>): void {
199
+ // Check if we've already merged this pair
200
+ let mergedSet = mergedEntries.get(mainTree);
201
+ if (mergedSet?.has(mergeTree)) {
202
+ return;
203
+ }
204
+ if (!mergedSet) {
205
+ mergedSet = new Set();
206
+ mergedEntries.set(mainTree, mergedSet);
207
+ }
208
+ mergedSet.add(mergeTree);
209
+
210
+ if (
211
+ mergeTree.lookup && (
212
+ !mainTree.lookup ||
213
+ mainTree.lookup.index > mergeTree.lookup.index ||
214
+ (mainTree.lookup.index === mergeTree.lookup.index && mainTree.lookup.subIndex > mergeTree.lookup.subIndex)
215
+ )
216
+ ) {
217
+ mainTree.lookup = mergeTree.lookup;
218
+ }
219
+
220
+ if (mergeTree.forward) {
221
+ if (!mainTree.forward) {
222
+ mainTree.forward = mergeTree.forward;
223
+ } else {
224
+ mergeSubtree(mainTree.forward, mergeTree.forward, mergedEntries);
225
+ }
226
+ }
227
+
228
+ if (mergeTree.reverse) {
229
+ if (!mainTree.reverse) {
230
+ mainTree.reverse = mergeTree.reverse;
231
+ } else {
232
+ mergeSubtree(mainTree.reverse, mergeTree.reverse, mergedEntries);
233
+ }
234
+ }
235
+ }
236
+
237
+ interface IOverlap {
238
+ first: (number | [number, number])[];
239
+ second: (number | [number, number])[];
240
+ both: number | [number, number] | null;
241
+ }
242
+
243
+ /**
244
+ * Determines the overlap (if any) between two ranges. Returns the distinct
245
+ * ranges for each range and the overlap (if any).
246
+ *
247
+ * @param first First range
248
+ * @param second Second range
249
+ */
250
+ function getRangeOverlap(first: [number, number], second: [number, number]): IOverlap {
251
+ const result: IOverlap = {
252
+ first: [],
253
+ second: [],
254
+ both: null
255
+ };
256
+
257
+ // Both
258
+ if (first[0] < second[1] && second[0] < first[1]) {
259
+ const start = Math.max(first[0], second[0]);
260
+ const end = Math.min(first[1], second[1]);
261
+ result.both = rangeOrIndividual(start, end);
262
+ }
263
+
264
+ // Before
265
+ if (first[0] < second[0]) {
266
+ const start = first[0];
267
+ const end = Math.min(second[0], first[1]);
268
+ result.first.push(rangeOrIndividual(start, end));
269
+ } else if (second[0] < first[0]) {
270
+ const start = second[0];
271
+ const end = Math.min(second[1], first[0]);
272
+ result.second.push(rangeOrIndividual(start, end));
273
+ }
274
+
275
+ // After
276
+ if (first[1] > second[1]) {
277
+ const start = Math.max(first[0], second[1]);
278
+ const end = first[1];
279
+ result.first.push(rangeOrIndividual(start, end));
280
+ } else if (second[1] > first[1]) {
281
+ const start = Math.max(first[1], second[0]);
282
+ const end = second[1];
283
+ result.second.push(rangeOrIndividual(start, end));
284
+ }
285
+
286
+ return result;
287
+ }
288
+
289
+ /**
290
+ * Determines the overlap (if any) between the individual glyph and the range
291
+ * provided. Returns the glyphs and/or ranges that are unique to each provided
292
+ * and the overlap (if any).
293
+ *
294
+ * @param first Individual glyph
295
+ * @param second Range
296
+ */
297
+ function getIndividualOverlap(first: number, second: [number, number]): IOverlap {
298
+ // Disjoint
299
+ if (first < second[0] || first > second[1]) {
300
+ return {
301
+ first: [first],
302
+ second: [second],
303
+ both: null
304
+ };
305
+ }
306
+
307
+ const result: IOverlap = {
308
+ first: [],
309
+ second: [],
310
+ both: first
311
+ };
312
+
313
+ if (second[0] < first) {
314
+ result.second.push(rangeOrIndividual(second[0], first));
315
+ }
316
+
317
+ if (second[1] > first) {
318
+ result.second.push(rangeOrIndividual(first + 1, second[1]));
319
+ }
320
+
321
+ return result;
322
+ }
323
+
324
+ /**
325
+ * Returns an individual glyph if the range is of size one or a range if it is
326
+ * larger.
327
+ *
328
+ * @param start Beginning of the range (inclusive)
329
+ * @param end End of the range (exclusive)
330
+ */
331
+ function rangeOrIndividual(start: number, end: number): number | [number, number] {
332
+ if (end - start === 1) {
333
+ return start;
334
+ }
335
+ return [start, end];
336
+
337
+ }
338
+
339
+ /**
340
+ * Clones an individual lookup tree entry.
341
+ *
342
+ * @param entry Lookup tree entry to clone
343
+ * @param visited Map to track already cloned entries (prevents infinite loops)
344
+ */
345
+ function cloneEntry(entry: ILookupTreeEntry, visited: Map<ILookupTreeEntry, ILookupTreeEntry> = new Map()): ILookupTreeEntry {
346
+ if (visited.has(entry)) {
347
+ return visited.get(entry)!;
348
+ }
349
+
350
+ const result: ILookupTreeEntry = {};
351
+ visited.set(entry, result);
352
+
353
+ if (entry.forward) {
354
+ result.forward = cloneTree(entry.forward, visited);
355
+ }
356
+
357
+ if (entry.reverse) {
358
+ result.reverse = cloneTree(entry.reverse, visited);
359
+ }
360
+
361
+ if (entry.lookup) {
362
+ result.lookup = {
363
+ contextRange: entry.lookup.contextRange.slice() as [number, number],
364
+ index: entry.lookup.index,
365
+ length: entry.lookup.length,
366
+ subIndex: entry.lookup.subIndex,
367
+ substitutions: entry.lookup.substitutions.slice()
368
+ };
369
+ }
370
+
371
+ return result;
372
+ }
373
+
374
+ /**
375
+ * Clones a lookup tree.
376
+ *
377
+ * @param tree Lookup tree to clone
378
+ * @param visited Map to track already cloned entries (prevents infinite loops)
379
+ */
380
+ function cloneTree(tree: ILookupTree, visited: Map<ILookupTreeEntry, ILookupTreeEntry> = new Map()): ILookupTree {
381
+ const individual: { [glyphId: string]: ILookupTreeEntry } = {};
382
+ for (const [glyphId, entry] of Object.entries(tree.individual)) {
383
+ individual[glyphId] = cloneEntry(entry, visited);
384
+ }
385
+
386
+ return {
387
+ individual,
388
+ range: tree.range.map(({ range, entry }) => ({
389
+ range: range.slice() as [number, number],
390
+ entry: cloneEntry(entry, visited)
391
+ }))
392
+ };
393
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Merges the range defined by the provided start and end into the list of
3
+ * existing ranges. The merge is done in place on the existing range for
4
+ * performance and is also returned.
5
+ *
6
+ * @param ranges Existing range list
7
+ * @param newRangeStart Start position of the range to merge, inclusive
8
+ * @param newRangeEnd End position of range to merge, exclusive
9
+ */
10
+ export default function mergeRange(ranges: [number, number][], newRangeStart: number, newRangeEnd: number): [number, number][] {
11
+ let inRange = false;
12
+ for (let i = 0; i < ranges.length; i++) {
13
+ const range = ranges[i];
14
+ if (!inRange) {
15
+ if (newRangeEnd <= range[0]) {
16
+ // Case 1: New range is before the search range
17
+ ranges.splice(i, 0, [newRangeStart, newRangeEnd]);
18
+ return ranges;
19
+ }
20
+ if (newRangeEnd <= range[1]) {
21
+ // Case 2: New range is either wholly contained within the
22
+ // search range or overlaps with the front of it
23
+ range[0] = Math.min(newRangeStart, range[0]);
24
+ return ranges;
25
+ }
26
+ if (newRangeStart < range[1]) {
27
+ // Case 3: New range either wholly contains the search range
28
+ // or overlaps with the end of it
29
+ range[0] = Math.min(newRangeStart, range[0]);
30
+ inRange = true;
31
+ } else {
32
+ // Case 4: New range starts after the search range
33
+ continue;
34
+ }
35
+ } else {
36
+ if (newRangeEnd <= range[0]) {
37
+ // Case 5: New range extends from previous range but doesn't
38
+ // reach the current one
39
+ ranges[i - 1][1] = newRangeEnd;
40
+ return ranges;
41
+ }
42
+ if (newRangeEnd <= range[1]) {
43
+ // Case 6: New range extends from prvious range into the
44
+ // current range
45
+ ranges[i - 1][1] = Math.max(newRangeEnd, range[1]);
46
+ ranges.splice(i, 1);
47
+ inRange = false;
48
+ return ranges;
49
+ }
50
+ // Case 7: New range extends from previous range past the
51
+ // end of the current range
52
+ ranges.splice(i, 1);
53
+ i--;
54
+ }
55
+ }
56
+
57
+ if (inRange) {
58
+ // Case 8: New range extends past the last existing range
59
+ ranges[ranges.length - 1][1] = newRangeEnd;
60
+ } else {
61
+ // Case 9: New range starts after the last existing range
62
+ ranges.push([newRangeStart, newRangeEnd]);
63
+ }
64
+
65
+ return ranges;
66
+ }
@@ -0,0 +1,82 @@
1
+ import { ChainingContextualSubstitutionTable, Lookup } from '../tables';
2
+ import { ILookupTree } from '../types';
3
+
4
+ import { listGlyphsByIndex } from './coverage';
5
+ import { processInputPosition, processLookaheadPosition, processBacktrackPosition, getInputTree, IEntryMeta } from './helper';
6
+
7
+ /**
8
+ * Build lookup tree for GSUB lookup table 6, format 1.
9
+ * https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#61-chaining-context-substitution-format-1-simple-glyph-contexts
10
+ *
11
+ * @param table JSON representation of the table
12
+ * @param lookups List of lookup tables
13
+ * @param tableIndex Index of this table in the overall lookup
14
+ */
15
+ export default function buildTree(table: ChainingContextualSubstitutionTable.IFormat1, lookups: Lookup[], tableIndex: number): ILookupTree {
16
+ const result: ILookupTree = {
17
+ individual: {},
18
+ range: []
19
+ };
20
+
21
+ const firstGlyphs = listGlyphsByIndex(table.coverage);
22
+
23
+ for (const { glyphId, index } of firstGlyphs) {
24
+ const chainRuleSet = table.chainRuleSets[index];
25
+
26
+ // If the chain rule set is null there's nothing to do with this table.
27
+ if (!chainRuleSet) {
28
+ continue;
29
+ }
30
+
31
+ for (const [subIndex, subTable] of chainRuleSet.entries()) {
32
+ let currentEntries: IEntryMeta[] = getInputTree(
33
+ result,
34
+ subTable.lookupRecords,
35
+ lookups,
36
+ 0,
37
+ glyphId
38
+ ).map(({ entry, substitution }) => ({ entry, substitutions: [substitution] }));
39
+
40
+ // We walk forward, then backward
41
+ for (const [index, glyph] of subTable.input.entries()) {
42
+ currentEntries = processInputPosition(
43
+ [glyph],
44
+ index + 1,
45
+ currentEntries,
46
+ subTable.lookupRecords,
47
+ lookups
48
+ );
49
+ }
50
+
51
+ for (const glyph of subTable.lookahead) {
52
+ currentEntries = processLookaheadPosition(
53
+ [glyph],
54
+ currentEntries
55
+ );
56
+ }
57
+
58
+ for (const glyph of subTable.backtrack) {
59
+ currentEntries = processBacktrackPosition(
60
+ [glyph],
61
+ currentEntries
62
+ );
63
+ }
64
+
65
+ // When we get to the end, insert the lookup information
66
+ for (const { entry, substitutions } of currentEntries) {
67
+ entry.lookup = {
68
+ substitutions,
69
+ length: subTable.input.length + 1,
70
+ index: tableIndex,
71
+ subIndex,
72
+ contextRange: [
73
+ -1 * subTable.backtrack.length,
74
+ 1 + subTable.input.length + subTable.lookahead.length
75
+ ]
76
+ };
77
+ }
78
+ }
79
+ }
80
+
81
+ return result;
82
+ }
@@ -0,0 +1,96 @@
1
+ import { ChainingContextualSubstitutionTable, Lookup } from '../tables';
2
+ import { ILookupTree } from '../types';
3
+ import mergeTrees from '../merge';
4
+
5
+ import { listGlyphsByIndex } from './coverage';
6
+ import getGlyphClass, { listClassGlyphs } from './classDef';
7
+ import { processInputPosition, processLookaheadPosition, processBacktrackPosition, getInputTree, IEntryMeta } from './helper';
8
+
9
+ /**
10
+ * Build lookup tree for GSUB lookup table 6, format 2.
11
+ * https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#62-chaining-context-substitution-format-2-class-based-glyph-contexts
12
+ *
13
+ * @param table JSON representation of the table
14
+ * @param lookups List of lookup tables
15
+ * @param tableIndex Index of this table in the overall lookup
16
+ */
17
+ export default function buildTree(table: ChainingContextualSubstitutionTable.IFormat2, lookups: Lookup[], tableIndex: number): ILookupTree {
18
+ const results: ILookupTree[] = [];
19
+
20
+ const firstGlyphs = listGlyphsByIndex(table.coverage);
21
+
22
+ for (const { glyphId } of firstGlyphs) {
23
+ const firstInputClass = getGlyphClass(table.inputClassDef, glyphId);
24
+ for (const [glyphId, inputClass] of firstInputClass.entries()) {
25
+ // istanbul ignore next - invalid font
26
+ if (inputClass === null) {
27
+ continue;
28
+ }
29
+
30
+ const classSet = table.chainClassSet[inputClass];
31
+
32
+ // If the class set is null there's nothing to do with this table.
33
+ if (!classSet) {
34
+ continue;
35
+ }
36
+
37
+ for (const [subIndex, subTable] of classSet.entries()) {
38
+ const result: ILookupTree = {
39
+ individual: {},
40
+ range: []
41
+ };
42
+
43
+ let currentEntries: IEntryMeta[] = getInputTree(
44
+ result,
45
+ subTable.lookupRecords,
46
+ lookups,
47
+ 0,
48
+ glyphId
49
+ ).map(({ entry, substitution }) => ({ entry, substitutions: [substitution] }));
50
+
51
+ for (const [index, classNum] of subTable.input.entries()) {
52
+ currentEntries = processInputPosition(
53
+ listClassGlyphs(table.inputClassDef, classNum),
54
+ index + 1,
55
+ currentEntries,
56
+ subTable.lookupRecords,
57
+ lookups
58
+ );
59
+ }
60
+
61
+ for (const classNum of subTable.lookahead) {
62
+ currentEntries = processLookaheadPosition(
63
+ listClassGlyphs(table.lookaheadClassDef, classNum),
64
+ currentEntries
65
+ );
66
+ }
67
+
68
+ for (const classNum of subTable.backtrack) {
69
+ currentEntries = processBacktrackPosition(
70
+ listClassGlyphs(table.backtrackClassDef, classNum),
71
+ currentEntries
72
+ );
73
+ }
74
+
75
+ // When we get to the end, all of the entries we've accumulated
76
+ // should have a lookup defined
77
+ for (const { entry, substitutions } of currentEntries) {
78
+ entry.lookup = {
79
+ substitutions,
80
+ index: tableIndex,
81
+ subIndex,
82
+ length: subTable.input.length + 1,
83
+ contextRange: [
84
+ -1 * subTable.backtrack.length,
85
+ 1 + subTable.input.length + subTable.lookahead.length
86
+ ]
87
+ };
88
+ }
89
+
90
+ results.push(result);
91
+ }
92
+ }
93
+ }
94
+
95
+ return mergeTrees(results);
96
+ }
@@ -0,0 +1,73 @@
1
+ import { ChainingContextualSubstitutionTable, Lookup } from '../tables';
2
+ import { ILookupTree } from '../types';
3
+
4
+ import { listGlyphsByIndex } from './coverage';
5
+ import { processInputPosition, processLookaheadPosition, processBacktrackPosition, getInputTree, IEntryMeta } from './helper';
6
+
7
+ /**
8
+ * Build lookup tree for GSUB lookup table 6, format 3.
9
+ * https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#63-chaining-context-substitution-format-3-coverage-based-glyph-contexts
10
+ *
11
+ * @param table JSON representation of the table
12
+ * @param lookups List of lookup tables
13
+ * @param tableIndex Index of this table in the overall lookup
14
+ */
15
+ export default function buildTree(table: ChainingContextualSubstitutionTable.IFormat3, lookups: Lookup[], tableIndex: number): ILookupTree {
16
+ const result: ILookupTree = {
17
+ individual: {},
18
+ range: []
19
+ };
20
+
21
+ const firstGlyphs = listGlyphsByIndex(table.inputCoverage[0]);
22
+
23
+ for (const { glyphId } of firstGlyphs) {
24
+ let currentEntries: IEntryMeta[] = getInputTree(
25
+ result,
26
+ table.lookupRecords,
27
+ lookups,
28
+ 0,
29
+ glyphId
30
+ ).map(({ entry, substitution }) => ({ entry, substitutions: [substitution] }));
31
+
32
+ for (const [index, coverage] of table.inputCoverage.slice(1).entries()) {
33
+ currentEntries = processInputPosition(
34
+ listGlyphsByIndex(coverage).map(glyph => glyph.glyphId),
35
+ index + 1,
36
+ currentEntries,
37
+ table.lookupRecords,
38
+ lookups
39
+ );
40
+ }
41
+
42
+ for (const coverage of table.lookaheadCoverage) {
43
+ currentEntries = processLookaheadPosition(
44
+ listGlyphsByIndex(coverage).map(glyph => glyph.glyphId),
45
+ currentEntries
46
+ );
47
+ }
48
+
49
+ for (const coverage of table.backtrackCoverage) {
50
+ currentEntries = processBacktrackPosition(
51
+ listGlyphsByIndex(coverage).map(glyph => glyph.glyphId),
52
+ currentEntries
53
+ );
54
+ }
55
+
56
+ // When we get to the end, all of the entries we've accumulated
57
+ // should have a lookup defined
58
+ for (const { entry, substitutions } of currentEntries) {
59
+ entry.lookup = {
60
+ substitutions,
61
+ index: tableIndex,
62
+ subIndex: 0,
63
+ length: table.inputCoverage.length,
64
+ contextRange: [
65
+ -1 * table.backtrackCoverage.length,
66
+ table.inputCoverage.length + table.lookaheadCoverage.length
67
+ ]
68
+ };
69
+ }
70
+ }
71
+
72
+ return result;
73
+ }