@mark1russell7/typeset 0.1.1 → 0.1.3
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/README.md +21 -1
- package/build/chunks/engines.d.ts +32 -0
- package/build/chunks/engines.d.ts.map +1 -0
- package/build/chunks/engines.js +35 -0
- package/build/chunks/engines.js.map +1 -0
- package/build/chunks/font.d.ts +228 -0
- package/build/chunks/font.d.ts.map +1 -0
- package/build/chunks/font.js +3779 -0
- package/build/chunks/font.js.map +1 -0
- package/build/chunks/fonts.d.ts +31 -0
- package/build/chunks/fonts.d.ts.map +1 -0
- package/build/chunks/fonts.js +150 -0
- package/build/chunks/fonts.js.map +1 -0
- package/build/chunks/measurer.d.ts +67 -0
- package/build/chunks/measurer.d.ts.map +1 -0
- package/build/chunks/measurer.js +277 -0
- package/build/chunks/measurer.js.map +1 -0
- package/build/chunks/sfnt.d.ts +22 -0
- package/build/chunks/sfnt.d.ts.map +1 -0
- package/build/chunks/text.js.map +1 -1
- package/build/fonts-inspect.d.ts +10 -0
- package/build/fonts-inspect.d.ts.map +1 -0
- package/build/fonts-inspect.js +1 -0
- package/build/fonts.d.ts +5 -0
- package/build/fonts.js +4 -0
- package/build/index.d.ts +3 -1
- package/build/index.js +3 -1
- package/build/measurer.d.ts +2 -0
- package/build/measurer.js +1 -0
- package/package.json +18 -4
- package/src/fonts/bidi.ts +125 -0
- package/src/fonts/binary.ts +41 -0
- package/src/fonts/bound.ts +167 -0
- package/src/fonts/engines.ts +38 -0
- package/src/fonts/font.ts +458 -0
- package/src/fonts/gvar.ts +220 -0
- package/src/fonts/index.ts +11 -0
- package/src/fonts/indic.ts +626 -0
- package/src/fonts/joining.ts +141 -0
- package/src/fonts/kerning.ts +278 -0
- package/src/fonts/sfnt.ts +102 -0
- package/src/fonts/substitution.ts +686 -0
- package/src/fonts/tables.ts +162 -0
- package/src/fonts/variations.ts +175 -0
- package/src/fonts/words.ts +119 -0
- package/src/fonts-inspect.ts +8 -0
- package/src/fonts.ts +9 -0
- package/src/index.ts +2 -0
- package/src/measurer/advance.ts +307 -0
- package/src/measurer.ts +5 -0
- package/src/text/rope.ts +1 -1
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
import type { Reader } from "./binary.ts";
|
|
2
|
+
import { IGNORE_BASE, IGNORE_LIGATURES, IGNORE_MARKS, scriptTags } from "./kerning.ts";
|
|
3
|
+
import { classOf, coverageGlyphs, coverageIndex } from "./tables.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The GSUB features that the default model applies: the features that HarfBuzz and Chromium apply to horizontal text
|
|
7
|
+
* when the page does not change them.
|
|
8
|
+
*/
|
|
9
|
+
export const defaultFeatures: readonly string[] = ["ccmp", "locl", "rlig", "liga", "clig", "calt", "rclt"];
|
|
10
|
+
|
|
11
|
+
/** The lookup flag that selects a mark filtering set of GDEF. */
|
|
12
|
+
const USE_MARK_FILTERING_SET = 0x10;
|
|
13
|
+
/** The largest depth of nested lookups, as in HarfBuzz. */
|
|
14
|
+
const MAX_NESTING = 64;
|
|
15
|
+
|
|
16
|
+
/** The data of GDEF that tells which glyphs a lookup skips. */
|
|
17
|
+
export interface GlyphFilter {
|
|
18
|
+
/** This function gives the GDEF class of a glyph: 1 base, 2 ligature, 3 mark, 4 component, 0 none. */
|
|
19
|
+
glyphClass(glyph: number): number;
|
|
20
|
+
/** This function gives the mark attachment class of a glyph, or 0. */
|
|
21
|
+
markAttachClass(glyph: number): number;
|
|
22
|
+
/** This function tells if a glyph is in the mark filtering set `set`. */
|
|
23
|
+
inMarkSet(set: number, glyph: number): boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** This function reads the glyph classes, the mark attachment classes and the mark filtering sets of GDEF. */
|
|
27
|
+
export function readGlyphFilter(gdef: Reader | undefined, glyphClass: (glyph: number) => number): GlyphFilter {
|
|
28
|
+
const attachOffset = gdef === undefined ? 0 : gdef.u16(10);
|
|
29
|
+
const attach = attachOffset === 0 ? undefined : gdef!.part(attachOffset);
|
|
30
|
+
// GDEF 1.2 adds the MarkGlyphSetsDef table
|
|
31
|
+
const setsOffset = gdef === undefined || gdef.u16(2) < 2 ? 0 : gdef.u16(12);
|
|
32
|
+
const sets = setsOffset === 0 ? undefined : gdef!.part(setsOffset);
|
|
33
|
+
return {
|
|
34
|
+
glyphClass,
|
|
35
|
+
markAttachClass: (glyph) => (attach === undefined ? 0 : classOf(attach, glyph)),
|
|
36
|
+
inMarkSet(set, glyph) {
|
|
37
|
+
if (sets === undefined || set >= sets.u16(2)) return false;
|
|
38
|
+
return coverageIndex(sets.part(sets.u32(4 + set * 4)), glyph) >= 0;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The glyph property of HarfBuzz for a glyph that a substitution changed. */
|
|
44
|
+
export const SUBSTITUTED = 0x10;
|
|
45
|
+
/** The glyph property of HarfBuzz for a glyph that a ligature substitution made. */
|
|
46
|
+
export const LIGATED = 0x20;
|
|
47
|
+
/** The glyph property of HarfBuzz for a glyph that a multiple substitution made, with more than one glyph. */
|
|
48
|
+
export const MULTIPLIED = 0x40;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The data that a syllabic shaper keeps with each glyph, as the `hb_glyph_info_t` of HarfBuzz keeps it. A substitution
|
|
52
|
+
* gives each new glyph a copy of the data of the glyph that it replaces. A ligature keeps the data of its first glyph.
|
|
53
|
+
*/
|
|
54
|
+
export interface GlyphInfo {
|
|
55
|
+
/** The feature mask. A lookup starts only at a glyph with a bit of the mask of the lookup. */
|
|
56
|
+
mask: number;
|
|
57
|
+
/** The syllable byte of HarfBuzz: the serial number in the high 4 bits, and the syllable type in the low 4 bits. */
|
|
58
|
+
syllable: number;
|
|
59
|
+
/** The glyph properties `SUBSTITUTED`, `LIGATED` and `MULTIPLIED`. */
|
|
60
|
+
props: number;
|
|
61
|
+
/** The category of the shaper. */
|
|
62
|
+
category: number;
|
|
63
|
+
/** The position of the shaper. */
|
|
64
|
+
position: number;
|
|
65
|
+
/** The code point of the character of the glyph, after the normalization. */
|
|
66
|
+
readonly codePoint: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** One substitution that a lookup can do, with no context. */
|
|
70
|
+
export interface SubstitutionRule {
|
|
71
|
+
/** The glyph that the rule replaces. The first glyph of a ligature is this glyph. */
|
|
72
|
+
readonly from: number;
|
|
73
|
+
/** The glyphs that replace it. A multiple substitution can give no glyph. */
|
|
74
|
+
readonly to: readonly number[];
|
|
75
|
+
/** The other glyphs of a ligature, in order. Another rule has no such glyphs. */
|
|
76
|
+
readonly components?: readonly number[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A lookup of GSUB for one script and one set of features. */
|
|
80
|
+
export interface SubstitutionLookup {
|
|
81
|
+
/** The index of the lookup in the lookup list of GSUB. */
|
|
82
|
+
readonly index: number;
|
|
83
|
+
/** The lookup type, from 1 to 8. The type of an Extension lookup is the type of its subtables. */
|
|
84
|
+
readonly type: number;
|
|
85
|
+
/** The lookup flags. */
|
|
86
|
+
readonly flags: number;
|
|
87
|
+
/** The tags of the features that use the lookup. */
|
|
88
|
+
readonly features: readonly string[];
|
|
89
|
+
/**
|
|
90
|
+
* This function applies the lookup to all the glyphs of a text. It changes the array. The array `masks`, when it is
|
|
91
|
+
* given, has one mask for each glyph. Then the lookup starts only at a glyph whose mask has a bit of `mask`, as the
|
|
92
|
+
* feature masks of HarfBuzz do. The function keeps `masks` in step with the glyphs.
|
|
93
|
+
*/
|
|
94
|
+
apply(glyphs: number[], masks?: number[], mask?: number): void;
|
|
95
|
+
/**
|
|
96
|
+
* This function applies the lookup with the data `infos` of the glyphs, as HarfBuzz applies a lookup of a syllabic shaper.
|
|
97
|
+
* The lookup starts at a glyph with a bit of `mask`, and each input glyph after it also needs a bit of `mask`. When
|
|
98
|
+
* `perSyllable` is true, all the matched glyphs have the syllable of the first glyph. The function keeps `infos` in step.
|
|
99
|
+
*/
|
|
100
|
+
applyInfos(glyphs: number[], infos: GlyphInfo[], mask: number, perSyllable: boolean): void;
|
|
101
|
+
/**
|
|
102
|
+
* This function tells if the lookup changes the glyph sequence `glyphs` when the sequence is all the text, as
|
|
103
|
+
* `hb_ot_layout_lookup_would_substitute` with zero context tells it. A context rule with backtrack or lookahead does not apply.
|
|
104
|
+
*/
|
|
105
|
+
wouldApply(glyphs: readonly number[]): boolean;
|
|
106
|
+
/**
|
|
107
|
+
* This function gives the rules of the lookup, and the rules of each lookup that it nests in a context rule. Each lookup
|
|
108
|
+
* has one list, and the list of this lookup is the first. The lists do not tell a context, so each nested rule counts.
|
|
109
|
+
* An alternate substitution gives one rule for each alternate.
|
|
110
|
+
*/
|
|
111
|
+
rules(): readonly (readonly SubstitutionRule[])[];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** One lookup of the lookup list, with the subtables of an extension lookup resolved. */
|
|
115
|
+
interface Lookup {
|
|
116
|
+
readonly type: number;
|
|
117
|
+
readonly flags: number;
|
|
118
|
+
readonly subtables: readonly Reader[];
|
|
119
|
+
readonly skip: (glyph: number) => boolean;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** One SubstLookupRecord of a context rule: the input position and the lookup to apply there. */
|
|
123
|
+
interface SequenceRecord {
|
|
124
|
+
readonly sequenceIndex: number;
|
|
125
|
+
readonly lookupIndex: number;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function records(table: Reader, at: number, count: number): SequenceRecord[] {
|
|
129
|
+
return Array.from({ length: count }, (_, k) => ({ sequenceIndex: table.u16(at + k * 4), lookupIndex: table.u16(at + k * 4 + 2) }));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** This function gives each rule table of the rule sets at `at` of a context subtable of format 1 or 2. An offset 0 is an empty set. */
|
|
133
|
+
function ruleTables(t: Reader, at: number): Reader[] {
|
|
134
|
+
const tables: Reader[] = [];
|
|
135
|
+
for (let s = 0; s < t.u16(at); s++) {
|
|
136
|
+
const offset = t.u16(at + 2 + s * 2);
|
|
137
|
+
if (offset === 0) continue;
|
|
138
|
+
const set = t.part(offset);
|
|
139
|
+
for (let r = 0; r < set.u16(0); r++) tables.push(set.part(set.u16(2 + r * 2)));
|
|
140
|
+
}
|
|
141
|
+
return tables;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** This function gives the records of each rule of a context subtable (lookup type 5). */
|
|
145
|
+
function contextRecords(t: Reader): SequenceRecord[] {
|
|
146
|
+
const format = t.u16(0);
|
|
147
|
+
if (format === 3) return records(t, 6 + t.u16(2) * 2, t.u16(4));
|
|
148
|
+
return ruleTables(t, format === 2 ? 6 : 4).flatMap((rule) => records(rule, 4 + (rule.u16(0) - 1) * 2, rule.u16(2)));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** This function gives the records of each rule of a chained context subtable (lookup type 6). */
|
|
152
|
+
function chainRecords(t: Reader): SequenceRecord[] {
|
|
153
|
+
const format = t.u16(0);
|
|
154
|
+
if (format === 3) {
|
|
155
|
+
const inputAt = 4 + t.u16(2) * 2;
|
|
156
|
+
const aheadAt = inputAt + 2 + t.u16(inputAt) * 2;
|
|
157
|
+
const recordsAt = aheadAt + 2 + t.u16(aheadAt) * 2;
|
|
158
|
+
return records(t, recordsAt + 2, t.u16(recordsAt));
|
|
159
|
+
}
|
|
160
|
+
return ruleTables(t, format === 2 ? 10 : 4).flatMap((rule) => {
|
|
161
|
+
const inputAt = 2 + rule.u16(0) * 2;
|
|
162
|
+
const aheadAt = inputAt + 2 + Math.max(0, rule.u16(inputAt) - 1) * 2;
|
|
163
|
+
const recordsAt = aheadAt + 2 + rule.u16(aheadAt) * 2;
|
|
164
|
+
return records(rule, recordsAt + 2, rule.u16(recordsAt));
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** This function adds the rules of one subtable of a lookup type to `rules`, and each nested lookup to `nested`. */
|
|
169
|
+
function readRules(type: number, t: Reader, rules: SubstitutionRule[], nested: number[]): void {
|
|
170
|
+
if (type === 5 || type === 6) {
|
|
171
|
+
for (const record of type === 5 ? contextRecords(t) : chainRecords(t)) nested.push(record.lookupIndex);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (type < 1 || type > 8 || type === 7) return;
|
|
175
|
+
const format = t.u16(0);
|
|
176
|
+
for (const [from, index] of coverageGlyphs(t.part(t.u16(2)))) {
|
|
177
|
+
if (type === 1) rules.push({ from, to: [format === 1 ? (from + t.i16(4)) & 0xffff : t.u16(6 + index * 2)] });
|
|
178
|
+
else if (type === 2) {
|
|
179
|
+
const sequence = t.part(t.u16(6 + index * 2));
|
|
180
|
+
rules.push({ from, to: Array.from({ length: sequence.u16(0) }, (_, k) => sequence.u16(2 + k * 2)) });
|
|
181
|
+
} else if (type === 3) {
|
|
182
|
+
const set = t.part(t.u16(6 + index * 2));
|
|
183
|
+
for (let k = 0; k < set.u16(0); k++) rules.push({ from, to: [set.u16(2 + k * 2)] });
|
|
184
|
+
} else if (type === 4) {
|
|
185
|
+
const set = t.part(t.u16(6 + index * 2));
|
|
186
|
+
for (let l = 0; l < set.u16(0); l++) {
|
|
187
|
+
const ligature = set.part(set.u16(2 + l * 2));
|
|
188
|
+
rules.push({ from, to: [ligature.u16(0)], components: Array.from({ length: ligature.u16(2) - 1 }, (_, k) => ligature.u16(4 + k * 2)) });
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
const aheadAt = 6 + t.u16(4) * 2;
|
|
192
|
+
rules.push({ from, to: [t.u16(aheadAt + 2 + t.u16(aheadAt) * 2 + 2 + index * 2)] });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** The lookups of one GSUB table. It reads each lookup at its first use. */
|
|
198
|
+
class Gsub {
|
|
199
|
+
private readonly lookups: Reader;
|
|
200
|
+
private readonly filter: GlyphFilter;
|
|
201
|
+
private readonly cache = new Map<number, Lookup>();
|
|
202
|
+
|
|
203
|
+
constructor(table: Reader, filter: GlyphFilter) {
|
|
204
|
+
this.lookups = table.part(table.u16(8));
|
|
205
|
+
this.filter = filter;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
lookup(index: number): Lookup | undefined {
|
|
209
|
+
if (index >= this.lookups.u16(0)) return undefined;
|
|
210
|
+
let found = this.cache.get(index);
|
|
211
|
+
if (found === undefined) {
|
|
212
|
+
const table = this.lookups.part(this.lookups.u16(2 + index * 2));
|
|
213
|
+
const type = table.u16(0);
|
|
214
|
+
const flags = table.u16(2);
|
|
215
|
+
const count = table.u16(4);
|
|
216
|
+
const markSet = (flags & USE_MARK_FILTERING_SET) !== 0 ? table.u16(6 + count * 2) : -1;
|
|
217
|
+
const subtables: Reader[] = [];
|
|
218
|
+
let subtype = type;
|
|
219
|
+
for (let s = 0; s < count; s++) {
|
|
220
|
+
const subtable = table.part(table.u16(6 + s * 2));
|
|
221
|
+
if (type === 7) {
|
|
222
|
+
subtype = subtable.u16(2);
|
|
223
|
+
subtables.push(subtable.part(subtable.u32(4)));
|
|
224
|
+
} else subtables.push(subtable);
|
|
225
|
+
}
|
|
226
|
+
const { glyphClass, markAttachClass, inMarkSet } = this.filter;
|
|
227
|
+
const skip = (glyph: number): boolean => {
|
|
228
|
+
const kind = glyphClass(glyph);
|
|
229
|
+
if ((flags & IGNORE_BASE) !== 0 && kind === 1) return true;
|
|
230
|
+
if ((flags & IGNORE_LIGATURES) !== 0 && kind === 2) return true;
|
|
231
|
+
if (kind !== 3) return false;
|
|
232
|
+
if ((flags & IGNORE_MARKS) !== 0) return true;
|
|
233
|
+
if (markSet >= 0) return !inMarkSet(markSet, glyph);
|
|
234
|
+
const attach = flags >> 8;
|
|
235
|
+
return attach !== 0 && markAttachClass(glyph) !== attach;
|
|
236
|
+
};
|
|
237
|
+
found = { type: subtype, flags, subtables, skip };
|
|
238
|
+
this.cache.set(index, found);
|
|
239
|
+
}
|
|
240
|
+
return found;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* The masks of the glyphs of the current call of `applyAll`, or `undefined`. A substitution that adds or removes glyphs
|
|
245
|
+
* changes this array in the same way.
|
|
246
|
+
*/
|
|
247
|
+
private masks: number[] | undefined;
|
|
248
|
+
|
|
249
|
+
/** The data of the glyphs of the current call of `applyRun`, or `undefined`. */
|
|
250
|
+
private infos: GlyphInfo[] | undefined;
|
|
251
|
+
/** The mask of the current call of `applyRun`. Each input glyph needs a bit of it. */
|
|
252
|
+
private lookupMask = -1;
|
|
253
|
+
/** It is true when the matched glyphs of the current call of `applyRun` need the syllable of the first glyph. */
|
|
254
|
+
private perSyllable = false;
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* This function replaces `count` glyphs at `at` with `replacement`. Each new glyph gets the mask and the data of the
|
|
258
|
+
* glyph at `at`.
|
|
259
|
+
*/
|
|
260
|
+
private splice(glyphs: number[], at: number, count: number, replacement: readonly number[]): void {
|
|
261
|
+
glyphs.splice(at, count, ...replacement);
|
|
262
|
+
const masks = this.masks;
|
|
263
|
+
if (masks !== undefined) masks.splice(at, count, ...replacement.map(() => masks[at] ?? 0));
|
|
264
|
+
const infos = this.infos;
|
|
265
|
+
if (infos !== undefined) {
|
|
266
|
+
const source = infos[at]!;
|
|
267
|
+
infos.splice(at, count, ...replacement.map(() => ({ ...source })));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** This function adds the glyph properties `set` to the glyph at `i`, and then removes the properties `clear`. */
|
|
272
|
+
private setProps(i: number, set: number, clear = 0): void {
|
|
273
|
+
const info = this.infos?.[i];
|
|
274
|
+
if (info !== undefined) info.props = (info.props | set) & ~clear;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* This function tells if the glyph at `j` can match for the glyph at `first`. An input glyph needs a bit of the mask of
|
|
279
|
+
* the lookup. With the per-syllable matching, each glyph needs the syllable of `first`.
|
|
280
|
+
*/
|
|
281
|
+
private admits(j: number, first: number, input: boolean): boolean {
|
|
282
|
+
const infos = this.infos;
|
|
283
|
+
if (infos === undefined) return true;
|
|
284
|
+
if (input && (infos[j]!.mask & this.lookupMask) === 0) return false;
|
|
285
|
+
const syllable = infos[first]?.syllable ?? 0;
|
|
286
|
+
return !this.perSyllable || syllable === 0 || infos[j]!.syllable === syllable;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** This function applies a lookup to all the glyphs with their data `infos`, as `applyInfos` of `SubstitutionLookup` tells. */
|
|
290
|
+
applyRun(index: number, glyphs: number[], infos: GlyphInfo[], mask: number, perSyllable: boolean): void {
|
|
291
|
+
const lookup = this.lookup(index);
|
|
292
|
+
if (lookup === undefined) return;
|
|
293
|
+
this.infos = infos;
|
|
294
|
+
this.lookupMask = mask;
|
|
295
|
+
this.perSyllable = perSyllable;
|
|
296
|
+
try {
|
|
297
|
+
this.applyMasked(lookup, glyphs, (i) => (infos[i]!.mask & mask) !== 0);
|
|
298
|
+
} finally {
|
|
299
|
+
this.infos = undefined;
|
|
300
|
+
this.lookupMask = -1;
|
|
301
|
+
this.perSyllable = false;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* This function applies a lookup to all the glyphs, as the main loop of a shaper does. With `masks`, the lookup starts
|
|
307
|
+
* only at a glyph whose mask has a bit of `mask`.
|
|
308
|
+
*/
|
|
309
|
+
applyAll(index: number, glyphs: number[], masks?: number[], mask = -1): void {
|
|
310
|
+
const lookup = this.lookup(index);
|
|
311
|
+
if (lookup === undefined) return;
|
|
312
|
+
this.masks = masks;
|
|
313
|
+
try {
|
|
314
|
+
this.applyMasked(lookup, glyphs, (i) => masks === undefined || ((masks[i] ?? 0) & mask) !== 0);
|
|
315
|
+
} finally {
|
|
316
|
+
this.masks = undefined;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private applyMasked(lookup: Lookup, glyphs: number[], selected: (i: number) => boolean): void {
|
|
321
|
+
if (lookup.type === 8) {
|
|
322
|
+
for (let i = glyphs.length - 1; i >= 0; i--) if (selected(i) && !lookup.skip(glyphs[i]!)) this.reverse(lookup, glyphs, i);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
let i = 0;
|
|
326
|
+
while (i < glyphs.length) {
|
|
327
|
+
if (!selected(i) || lookup.skip(glyphs[i]!)) {
|
|
328
|
+
i++;
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
const before = glyphs.length;
|
|
332
|
+
const next = this.applyAt(lookup, glyphs, i, 0);
|
|
333
|
+
// A deletion (a Sequence with no glyph) gives the same position, and the text is shorter
|
|
334
|
+
i = next < 0 ? i + 1 : next > i || glyphs.length < before ? next : i + 1;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** This function applies the first subtable of a lookup that matches at `i`. It gives the next position, or -1. */
|
|
339
|
+
private applyAt(lookup: Lookup, glyphs: number[], i: number, depth: number): number {
|
|
340
|
+
for (const subtable of lookup.subtables) {
|
|
341
|
+
const next = this.subtable(lookup, subtable, glyphs, i, depth);
|
|
342
|
+
if (next >= 0) return next;
|
|
343
|
+
}
|
|
344
|
+
return -1;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private nextOf(lookup: Lookup, glyphs: readonly number[], from: number): number {
|
|
348
|
+
for (let j = from; j < glyphs.length; j++) if (!lookup.skip(glyphs[j]!)) return j;
|
|
349
|
+
return -1;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private previousOf(lookup: Lookup, glyphs: readonly number[], from: number): number {
|
|
353
|
+
for (let j = from; j >= 0; j--) if (!lookup.skip(glyphs[j]!)) return j;
|
|
354
|
+
return -1;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* This function matches `count` glyphs after the position `i` with a test for each. It gives the positions of the
|
|
359
|
+
* matched glyphs, the first glyph at `i` included, or `undefined`.
|
|
360
|
+
*/
|
|
361
|
+
private matchInput(lookup: Lookup, glyphs: readonly number[], i: number, count: number, test: (k: number, glyph: number) => boolean): number[] | undefined {
|
|
362
|
+
const positions = [i];
|
|
363
|
+
let at = i;
|
|
364
|
+
for (let k = 0; k < count; k++) {
|
|
365
|
+
at = this.nextOf(lookup, glyphs, at + 1);
|
|
366
|
+
if (at < 0 || !this.admits(at, i, true) || !test(k, glyphs[at]!)) return undefined;
|
|
367
|
+
positions.push(at);
|
|
368
|
+
}
|
|
369
|
+
return positions;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private matchBacktrack(lookup: Lookup, glyphs: readonly number[], i: number, count: number, test: (k: number, glyph: number) => boolean): boolean {
|
|
373
|
+
let at = i;
|
|
374
|
+
for (let k = 0; k < count; k++) {
|
|
375
|
+
at = this.previousOf(lookup, glyphs, at - 1);
|
|
376
|
+
if (at < 0 || !this.admits(at, i, false) || !test(k, glyphs[at]!)) return false;
|
|
377
|
+
}
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private matchLookahead(lookup: Lookup, glyphs: readonly number[], first: number, last: number, count: number, test: (k: number, glyph: number) => boolean): boolean {
|
|
382
|
+
let at = last;
|
|
383
|
+
for (let k = 0; k < count; k++) {
|
|
384
|
+
at = this.nextOf(lookup, glyphs, at + 1);
|
|
385
|
+
if (at < 0 || !this.admits(at, first, false) || !test(k, glyphs[at]!)) return false;
|
|
386
|
+
}
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** This function applies the lookups of the records of a context rule at the matched positions. It gives the next position. */
|
|
391
|
+
private applyRecords(glyphs: number[], positions: number[], list: readonly SequenceRecord[], depth: number): number {
|
|
392
|
+
if (depth < MAX_NESTING) {
|
|
393
|
+
for (const record of list) {
|
|
394
|
+
const at = positions[record.sequenceIndex];
|
|
395
|
+
const nested = this.lookup(record.lookupIndex);
|
|
396
|
+
if (at === undefined || at >= glyphs.length || nested === undefined || nested.skip(glyphs[at]!)) continue;
|
|
397
|
+
const before = glyphs.length;
|
|
398
|
+
if (nested.type === 8) this.reverse(nested, glyphs, at);
|
|
399
|
+
else this.applyAt(nested, glyphs, at, depth + 1);
|
|
400
|
+
const delta = glyphs.length - before;
|
|
401
|
+
if (delta > 0) {
|
|
402
|
+
for (let k = record.sequenceIndex + 1; k < positions.length; k++) positions[k] = positions[k]! + delta;
|
|
403
|
+
positions.splice(record.sequenceIndex + 1, 0, ...Array.from({ length: delta }, (_, k) => at + 1 + k));
|
|
404
|
+
} else if (delta < 0) {
|
|
405
|
+
// A ligature took the next input glyphs: remove their positions and move the others
|
|
406
|
+
positions.splice(record.sequenceIndex + 1, Math.min(-delta, positions.length - record.sequenceIndex - 1));
|
|
407
|
+
for (let k = record.sequenceIndex + 1; k < positions.length; k++) positions[k] = positions[k]! + delta;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return Math.min(glyphs.length, positions[positions.length - 1]! + 1);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private reverse(lookup: Lookup, glyphs: number[], i: number): boolean {
|
|
415
|
+
for (const t of lookup.subtables) {
|
|
416
|
+
const index = coverageIndex(t.part(t.u16(2)), glyphs[i]!);
|
|
417
|
+
if (index < 0) continue;
|
|
418
|
+
const backCount = t.u16(4);
|
|
419
|
+
const aheadAt = 6 + backCount * 2;
|
|
420
|
+
const aheadCount = t.u16(aheadAt);
|
|
421
|
+
const substituteAt = aheadAt + 2 + aheadCount * 2;
|
|
422
|
+
if (!this.matchBacktrack(lookup, glyphs, i, backCount, (k, g) => coverageIndex(t.part(t.u16(6 + k * 2)), g) >= 0)) continue;
|
|
423
|
+
if (!this.matchLookahead(lookup, glyphs, i, i, aheadCount, (k, g) => coverageIndex(t.part(t.u16(aheadAt + 2 + k * 2)), g) >= 0)) continue;
|
|
424
|
+
glyphs[i] = t.u16(substituteAt + 2 + index * 2);
|
|
425
|
+
this.setProps(i, SUBSTITUTED);
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** This function tells if a lookup changes the sequence `glyphs` with no context, as `wouldApply` of `SubstitutionLookup` tells. */
|
|
432
|
+
wouldApply(index: number, glyphs: readonly number[]): boolean {
|
|
433
|
+
const lookup = this.lookup(index);
|
|
434
|
+
if (lookup === undefined || glyphs.length === 0) return false;
|
|
435
|
+
const [first] = glyphs as [number];
|
|
436
|
+
return lookup.subtables.some((t) => {
|
|
437
|
+
const format = t.u16(0);
|
|
438
|
+
if (lookup.type === 1 || lookup.type === 2 || lookup.type === 3 || lookup.type === 8) return glyphs.length === 1 && coverageIndex(t.part(t.u16(2)), first) >= 0;
|
|
439
|
+
if (lookup.type === 4) {
|
|
440
|
+
const index = coverageIndex(t.part(t.u16(2)), first);
|
|
441
|
+
if (index < 0) return false;
|
|
442
|
+
const set = t.part(t.u16(6 + index * 2));
|
|
443
|
+
return Array.from({ length: set.u16(0) }, (_, l) => set.part(set.u16(2 + l * 2))).some((ligature) => ligature.u16(2) === glyphs.length && glyphs.every((g, k) => k === 0 || ligature.u16(4 + (k - 1) * 2) === g));
|
|
444
|
+
}
|
|
445
|
+
if (lookup.type !== 5 && lookup.type !== 6) return false;
|
|
446
|
+
const chain = lookup.type === 6;
|
|
447
|
+
if (format === 3) {
|
|
448
|
+
const backCount = chain ? t.u16(2) : 0;
|
|
449
|
+
const inputAt = chain ? 4 + backCount * 2 : 2;
|
|
450
|
+
const count = t.u16(inputAt);
|
|
451
|
+
const coverageAt = chain ? inputAt + 2 : 6;
|
|
452
|
+
if (chain && (backCount !== 0 || t.u16(inputAt + 2 + count * 2) !== 0)) return false;
|
|
453
|
+
return count === glyphs.length && glyphs.every((g, k) => coverageIndex(t.part(t.u16(coverageAt + k * 2)), g) >= 0);
|
|
454
|
+
}
|
|
455
|
+
const index = coverageIndex(t.part(t.u16(2)), first);
|
|
456
|
+
if (index < 0) return false;
|
|
457
|
+
const classes = format === 2 ? t.part(t.u16(chain ? 6 : 4)) : undefined;
|
|
458
|
+
const value = (g: number) => (classes === undefined ? g : classOf(classes, g));
|
|
459
|
+
const setIndex = classes === undefined ? index : value(first);
|
|
460
|
+
const setCountAt = chain ? (format === 2 ? 10 : 4) : format === 2 ? 6 : 4;
|
|
461
|
+
if (setIndex >= t.u16(setCountAt) || t.u16(setCountAt + 2 + setIndex * 2) === 0) return false;
|
|
462
|
+
const set = t.part(t.u16(setCountAt + 2 + setIndex * 2));
|
|
463
|
+
for (let r = 0; r < set.u16(0); r++) {
|
|
464
|
+
const rule = set.part(set.u16(2 + r * 2));
|
|
465
|
+
const backCount = chain ? rule.u16(0) : 0;
|
|
466
|
+
const inputAt = chain ? 2 + backCount * 2 : 0;
|
|
467
|
+
const count = rule.u16(inputAt);
|
|
468
|
+
const inputStart = chain ? inputAt + 2 : 4;
|
|
469
|
+
if (chain && (backCount !== 0 || rule.u16(inputStart + Math.max(0, count - 1) * 2) !== 0)) continue;
|
|
470
|
+
if (count === glyphs.length && glyphs.every((g, k) => k === 0 || value(g) === rule.u16(inputStart + (k - 1) * 2))) return true;
|
|
471
|
+
}
|
|
472
|
+
return false;
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** This function gives the rules of a lookup and of each lookup that it nests, as `rules` of `SubstitutionLookup` tells. */
|
|
477
|
+
rulesOf(index: number): SubstitutionRule[][] {
|
|
478
|
+
const lists: SubstitutionRule[][] = [];
|
|
479
|
+
const seen = new Set<number>();
|
|
480
|
+
const visit = (at: number): void => {
|
|
481
|
+
const lookup = this.lookup(at);
|
|
482
|
+
if (seen.has(at) || lookup === undefined) return;
|
|
483
|
+
seen.add(at);
|
|
484
|
+
const rules: SubstitutionRule[] = [];
|
|
485
|
+
const nested: number[] = [];
|
|
486
|
+
for (const subtable of lookup.subtables) readRules(lookup.type, subtable, rules, nested);
|
|
487
|
+
lists.push(rules);
|
|
488
|
+
nested.forEach(visit);
|
|
489
|
+
};
|
|
490
|
+
visit(index);
|
|
491
|
+
return lists;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** This function applies one subtable at `i`. It gives the next position, or -1 when the subtable does not match. */
|
|
495
|
+
private subtable(lookup: Lookup, t: Reader, glyphs: number[], i: number, depth: number): number {
|
|
496
|
+
const glyph = glyphs[i]!;
|
|
497
|
+
const format = t.u16(0);
|
|
498
|
+
switch (lookup.type) {
|
|
499
|
+
case 1: {
|
|
500
|
+
const index = coverageIndex(t.part(t.u16(2)), glyph);
|
|
501
|
+
if (index < 0) return -1;
|
|
502
|
+
glyphs[i] = format === 1 ? (glyph + t.i16(4)) & 0xffff : t.u16(6 + index * 2);
|
|
503
|
+
this.setProps(i, SUBSTITUTED);
|
|
504
|
+
return i + 1;
|
|
505
|
+
}
|
|
506
|
+
case 2: {
|
|
507
|
+
const index = coverageIndex(t.part(t.u16(2)), glyph);
|
|
508
|
+
if (index < 0) return -1;
|
|
509
|
+
const sequence = t.part(t.u16(6 + index * 2));
|
|
510
|
+
const replacement = Array.from({ length: sequence.u16(0) }, (_, k) => sequence.u16(2 + k * 2));
|
|
511
|
+
this.splice(glyphs, i, 1, replacement);
|
|
512
|
+
for (let k = 0; k < replacement.length; k++) this.setProps(i + k, replacement.length > 1 ? SUBSTITUTED | MULTIPLIED : SUBSTITUTED);
|
|
513
|
+
return i + replacement.length;
|
|
514
|
+
}
|
|
515
|
+
case 3: {
|
|
516
|
+
const index = coverageIndex(t.part(t.u16(2)), glyph);
|
|
517
|
+
if (index < 0) return -1;
|
|
518
|
+
const set = t.part(t.u16(6 + index * 2));
|
|
519
|
+
if (set.u16(0) > 0) glyphs[i] = set.u16(2);
|
|
520
|
+
this.setProps(i, SUBSTITUTED);
|
|
521
|
+
return i + 1;
|
|
522
|
+
}
|
|
523
|
+
case 4: {
|
|
524
|
+
const index = coverageIndex(t.part(t.u16(2)), glyph);
|
|
525
|
+
if (index < 0) return -1;
|
|
526
|
+
const set = t.part(t.u16(6 + index * 2));
|
|
527
|
+
for (let l = 0; l < set.u16(0); l++) {
|
|
528
|
+
const ligature = set.part(set.u16(2 + l * 2));
|
|
529
|
+
const components = ligature.u16(2);
|
|
530
|
+
const positions = this.matchInput(lookup, glyphs, i, components - 1, (k, g) => g === ligature.u16(4 + k * 2));
|
|
531
|
+
if (positions === undefined) continue;
|
|
532
|
+
glyphs[i] = ligature.u16(0);
|
|
533
|
+
// A ligature is not a multiplied glyph, even when one of its glyphs was
|
|
534
|
+
this.setProps(i, SUBSTITUTED | LIGATED, MULTIPLIED);
|
|
535
|
+
for (let k = positions.length - 1; k >= 1; k--) this.splice(glyphs, positions[k]!, 1, []);
|
|
536
|
+
return i + 1;
|
|
537
|
+
}
|
|
538
|
+
return -1;
|
|
539
|
+
}
|
|
540
|
+
case 5:
|
|
541
|
+
return this.context(lookup, t, glyphs, i, depth);
|
|
542
|
+
case 6:
|
|
543
|
+
return this.chainContext(lookup, t, glyphs, i, depth);
|
|
544
|
+
case 8:
|
|
545
|
+
return this.reverse(lookup, glyphs, i) ? i + 1 : -1;
|
|
546
|
+
default:
|
|
547
|
+
return -1;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
private context(lookup: Lookup, t: Reader, glyphs: number[], i: number, depth: number): number {
|
|
552
|
+
const glyph = glyphs[i]!;
|
|
553
|
+
const format = t.u16(0);
|
|
554
|
+
if (format === 3) {
|
|
555
|
+
const count = t.u16(2);
|
|
556
|
+
const recordCount = t.u16(4);
|
|
557
|
+
if (count === 0 || coverageIndex(t.part(t.u16(6)), glyph) < 0) return -1;
|
|
558
|
+
const positions = this.matchInput(lookup, glyphs, i, count - 1, (k, g) => coverageIndex(t.part(t.u16(8 + k * 2)), g) >= 0);
|
|
559
|
+
if (positions === undefined) return -1;
|
|
560
|
+
return this.applyRecords(glyphs, positions, records(t, 6 + count * 2, recordCount), depth);
|
|
561
|
+
}
|
|
562
|
+
const index = coverageIndex(t.part(t.u16(2)), glyph);
|
|
563
|
+
if (index < 0) return -1;
|
|
564
|
+
const classes = format === 2 ? t.part(t.u16(4)) : undefined;
|
|
565
|
+
const setIndex = classes === undefined ? index : classOf(classes, glyph);
|
|
566
|
+
const setCount = t.u16(classes === undefined ? 4 : 6);
|
|
567
|
+
if (setIndex >= setCount) return -1;
|
|
568
|
+
const setOffset = t.u16((classes === undefined ? 6 : 8) + setIndex * 2);
|
|
569
|
+
if (setOffset === 0) return -1;
|
|
570
|
+
const set = t.part(setOffset);
|
|
571
|
+
const value = (g: number) => (classes === undefined ? g : classOf(classes, g));
|
|
572
|
+
for (let r = 0; r < set.u16(0); r++) {
|
|
573
|
+
const rule = set.part(set.u16(2 + r * 2));
|
|
574
|
+
const count = rule.u16(0);
|
|
575
|
+
const recordCount = rule.u16(2);
|
|
576
|
+
const positions = this.matchInput(lookup, glyphs, i, count - 1, (k, g) => value(g) === rule.u16(4 + k * 2));
|
|
577
|
+
if (positions === undefined) continue;
|
|
578
|
+
return this.applyRecords(glyphs, positions, records(rule, 4 + (count - 1) * 2, recordCount), depth);
|
|
579
|
+
}
|
|
580
|
+
return -1;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
private chainContext(lookup: Lookup, t: Reader, glyphs: number[], i: number, depth: number): number {
|
|
584
|
+
const glyph = glyphs[i]!;
|
|
585
|
+
const format = t.u16(0);
|
|
586
|
+
if (format === 3) {
|
|
587
|
+
const backCount = t.u16(2);
|
|
588
|
+
const inputAt = 4 + backCount * 2;
|
|
589
|
+
const inputCount = t.u16(inputAt);
|
|
590
|
+
const aheadAt = inputAt + 2 + inputCount * 2;
|
|
591
|
+
const aheadCount = t.u16(aheadAt);
|
|
592
|
+
const recordsAt = aheadAt + 2 + aheadCount * 2;
|
|
593
|
+
const covers = (offset: number, g: number) => coverageIndex(t.part(t.u16(offset)), g) >= 0;
|
|
594
|
+
if (inputCount === 0 || !covers(inputAt + 2, glyph)) return -1;
|
|
595
|
+
if (!this.matchBacktrack(lookup, glyphs, i, backCount, (k, g) => covers(4 + k * 2, g))) return -1;
|
|
596
|
+
const positions = this.matchInput(lookup, glyphs, i, inputCount - 1, (k, g) => covers(inputAt + 4 + k * 2, g));
|
|
597
|
+
if (positions === undefined) return -1;
|
|
598
|
+
if (!this.matchLookahead(lookup, glyphs, i, positions[positions.length - 1]!, aheadCount, (k, g) => covers(aheadAt + 2 + k * 2, g))) return -1;
|
|
599
|
+
return this.applyRecords(glyphs, positions, records(t, recordsAt + 2, t.u16(recordsAt)), depth);
|
|
600
|
+
}
|
|
601
|
+
const index = coverageIndex(t.part(t.u16(2)), glyph);
|
|
602
|
+
if (index < 0) return -1;
|
|
603
|
+
const byClass = format === 2;
|
|
604
|
+
const backClasses = byClass ? t.part(t.u16(4)) : undefined;
|
|
605
|
+
const inputClasses = byClass ? t.part(t.u16(6)) : undefined;
|
|
606
|
+
const aheadClasses = byClass ? t.part(t.u16(8)) : undefined;
|
|
607
|
+
const setIndex = inputClasses === undefined ? index : classOf(inputClasses, glyph);
|
|
608
|
+
const setCount = t.u16(byClass ? 10 : 4);
|
|
609
|
+
if (setIndex >= setCount) return -1;
|
|
610
|
+
const setOffset = t.u16((byClass ? 12 : 6) + setIndex * 2);
|
|
611
|
+
if (setOffset === 0) return -1;
|
|
612
|
+
const set = t.part(setOffset);
|
|
613
|
+
const valueIn = (classes: Reader | undefined, g: number) => (classes === undefined ? g : classOf(classes, g));
|
|
614
|
+
for (let r = 0; r < set.u16(0); r++) {
|
|
615
|
+
const rule = set.part(set.u16(2 + r * 2));
|
|
616
|
+
const backCount = rule.u16(0);
|
|
617
|
+
const inputAt = 2 + backCount * 2;
|
|
618
|
+
const inputCount = rule.u16(inputAt);
|
|
619
|
+
const aheadAt = inputAt + 2 + Math.max(0, inputCount - 1) * 2;
|
|
620
|
+
const aheadCount = rule.u16(aheadAt);
|
|
621
|
+
const recordsAt = aheadAt + 2 + aheadCount * 2;
|
|
622
|
+
if (!this.matchBacktrack(lookup, glyphs, i, backCount, (k, g) => valueIn(backClasses, g) === rule.u16(2 + k * 2))) continue;
|
|
623
|
+
const positions = this.matchInput(lookup, glyphs, i, inputCount - 1, (k, g) => valueIn(inputClasses, g) === rule.u16(inputAt + 2 + k * 2));
|
|
624
|
+
if (positions === undefined) continue;
|
|
625
|
+
if (!this.matchLookahead(lookup, glyphs, i, positions[positions.length - 1]!, aheadCount, (k, g) => valueIn(aheadClasses, g) === rule.u16(aheadAt + 2 + k * 2))) continue;
|
|
626
|
+
return this.applyRecords(glyphs, positions, records(rule, recordsAt + 2, rule.u16(recordsAt)), depth);
|
|
627
|
+
}
|
|
628
|
+
return -1;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* This function gives the lookups of the features `features` of GSUB for a script, in the order of the lookup list, as
|
|
634
|
+
* HarfBuzz applies them. The lookups include the lookups of the necessary feature of the language system.
|
|
635
|
+
*/
|
|
636
|
+
export function readSubstitutions(gsub: Reader, script: string, features: readonly string[], filter: GlyphFilter): SubstitutionLookup[] {
|
|
637
|
+
const scripts = gsub.part(gsub.u16(4));
|
|
638
|
+
const featureList = gsub.part(gsub.u16(6));
|
|
639
|
+
const scriptCount = scripts.u16(0);
|
|
640
|
+
let langSys: Reader | undefined;
|
|
641
|
+
for (const wanted of scriptTags(script)) {
|
|
642
|
+
for (let i = 0; i < scriptCount && langSys === undefined; i++) {
|
|
643
|
+
if (scripts.tag(2 + i * 6) !== wanted) continue;
|
|
644
|
+
const table = scripts.part(scripts.u16(2 + i * 6 + 4));
|
|
645
|
+
const fallback = table.u16(0);
|
|
646
|
+
if (fallback !== 0) langSys = table.part(fallback);
|
|
647
|
+
}
|
|
648
|
+
if (langSys !== undefined) break;
|
|
649
|
+
}
|
|
650
|
+
if (langSys === undefined || features.length === 0) return [];
|
|
651
|
+
const wanted = new Set(features);
|
|
652
|
+
const indices = new Map<number, string[]>();
|
|
653
|
+
const add = (featureIndex: number, required: boolean) => {
|
|
654
|
+
const record = 2 + featureIndex * 6;
|
|
655
|
+
if (featureIndex >= featureList.u16(0)) return;
|
|
656
|
+
const tag = featureList.tag(record);
|
|
657
|
+
if (!required && !wanted.has(tag)) return;
|
|
658
|
+
const feature = featureList.part(featureList.u16(record + 4));
|
|
659
|
+
for (let k = 0; k < feature.u16(2); k++) {
|
|
660
|
+
const index = feature.u16(4 + k * 2);
|
|
661
|
+
const tags = indices.get(index) ?? [];
|
|
662
|
+
if (!tags.includes(tag)) tags.push(tag);
|
|
663
|
+
indices.set(index, tags);
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
const required = langSys.u16(2);
|
|
667
|
+
if (required !== 0xffff) add(required, true);
|
|
668
|
+
for (let i = 0; i < langSys.u16(4); i++) add(langSys.u16(6 + i * 2), false);
|
|
669
|
+
const table = new Gsub(gsub, filter);
|
|
670
|
+
const result: SubstitutionLookup[] = [];
|
|
671
|
+
for (const [index, tags] of [...indices].sort(([a], [b]) => a - b)) {
|
|
672
|
+
const lookup = table.lookup(index);
|
|
673
|
+
if (lookup === undefined) continue;
|
|
674
|
+
result.push({
|
|
675
|
+
index,
|
|
676
|
+
type: lookup.type,
|
|
677
|
+
flags: lookup.flags,
|
|
678
|
+
features: tags,
|
|
679
|
+
apply: (glyphs: number[], masks?: number[], mask?: number) => table.applyAll(index, glyphs, masks, mask),
|
|
680
|
+
applyInfos: (glyphs: number[], infos: GlyphInfo[], mask: number, perSyllable: boolean) => table.applyRun(index, glyphs, infos, mask, perSyllable),
|
|
681
|
+
wouldApply: (glyphs: readonly number[]) => table.wouldApply(index, glyphs),
|
|
682
|
+
rules: () => table.rulesOf(index),
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
return result;
|
|
686
|
+
}
|