@xterm/addon-ligatures 0.11.0-beta.9 → 0.11.0-beta.91
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 +25 -1
- package/lib/addon-ligatures.mjs +2 -2
- package/lib/addon-ligatures.mjs.map +4 -4
- package/package.json +8 -7
- package/src/font.ts +1 -1
- package/src/fontLigatures/flatten.ts +40 -0
- package/src/fontLigatures/index.ts +262 -0
- package/src/fontLigatures/merge.ts +393 -0
- package/src/fontLigatures/mergeRange.ts +66 -0
- package/src/fontLigatures/processors/6-1.ts +82 -0
- package/src/fontLigatures/processors/6-2.ts +96 -0
- package/src/fontLigatures/processors/6-3.ts +73 -0
- package/src/fontLigatures/processors/8-1.ts +69 -0
- package/src/fontLigatures/processors/classDef.ts +84 -0
- package/src/fontLigatures/processors/coverage.ts +43 -0
- package/src/fontLigatures/processors/helper.ts +187 -0
- package/src/fontLigatures/processors/substitution.ts +62 -0
- package/src/fontLigatures/tables.ts +112 -0
- package/src/fontLigatures/types.ts +86 -0
- package/src/fontLigatures/walk.ts +67 -0
- package/src/index.ts +1 -1
|
@@ -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
|
+
}
|