@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.
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xterm/addon-ligatures",
|
|
3
|
-
"version": "0.11.0-beta.
|
|
3
|
+
"version": "0.11.0-beta.90",
|
|
4
4
|
"description": "Add support for programming ligatures to xterm.js",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "The xterm.js authors",
|
|
@@ -32,18 +32,19 @@
|
|
|
32
32
|
],
|
|
33
33
|
"license": "MIT",
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"
|
|
36
|
-
"
|
|
35
|
+
"lru-cache": "^6.0.0",
|
|
36
|
+
"opentype.js": "^0.8.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@types/
|
|
39
|
+
"@types/lru-cache": "^5.1.0",
|
|
40
|
+
"@types/opentype.js": "^0.7.0",
|
|
40
41
|
"axios": "^1.6.0",
|
|
42
|
+
"font-finder": "^1.1.0",
|
|
41
43
|
"mkdirp": "0.5.5",
|
|
42
|
-
"sinon": "6.3.5",
|
|
43
44
|
"yauzl": "^2.10.0"
|
|
44
45
|
},
|
|
45
|
-
"commit": "
|
|
46
|
+
"commit": "c97abced76fe3ca2191b86f6c03c1275b6d4e696",
|
|
46
47
|
"peerDependencies": {
|
|
47
|
-
"@xterm/xterm": "^6.1.0-beta.
|
|
48
|
+
"@xterm/xterm": "^6.1.0-beta.90"
|
|
48
49
|
}
|
|
49
50
|
}
|
package/src/font.ts
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ILookupTree, IFlattenedLookupTree, ILookupTreeEntry, IFlattenedLookupTreeEntry } from './types';
|
|
2
|
+
|
|
3
|
+
export default function flatten(tree: ILookupTree, visited: Map<ILookupTreeEntry, IFlattenedLookupTreeEntry> = new Map()): IFlattenedLookupTree {
|
|
4
|
+
const result: IFlattenedLookupTree = {};
|
|
5
|
+
for (const [glyphId, entry] of Object.entries(tree.individual)) {
|
|
6
|
+
result[glyphId] = flattenEntry(entry, visited);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
for (const { range, entry } of tree.range) {
|
|
10
|
+
const flattened = flattenEntry(entry, visited);
|
|
11
|
+
for (let glyphId = range[0]; glyphId < range[1]; glyphId++) {
|
|
12
|
+
result[glyphId] = flattened;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function flattenEntry(entry: ILookupTreeEntry, visited: Map<ILookupTreeEntry, IFlattenedLookupTreeEntry>): IFlattenedLookupTreeEntry {
|
|
20
|
+
if (visited.has(entry)) {
|
|
21
|
+
return visited.get(entry)!;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const result: IFlattenedLookupTreeEntry = {};
|
|
25
|
+
visited.set(entry, result);
|
|
26
|
+
|
|
27
|
+
if (entry.forward) {
|
|
28
|
+
result.forward = flatten(entry.forward, visited);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (entry.reverse) {
|
|
32
|
+
result.reverse = flatten(entry.reverse, visited);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (entry.lookup) {
|
|
36
|
+
result.lookup = entry.lookup;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import * as opentype from 'opentype.js';
|
|
2
|
+
import LRUCache = require('lru-cache');
|
|
3
|
+
|
|
4
|
+
import { IFont, ILigatureData, IFlattenedLookupTree, ILookupTree, IOptions } from './types';
|
|
5
|
+
import mergeTrees from './merge';
|
|
6
|
+
import walkTree from './walk';
|
|
7
|
+
import mergeRange from './mergeRange';
|
|
8
|
+
|
|
9
|
+
import buildTreeGsubType6Format1 from './processors/6-1';
|
|
10
|
+
import buildTreeGsubType6Format2 from './processors/6-2';
|
|
11
|
+
import buildTreeGsubType6Format3 from './processors/6-3';
|
|
12
|
+
import buildTreeGsubType8Format1 from './processors/8-1';
|
|
13
|
+
import flatten from './flatten';
|
|
14
|
+
|
|
15
|
+
class FontImpl implements IFont {
|
|
16
|
+
private _font: opentype.Font;
|
|
17
|
+
private _lookupTrees: { tree: IFlattenedLookupTree, processForward: boolean }[] = [];
|
|
18
|
+
private _glyphLookups: { [glyphId: string]: number[] } = {};
|
|
19
|
+
private _cache?: LRUCache<string, ILigatureData | [number, number][]>;
|
|
20
|
+
|
|
21
|
+
constructor(font: opentype.Font, options: Required<IOptions>) {
|
|
22
|
+
this._font = font;
|
|
23
|
+
|
|
24
|
+
if (options.cacheSize > 0) {
|
|
25
|
+
this._cache = new LRUCache({
|
|
26
|
+
max: options.cacheSize,
|
|
27
|
+
length: ((val: ILigatureData | [number, number][], key: string) => key.length) as any
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const caltFeatures = this._font.tables.gsub && this._font.tables.gsub.features.filter((f: { tag: string }) => f.tag === 'calt') || [];
|
|
32
|
+
const lookupIndices: number[] = caltFeatures
|
|
33
|
+
.reduce((acc: number[], val: { feature: { lookupListIndexes: number[] } }) => [...acc, ...val.feature.lookupListIndexes], []);
|
|
34
|
+
|
|
35
|
+
const allLookups = this._font.tables.gsub && this._font.tables.gsub.lookups || [];
|
|
36
|
+
const lookupGroups = allLookups.filter((l: unknown, i: number) => lookupIndices.some(idx => idx === i));
|
|
37
|
+
|
|
38
|
+
for (const [index, lookup] of lookupGroups.entries()) {
|
|
39
|
+
const trees: ILookupTree[] = [];
|
|
40
|
+
switch (lookup.lookupType) {
|
|
41
|
+
case 6:
|
|
42
|
+
for (const [index, table] of lookup.subtables.entries()) {
|
|
43
|
+
switch (table.substFormat) {
|
|
44
|
+
case 1:
|
|
45
|
+
trees.push(buildTreeGsubType6Format1(table, allLookups, index));
|
|
46
|
+
break;
|
|
47
|
+
case 2:
|
|
48
|
+
trees.push(buildTreeGsubType6Format2(table, allLookups, index));
|
|
49
|
+
break;
|
|
50
|
+
case 3:
|
|
51
|
+
trees.push(buildTreeGsubType6Format3(table, allLookups, index));
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
break;
|
|
56
|
+
case 8:
|
|
57
|
+
for (const [index, table] of lookup.subtables.entries()) {
|
|
58
|
+
trees.push(buildTreeGsubType8Format1(table, index));
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const tree = flatten(mergeTrees(trees));
|
|
64
|
+
|
|
65
|
+
this._lookupTrees.push({
|
|
66
|
+
tree,
|
|
67
|
+
processForward: lookup.lookupType !== 8
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
for (const glyphId of Object.keys(tree)) {
|
|
71
|
+
if (!this._glyphLookups[glyphId]) {
|
|
72
|
+
this._glyphLookups[glyphId] = [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this._glyphLookups[glyphId].push(index);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public findLigatures(text: string): ILigatureData {
|
|
81
|
+
const cached = this._cache && this._cache.get(text);
|
|
82
|
+
if (cached && !Array.isArray(cached)) {
|
|
83
|
+
return cached;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const glyphIds: number[] = [];
|
|
87
|
+
for (const char of text) {
|
|
88
|
+
glyphIds.push(this._font.charToGlyphIndex(char));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// If there are no lookup groups, there's no point looking for
|
|
92
|
+
// replacements. This gives us a minor performance boost for fonts with
|
|
93
|
+
// no ligatures
|
|
94
|
+
if (this._lookupTrees.length === 0) {
|
|
95
|
+
return {
|
|
96
|
+
inputGlyphs: glyphIds,
|
|
97
|
+
outputGlyphs: glyphIds,
|
|
98
|
+
contextRanges: []
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const result = this._findInternal(glyphIds.slice());
|
|
103
|
+
const finalResult: ILigatureData = {
|
|
104
|
+
inputGlyphs: glyphIds,
|
|
105
|
+
outputGlyphs: result.sequence,
|
|
106
|
+
contextRanges: result.ranges
|
|
107
|
+
};
|
|
108
|
+
if (this._cache) {
|
|
109
|
+
this._cache.set(text, finalResult);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return finalResult;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public findLigatureRanges(text: string): [number, number][] {
|
|
116
|
+
// Short circuit the process if there are no possible ligatures in the
|
|
117
|
+
// font
|
|
118
|
+
if (this._lookupTrees.length === 0) {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const cached = this._cache && this._cache.get(text);
|
|
123
|
+
if (cached) {
|
|
124
|
+
return Array.isArray(cached) ? cached : cached.contextRanges;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const glyphIds: number[] = [];
|
|
128
|
+
for (const char of text) {
|
|
129
|
+
glyphIds.push(this._font.charToGlyphIndex(char));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const result = this._findInternal(glyphIds);
|
|
133
|
+
if (this._cache) {
|
|
134
|
+
this._cache.set(text, result.ranges);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return result.ranges;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private _findInternal(sequence: number[]): { sequence: number[], ranges: [number, number][] } {
|
|
141
|
+
const ranges: [number, number][] = [];
|
|
142
|
+
|
|
143
|
+
let nextLookup = this._getNextLookup(sequence, 0);
|
|
144
|
+
while (nextLookup.index !== null) {
|
|
145
|
+
const lookup = this._lookupTrees[nextLookup.index];
|
|
146
|
+
if (lookup.processForward) {
|
|
147
|
+
let lastGlyphIndex = nextLookup.last;
|
|
148
|
+
for (let i = nextLookup.first; i < lastGlyphIndex; i++) {
|
|
149
|
+
const result = walkTree(lookup.tree, sequence, i, i);
|
|
150
|
+
if (result) {
|
|
151
|
+
for (let j = 0; j < result.substitutions.length; j++) {
|
|
152
|
+
const sub = result.substitutions[j];
|
|
153
|
+
if (sub !== null) {
|
|
154
|
+
sequence[i + j] = sub;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
mergeRange(
|
|
159
|
+
ranges,
|
|
160
|
+
result.contextRange[0] + i,
|
|
161
|
+
result.contextRange[1] + i
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// Substitutions can end up extending the search range
|
|
165
|
+
if (i + result.length >= lastGlyphIndex) {
|
|
166
|
+
lastGlyphIndex = i + result.length + 1;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
i += result.length - 1;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
// We don't need to do the lastGlyphIndex tracking here because
|
|
174
|
+
// reverse processing isn't allowed to replace more than one
|
|
175
|
+
// character at a time.
|
|
176
|
+
for (let i = nextLookup.last - 1; i >= nextLookup.first; i--) {
|
|
177
|
+
const result = walkTree(lookup.tree, sequence, i, i);
|
|
178
|
+
if (result) {
|
|
179
|
+
for (let j = 0; j < result.substitutions.length; j++) {
|
|
180
|
+
const sub = result.substitutions[j];
|
|
181
|
+
if (sub !== null) {
|
|
182
|
+
sequence[i + j] = sub;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
mergeRange(
|
|
187
|
+
ranges,
|
|
188
|
+
result.contextRange[0] + i,
|
|
189
|
+
result.contextRange[1] + i
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
i -= result.length - 1;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
nextLookup = this._getNextLookup(sequence, nextLookup.index + 1);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return { sequence, ranges };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Returns the lookup and glyph range for the first lookup that might
|
|
205
|
+
* contain a match.
|
|
206
|
+
*
|
|
207
|
+
* @param sequence Input glyph sequence
|
|
208
|
+
* @param start The first input to try
|
|
209
|
+
*/
|
|
210
|
+
private _getNextLookup(sequence: number[], start: number): { index: number | null, first: number, last: number } {
|
|
211
|
+
const result: { index: number | null, first: number, last: number } = {
|
|
212
|
+
index: null,
|
|
213
|
+
first: Infinity,
|
|
214
|
+
last: -1
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// Loop through each glyph and find the first valid lookup for it
|
|
218
|
+
for (let i = 0; i < sequence.length; i++) {
|
|
219
|
+
const lookups = this._glyphLookups[sequence[i]];
|
|
220
|
+
if (!lookups) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
for (let j = 0; j < lookups.length; j++) {
|
|
225
|
+
const lookupIndex = lookups[j];
|
|
226
|
+
if (lookupIndex >= start) {
|
|
227
|
+
// Update the lookup information if it's the one we're
|
|
228
|
+
// storing or earlier than it.
|
|
229
|
+
if (result.index === null || lookupIndex <= result.index) {
|
|
230
|
+
result.index = lookupIndex;
|
|
231
|
+
|
|
232
|
+
if (result.first > i) {
|
|
233
|
+
result.first = i;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
result.last = i + 1;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Load the font from it's binary data. The returned value can be used to find
|
|
250
|
+
* ligatures for the font.
|
|
251
|
+
*
|
|
252
|
+
* @param buffer ArrayBuffer of the font to load
|
|
253
|
+
*/
|
|
254
|
+
export function loadBuffer(buffer: ArrayBuffer, options?: IOptions): IFont {
|
|
255
|
+
const font = opentype.parse(buffer);
|
|
256
|
+
return new FontImpl(font, {
|
|
257
|
+
cacheSize: 0,
|
|
258
|
+
...options
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export { IFont as Font, ILigatureData as LigatureData, IOptions as Options };
|