mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge pull request #5569 from Tyriar/5568_font_ligatures
Merge font-ligatures package into addon-ligatures
This commit is contained in:
@@ -3,7 +3,6 @@ node_modules/
|
||||
coverage/
|
||||
|
||||
lib/
|
||||
fonts/
|
||||
|
||||
.env
|
||||
.vscode/
|
||||
|
||||
@@ -1,6 +1,30 @@
|
||||
Copyright (c) 2019, The xterm.js authors (https://github.com/xtermjs/xterm.js)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
The code that analyzes font ligatures is forked from https://github.com/princjef/font-ligatures with this license:
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018
|
||||
Copyright (c) 2018 Jeffrey Principe
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -32,11 +32,14 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"font-finder": "^1.1.0",
|
||||
"font-ligatures": "^1.4.1"
|
||||
"lru-cache": "^6.0.0",
|
||||
"opentype.js": "^0.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lru-cache": "^5.1.0",
|
||||
"@types/opentype.js": "^0.7.0",
|
||||
"axios": "^1.6.0",
|
||||
"font-finder": "^1.1.0",
|
||||
"mkdirp": "0.5.5",
|
||||
"yauzl": "^2.10.0"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Font, loadBuffer } from 'font-ligatures';
|
||||
import { Font, loadBuffer } from './fontLigatures/index';
|
||||
|
||||
import parse from './parse';
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { ILookupTree, IFlattenedLookupTree, ILookupTreeEntry, IFlattenedLookupTreeEntry } from './types';
|
||||
|
||||
export default function flatten(tree: ILookupTree): IFlattenedLookupTree {
|
||||
const result: IFlattenedLookupTree = {};
|
||||
for (const [glyphId, entry] of Object.entries(tree.individual)) {
|
||||
result[glyphId] = flattenEntry(entry);
|
||||
}
|
||||
|
||||
for (const { range, entry } of tree.range) {
|
||||
const flattened = flattenEntry(entry);
|
||||
for (let glyphId = range[0]; glyphId < range[1]; glyphId++) {
|
||||
result[glyphId] = flattened;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function flattenEntry(entry: ILookupTreeEntry): IFlattenedLookupTreeEntry {
|
||||
const result: IFlattenedLookupTreeEntry = {};
|
||||
|
||||
if (entry.forward) {
|
||||
result.forward = flatten(entry.forward);
|
||||
}
|
||||
|
||||
if (entry.reverse) {
|
||||
result.reverse = flatten(entry.reverse);
|
||||
}
|
||||
|
||||
if (entry.lookup) {
|
||||
result.lookup = entry.lookup;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { assert } from 'chai';
|
||||
import { loadBuffer } from './index';
|
||||
|
||||
interface IFont {
|
||||
findLigatures(text: string): { outputGlyphs: number[], contextRanges: [number, number][] };
|
||||
findLigatureRanges(text: string): [number, number][];
|
||||
}
|
||||
|
||||
interface ITestCase {
|
||||
font: string;
|
||||
input: string;
|
||||
glyphs: number[];
|
||||
ranges: [number, number][];
|
||||
}
|
||||
|
||||
const fira = (input: string, glyphs: number[], ranges: [number, number][]): ITestCase =>
|
||||
({ font: 'Fira Code', input, glyphs, ranges });
|
||||
|
||||
const iosevka = (input: string, glyphs: number[], ranges: [number, number][]): ITestCase =>
|
||||
({ font: 'Iosevka', input, glyphs, ranges });
|
||||
|
||||
const monoid = (input: string, glyphs: number[], ranges: [number, number][]): ITestCase =>
|
||||
({ font: 'Monoid', input, glyphs, ranges });
|
||||
|
||||
const ubuntu = (input: string, glyphs: number[], ranges: [number, number][]): ITestCase =>
|
||||
({ font: 'Ubuntu Mono', input, glyphs, ranges });
|
||||
|
||||
const firaCases: ITestCase[] = [
|
||||
fira('abc', [133, 145, 146], []),
|
||||
fira('.=', [1614, 1081], [[0, 2]]),
|
||||
fira('..=', [1614, 1614, 1083], [[0, 3]]),
|
||||
fira('.-', [1614, 1080], [[0, 2]]),
|
||||
fira(':=', [1614, 1055], [[0, 2]]),
|
||||
fira('=:=', [1614, 1614, 1483], [[0, 3]]),
|
||||
fira('=!=', [1614, 1614, 1484], [[0, 3]]),
|
||||
fira('__', [1614, 1099], [[0, 2]]),
|
||||
fira('==', [1614, 1485], [[0, 2]]),
|
||||
fira('!=', [1614, 1058], [[0, 2]]),
|
||||
fira('===', [1614, 1614, 1486], [[0, 3]]),
|
||||
fira('!==', [1614, 1614, 1059], [[0, 3]]),
|
||||
fira('=/=', [1614, 1614, 1491], [[0, 3]]),
|
||||
fira('<-<', [1614, 1614, 1513], [[0, 3]]),
|
||||
fira('<<-', [1614, 1614, 1522], [[0, 3]]),
|
||||
fira('<--', [1614, 1614, 1511], [[0, 3]]),
|
||||
fira('<-', [1614, 1510], [[0, 2]]),
|
||||
fira('<->', [1614, 1614, 1512], [[0, 3]]),
|
||||
fira('->', [1614, 1064], [[0, 2]]),
|
||||
fira('-->', [1614, 1614, 1063], [[0, 3]]),
|
||||
fira('->>', [1614, 1614, 1065], [[0, 3]]),
|
||||
fira('>->', [1614, 1614, 1493], [[0, 3]]),
|
||||
fira('<=<', [1614, 1614, 1519], [[0, 3]]),
|
||||
fira('<<=', [1614, 1614, 1523], [[0, 3]]),
|
||||
fira('<==', [1614, 1614, 1517], [[0, 3]]),
|
||||
fira('<=>', [1614, 1614, 1518], [[0, 3]]),
|
||||
fira('=>', [1614, 1488], [[0, 2]]),
|
||||
fira('==>', [1614, 1614, 1487], [[0, 3]]),
|
||||
fira('=>>', [1614, 1614, 1489], [[0, 3]]),
|
||||
fira('>=>', [1614, 1614, 1495], [[0, 3]]),
|
||||
fira('>>=', [1614, 1614, 1498], [[0, 3]]),
|
||||
fira('>>-', [1614, 1614, 1497], [[0, 3]]),
|
||||
fira('>-', [1614, 1492], [[0, 2]]),
|
||||
fira('<~>', [1614, 1614, 1526], [[0, 3]]),
|
||||
fira('-<', [1614, 1066], [[0, 2]]),
|
||||
fira('-<<', [1614, 1614, 1067], [[0, 3]]),
|
||||
fira('=<<', [1614, 1614, 1490], [[0, 3]]),
|
||||
fira('<~~', [1614, 1614, 1527], [[0, 3]]),
|
||||
fira('<~', [1614, 1525], [[0, 2]]),
|
||||
fira('~~', [1614, 1534], [[0, 2]]),
|
||||
fira('~>', [1614, 1533], [[0, 2]]),
|
||||
fira('~~>', [1614, 1614, 1535], [[0, 3]]),
|
||||
fira('<<<', [1614, 1614, 1524], [[0, 3]]),
|
||||
fira('<<', [1614, 1521], [[0, 2]]),
|
||||
fira('<=', [1614, 1516], [[0, 2]]),
|
||||
fira('<>', [1614, 1520], [[0, 2]]),
|
||||
fira('>=', [1614, 1494], [[0, 2]]),
|
||||
fira('>>', [1614, 1496], [[0, 2]]),
|
||||
fira('>>>', [1614, 1614, 1499], [[0, 3]]),
|
||||
fira('{.', [1001, 977], [[0, 2]]),
|
||||
fira('{|', [1614, 1049], [[0, 2]]),
|
||||
fira('[|', [1614, 1050], [[0, 2]]),
|
||||
fira('<:', [1614, 1506], [[0, 2]]),
|
||||
fira(':>', [1614, 1056], [[0, 2]]),
|
||||
fira('|]', [1614, 1474], [[0, 2]]),
|
||||
fira('|}', [1614, 1473], [[0, 2]]),
|
||||
fira('.}', [977, 1002], [[0, 2]]),
|
||||
fira('<|||', [1614, 1614, 1614, 1504], [[0, 4]]),
|
||||
fira('<||', [1614, 1614, 1503], [[0, 3]]),
|
||||
fira('<|', [1614, 1502], [[0, 2]]),
|
||||
fira('<|>', [1614, 1614, 1505], [[0, 3]]),
|
||||
fira('|>', [1614, 1477], [[0, 2]]),
|
||||
fira('||>', [1614, 1614, 1472], [[0, 3]]),
|
||||
fira('|||>', [1614, 1614, 1614, 1470], [[0, 4]]),
|
||||
fira('<$', [1614, 1507], [[0, 2]]),
|
||||
fira('<$>', [1614, 1614, 1508], [[0, 3]]),
|
||||
fira('$>', [1614, 1479], [[0, 2]]),
|
||||
fira('<+', [1614, 1514], [[0, 2]]),
|
||||
fira('<+>', [1614, 1614, 1515], [[0, 3]]),
|
||||
fira('+>', [1614, 1482], [[0, 2]]),
|
||||
fira('<*', [1614, 1500], [[0, 2]]),
|
||||
fira('<*>', [1614, 1614, 1501], [[0, 3]]),
|
||||
fira('*>', [1614, 1047], [[0, 2]]),
|
||||
fira('/*', [1614, 1092], [[0, 2]]),
|
||||
fira('*/', [1614, 1048], [[0, 2]]),
|
||||
fira('///', [1614, 1614, 1097], [[0, 3]]),
|
||||
fira('//', [1614, 1096], [[0, 2]]),
|
||||
fira('</', [1614, 1528], [[0, 2]]),
|
||||
fira('<!--', [1614, 1614, 1614, 1509], [[0, 4]]),
|
||||
fira('</>', [1614, 1614, 1529], [[0, 3]]),
|
||||
fira('/>', [1614, 1095], [[0, 2]]),
|
||||
fira('0xff', [895, 270, 166, 166], [[0, 3]]),
|
||||
fira('10x10', [896, 895, 270, 896, 895], [[1, 4]]),
|
||||
fira('9:45', [904, 998, 899, 900], [[0, 2]]),
|
||||
fira('[:]', [1003, 998, 1004], [[0, 2]]),
|
||||
fira(';;', [1614, 1091], [[0, 2]]),
|
||||
fira('::', [1614, 1052], [[0, 2]]),
|
||||
fira(':::', [1614, 1614, 1053], [[0, 3]]),
|
||||
fira('..', [1614, 1082], [[0, 2]]),
|
||||
fira('...', [1614, 1614, 1085], [[0, 3]]),
|
||||
fira('..<', [1614, 1614, 1084], [[0, 3]]),
|
||||
fira('!!', [1614, 1057], [[0, 2]]),
|
||||
fira('??', [1614, 1090], [[0, 2]]),
|
||||
fira('%%', [1614, 1536], [[0, 2]]),
|
||||
fira('&&', [1614, 1468], [[0, 2]]),
|
||||
fira('||', [1614, 1469], [[0, 2]]),
|
||||
fira('?.', [1614, 1089], [[0, 2]]),
|
||||
fira('?:', [1614, 1087], [[0, 2]]),
|
||||
fira('++', [1614, 1480], [[0, 2]]),
|
||||
fira('+++', [1614, 1614, 1481], [[0, 3]]),
|
||||
fira('--', [1614, 1061], [[0, 2]]),
|
||||
fira('---', [1614, 1614, 1062], [[0, 3]]),
|
||||
fira('**', [1614, 1045], [[0, 2]]),
|
||||
fira('***', [1614, 1614, 1046], [[0, 3]]),
|
||||
fira('~=', [1614, 1532], [[0, 2]]),
|
||||
fira('~-', [1614, 1531], [[0, 2]]),
|
||||
fira('www', [1614, 1614, 271], [[0, 3]]),
|
||||
fira('-~', [1614, 1068], [[0, 2]]),
|
||||
fira('~@', [1614, 1530], [[0, 2]]),
|
||||
fira('^=', [1614, 1478], [[0, 2]]),
|
||||
fira('?=', [1614, 1088], [[0, 2]]),
|
||||
fira('/=', [1614, 1093], [[0, 2]]),
|
||||
fira('/==', [1614, 1614, 1094], [[0, 3]]),
|
||||
fira('-|', [1614, 1060], [[0, 2]]),
|
||||
fira('_|_', [1614, 1614, 1098], [[0, 3]]),
|
||||
fira('|-', [1614, 1475], [[0, 2]]),
|
||||
fira('|=', [1614, 1476], [[0, 2]]),
|
||||
fira('||=', [1614, 1614, 1471], [[0, 3]]),
|
||||
fira('#!', [1614, 1071], [[0, 2]]),
|
||||
fira('#=', [1614, 1075], [[0, 2]]),
|
||||
fira('##', [1614, 1072], [[0, 2]]),
|
||||
fira('###', [1614, 1614, 1073], [[0, 3]]),
|
||||
fira('####', [1614, 1614, 1614, 1074], [[0, 4]]),
|
||||
fira('#{', [1614, 1069], [[0, 2]]),
|
||||
fira('#[', [1614, 1070], [[0, 2]]),
|
||||
fira(']#', [1614, 1051], [[0, 2]]),
|
||||
fira('#(', [1614, 1076], [[0, 2]]),
|
||||
fira('#?', [1614, 1077], [[0, 2]]),
|
||||
fira('#_', [1614, 1078], [[0, 2]]),
|
||||
fira('#_(', [1614, 1614, 1079], [[0, 3]]),
|
||||
fira('::=', [1614, 1614, 1054], [[0, 3]]),
|
||||
fira('.?', [1614, 1086], [[0, 2]]),
|
||||
fira('===>', [1614, 1614, 1486, 1148], [[0, 4]])
|
||||
];
|
||||
|
||||
const iosevkaCases: ITestCase[] = [
|
||||
iosevka('<-', [31, 3127], [[0, 2]]),
|
||||
iosevka('<--', [31, 3129, 3139], [[0, 3]]),
|
||||
iosevka('<---', [31, 3129, 3150, 3139], [[0, 4]]),
|
||||
iosevka('<-----', [31, 3129, 3150, 3139, 3151, 3151], [[0, 6]]),
|
||||
iosevka('->', [3126, 33], [[0, 2]]),
|
||||
iosevka('-->', [3140, 3128, 33], [[0, 3]]),
|
||||
iosevka('--->', [3140, 3150, 3128, 33], [[0, 4]]),
|
||||
iosevka('----->', [3153, 3153, 3140, 3150, 3128, 33], [[0, 6]]),
|
||||
iosevka('<->', [31, 3149, 33], [[0, 3]]),
|
||||
iosevka('<-->', [31, 3129, 3128, 33], [[0, 4]]),
|
||||
iosevka('<--->', [31, 3129, 3150, 3128, 33], [[0, 5]]),
|
||||
iosevka('<----->', [31, 3129, 3150, 3150, 3150, 3128, 33], [[0, 7]]),
|
||||
iosevka('<=', [3094, 3095], [[0, 2]]),
|
||||
iosevka('<==', [31, 3158, 3168], [[0, 3]]),
|
||||
iosevka('<===', [31, 3158, 3179, 3168], [[0, 4]]),
|
||||
iosevka('<=====', [31, 3158, 3179, 3168, 3180, 3180], [[0, 6]]),
|
||||
iosevka('=>', [3155, 33], [[0, 2]]),
|
||||
iosevka('==>', [3169, 3157, 33], [[0, 3]]),
|
||||
iosevka('===>', [3169, 3179, 3157, 33], [[0, 4]]),
|
||||
iosevka('=====>', [3182, 3182, 3169, 3179, 3157, 33], [[0, 6]]),
|
||||
iosevka('<=>', [31, 3178, 33], [[0, 3]]),
|
||||
iosevka('<==>', [31, 3158, 3157, 33], [[0, 4]]),
|
||||
iosevka('<===>', [31, 3158, 3179, 3157, 33], [[0, 5]]),
|
||||
iosevka('<=====>', [31, 3158, 3179, 3179, 3179, 3157, 33], [[0, 7]]),
|
||||
iosevka('<!--', [31, 3184, 3130, 3139], [[0, 4]]),
|
||||
iosevka('<!---', [31, 3184, 3130, 3150, 3139], [[0, 5]]),
|
||||
iosevka('<!-----', [31, 3184, 3130, 3150, 3139, 3151, 3151], [[0, 7]]),
|
||||
iosevka('a:b', [68, 29, 69], []),
|
||||
iosevka('a::b', [68, 3012, 3013, 69], [[1, 3]]),
|
||||
iosevka('a:::b', [68, 3012, 3010, 3013, 69], [[1, 4]]),
|
||||
iosevka(':=', [3011, 32], [[0, 2]]),
|
||||
iosevka(':-', [3011, 16], [[0, 2]]),
|
||||
iosevka(':+', [3011, 14], [[0, 2]]),
|
||||
iosevka('=:', [32, 3011], [[0, 2]]),
|
||||
iosevka('-:', [16, 3011], [[0, 2]]),
|
||||
iosevka('+:', [14, 3011], [[0, 2]]),
|
||||
iosevka('<*', [31, 3023], [[0, 2]]),
|
||||
iosevka('*>', [3023, 33], [[0, 2]]),
|
||||
iosevka('<*>', [31, 3023, 33], [[1, 3]]),
|
||||
iosevka('<**>', [31, 3023, 3023, 33], [[1, 4]]),
|
||||
iosevka('<****>', [31, 3023, 3023, 3023, 3023, 33], [[0, 3], [3, 6]]),
|
||||
iosevka('==', [3083, 3084], [[0, 2]]),
|
||||
iosevka('!=', [3098, 3084], [[0, 2]]),
|
||||
iosevka('===', [3083, 3085, 3084], [[0, 3]]),
|
||||
iosevka('!==', [3099, 3085, 3084], [[0, 3]]),
|
||||
iosevka('====', [3083, 3085, 3085, 3084], [[0, 4]]),
|
||||
iosevka('!===', [3100, 3085, 3085, 3084], [[0, 4]])
|
||||
];
|
||||
|
||||
const monoidCases: ITestCase[] = [
|
||||
monoid('<!--', [775, 775, 775, 643], [[0, 4]]),
|
||||
monoid('-->', [779, 779, 628], [[0, 3]]),
|
||||
monoid('<--', [776, 776, 627], [[0, 3]]),
|
||||
monoid('->>', [780, 780, 626], [[0, 3]]),
|
||||
monoid('<<-', [777, 777, 625], [[0, 3]]),
|
||||
monoid('->', [781, 623], [[0, 2]]),
|
||||
monoid('<-', [778, 624], [[0, 2]]),
|
||||
monoid('=>', [793, 666], [[0, 2]]),
|
||||
monoid('<=>', [785, 785, 760], [[0, 3]]),
|
||||
monoid('<==>', [786, 786, 786, 771], [[0, 4]]),
|
||||
monoid('==>', [787, 787, 672], [[0, 3]]),
|
||||
monoid('<==', [788, 788, 671], [[0, 3]]),
|
||||
monoid('>>=', [791, 791, 758], [[0, 3]]),
|
||||
monoid('=<<', [792, 792, 759], [[0, 3]]),
|
||||
monoid('--', [667, 667], [[0, 2]]),
|
||||
monoid(':=', [29, 761], [[0, 2]]),
|
||||
monoid('=:=', [789, 789, 665], [[0, 3]]),
|
||||
monoid('==', [794, 641], [[0, 2]]),
|
||||
monoid('!==', [782, 782, 646], [[0, 3]]),
|
||||
monoid('!=', [783, 629], [[0, 2]]),
|
||||
monoid('<=', [790, 630], [[0, 2]]),
|
||||
monoid('>=', [792, 631], [[0, 2]]),
|
||||
monoid('//', [621, 664], [[0, 2]]),
|
||||
monoid('/**', [18, 753, 753], [[0, 3]]),
|
||||
monoid('/*', [18, 753], [[0, 2]]),
|
||||
monoid('*/', [754, 18], [[0, 2]]),
|
||||
monoid('&&', [633, 775], [[0, 2]]),
|
||||
monoid('.&', [17, 755], [[0, 2]]),
|
||||
monoid('||', [634, 635], [[0, 2]]),
|
||||
monoid('!!', [769, 770], [[0, 2]]),
|
||||
monoid('::', [772, 773], [[0, 2]]),
|
||||
monoid('>>', [637, 638], [[0, 2]]),
|
||||
monoid('<<', [639, 640], [[0, 2]]),
|
||||
monoid('¯\\_(ツ)_/¯', [113, 765, 66, 767, 613, 768, 66, 766, 113], [[0, 3], [3, 6], [6, 9]]),
|
||||
monoid('__', [763, 764], [[0, 2]])
|
||||
];
|
||||
|
||||
const ubuntuCases: ITestCase[] = [
|
||||
ubuntu('==>', [32, 32, 33], [])
|
||||
];
|
||||
|
||||
const fontPaths: Record<string, string> = {
|
||||
'Fira Code': path.join(__dirname, '../../fonts/FiraCode-Regular.otf'),
|
||||
'Iosevka': path.join(__dirname, '../../fonts/iosevka-regular.ttf'),
|
||||
'Monoid': path.join(__dirname, '../../fonts/Monoid-Regular.ttf'),
|
||||
'Ubuntu Mono': path.join(__dirname, '../../fonts/UbuntuMono-Regular.ttf')
|
||||
};
|
||||
|
||||
const fontCache: Map<string, IFont> = new Map();
|
||||
|
||||
function loadFont(fontName: string): IFont {
|
||||
let font = fontCache.get(fontName);
|
||||
if (!font) {
|
||||
const fontPath = fontPaths[fontName];
|
||||
const buffer = fs.readFileSync(fontPath);
|
||||
font = loadBuffer(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength));
|
||||
fontCache.set(fontName, font);
|
||||
}
|
||||
return font;
|
||||
}
|
||||
|
||||
describe('addon-ligatures - index', () => {
|
||||
describe('findLigatures', () => {
|
||||
for (const { font: fontName, input, glyphs, ranges } of [...firaCases, ...iosevkaCases, ...monoidCases, ...ubuntuCases]) {
|
||||
it(`${fontName}: '${input}'`, () => {
|
||||
const font = loadFont(fontName);
|
||||
const result = font.findLigatures(input);
|
||||
assert.deepEqual(result.outputGlyphs, glyphs);
|
||||
assert.deepEqual(result.contextRanges, ranges);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('findLigatureRanges', () => {
|
||||
for (const { font: fontName, input, ranges } of [...firaCases, ...iosevkaCases, ...monoidCases, ...ubuntuCases]) {
|
||||
it(`${fontName}: '${input}'`, () => {
|
||||
const font = loadFont(fontName);
|
||||
const result = font.findLigatureRanges(input);
|
||||
assert.deepEqual(result, ranges);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('caching', () => {
|
||||
it('findLigatures caches successive calls correctly', () => {
|
||||
const fontPath = fontPaths['Fira Code'];
|
||||
const buffer = fs.readFileSync(fontPath);
|
||||
const font = loadBuffer(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength), { cacheSize: 100 });
|
||||
const result1 = font.findLigatures('in --> out');
|
||||
const result2 = font.findLigatures('in --> out');
|
||||
assert.deepEqual(result1, result2);
|
||||
});
|
||||
|
||||
it('findLigatureRanges caches successive calls correctly', () => {
|
||||
const fontPath = fontPaths['Fira Code'];
|
||||
const buffer = fs.readFileSync(fontPath);
|
||||
const font = loadBuffer(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength), { cacheSize: 100 });
|
||||
const result1 = font.findLigatureRanges('in --> out');
|
||||
const result2 = font.findLigatureRanges('in --> out');
|
||||
assert.deepEqual(result1, result2);
|
||||
});
|
||||
|
||||
it('caches calls to findLigatures after findLigatureRanges correctly', () => {
|
||||
const fontPath = fontPaths['Fira Code'];
|
||||
const buffer = fs.readFileSync(fontPath);
|
||||
|
||||
const uncached = loadBuffer(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength));
|
||||
const uncachedResult1 = uncached.findLigatureRanges('in --> out');
|
||||
const uncachedResult2 = uncached.findLigatures('in --> out');
|
||||
|
||||
const font = loadBuffer(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength), { cacheSize: 100 });
|
||||
const result1 = font.findLigatureRanges('in --> out');
|
||||
const result2 = font.findLigatures('in --> out');
|
||||
|
||||
assert.deepEqual(result1, uncachedResult1);
|
||||
assert.deepEqual(result2, uncachedResult2);
|
||||
assert.deepEqual(result1, result2.contextRanges);
|
||||
});
|
||||
|
||||
it('caches calls to findLigatureRanges after findLigatures correctly', () => {
|
||||
const fontPath = fontPaths['Fira Code'];
|
||||
const buffer = fs.readFileSync(fontPath);
|
||||
|
||||
const uncached = loadBuffer(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength));
|
||||
const uncachedResult1 = uncached.findLigatures('in --> out');
|
||||
const uncachedResult2 = uncached.findLigatureRanges('in --> out');
|
||||
|
||||
const font = loadBuffer(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength), { cacheSize: 100 });
|
||||
const result1 = font.findLigatures('in --> out');
|
||||
const result2 = font.findLigatureRanges('in --> out');
|
||||
|
||||
assert.deepEqual(result1, uncachedResult1);
|
||||
assert.deepEqual(result2, uncachedResult2);
|
||||
assert.deepEqual(result1.contextRanges, result2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,262 @@
|
||||
import * as opentype from 'opentype.js';
|
||||
import LRUCache = require('lru-cache');
|
||||
|
||||
import { IFont, ILigatureData, IFlattenedLookupTree, ILookupTree, IOptions } from './types';
|
||||
import mergeTrees from './merge';
|
||||
import walkTree from './walk';
|
||||
import mergeRange from './mergeRange';
|
||||
|
||||
import buildTreeGsubType6Format1 from './processors/6-1';
|
||||
import buildTreeGsubType6Format2 from './processors/6-2';
|
||||
import buildTreeGsubType6Format3 from './processors/6-3';
|
||||
import buildTreeGsubType8Format1 from './processors/8-1';
|
||||
import flatten from './flatten';
|
||||
|
||||
class FontImpl implements IFont {
|
||||
private _font: opentype.Font;
|
||||
private _lookupTrees: { tree: IFlattenedLookupTree, processForward: boolean }[] = [];
|
||||
private _glyphLookups: { [glyphId: string]: number[] } = {};
|
||||
private _cache?: LRUCache<string, ILigatureData | [number, number][]>;
|
||||
|
||||
constructor(font: opentype.Font, options: Required<IOptions>) {
|
||||
this._font = font;
|
||||
|
||||
if (options.cacheSize > 0) {
|
||||
this._cache = new LRUCache({
|
||||
max: options.cacheSize,
|
||||
length: ((val: ILigatureData | [number, number][], key: string) => key.length) as any
|
||||
});
|
||||
}
|
||||
|
||||
const caltFeatures = this._font.tables.gsub && this._font.tables.gsub.features.filter((f: { tag: string }) => f.tag === 'calt') || [];
|
||||
const lookupIndices: number[] = caltFeatures
|
||||
.reduce((acc: number[], val: { feature: { lookupListIndexes: number[] } }) => [...acc, ...val.feature.lookupListIndexes], []);
|
||||
|
||||
const allLookups = this._font.tables.gsub && this._font.tables.gsub.lookups || [];
|
||||
const lookupGroups = allLookups.filter((l: unknown, i: number) => lookupIndices.some(idx => idx === i));
|
||||
|
||||
for (const [index, lookup] of lookupGroups.entries()) {
|
||||
const trees: ILookupTree[] = [];
|
||||
switch (lookup.lookupType) {
|
||||
case 6:
|
||||
for (const [index, table] of lookup.subtables.entries()) {
|
||||
switch (table.substFormat) {
|
||||
case 1:
|
||||
trees.push(buildTreeGsubType6Format1(table, allLookups, index));
|
||||
break;
|
||||
case 2:
|
||||
trees.push(buildTreeGsubType6Format2(table, allLookups, index));
|
||||
break;
|
||||
case 3:
|
||||
trees.push(buildTreeGsubType6Format3(table, allLookups, index));
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 8:
|
||||
for (const [index, table] of lookup.subtables.entries()) {
|
||||
trees.push(buildTreeGsubType8Format1(table, index));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const tree = flatten(mergeTrees(trees));
|
||||
|
||||
this._lookupTrees.push({
|
||||
tree,
|
||||
processForward: lookup.lookupType !== 8
|
||||
});
|
||||
|
||||
for (const glyphId of Object.keys(tree)) {
|
||||
if (!this._glyphLookups[glyphId]) {
|
||||
this._glyphLookups[glyphId] = [];
|
||||
}
|
||||
|
||||
this._glyphLookups[glyphId].push(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public findLigatures(text: string): ILigatureData {
|
||||
const cached = this._cache && this._cache.get(text);
|
||||
if (cached && !Array.isArray(cached)) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const glyphIds: number[] = [];
|
||||
for (const char of text) {
|
||||
glyphIds.push(this._font.charToGlyphIndex(char));
|
||||
}
|
||||
|
||||
// If there are no lookup groups, there's no point looking for
|
||||
// replacements. This gives us a minor performance boost for fonts with
|
||||
// no ligatures
|
||||
if (this._lookupTrees.length === 0) {
|
||||
return {
|
||||
inputGlyphs: glyphIds,
|
||||
outputGlyphs: glyphIds,
|
||||
contextRanges: []
|
||||
};
|
||||
}
|
||||
|
||||
const result = this._findInternal(glyphIds.slice());
|
||||
const finalResult: ILigatureData = {
|
||||
inputGlyphs: glyphIds,
|
||||
outputGlyphs: result.sequence,
|
||||
contextRanges: result.ranges
|
||||
};
|
||||
if (this._cache) {
|
||||
this._cache.set(text, finalResult);
|
||||
}
|
||||
|
||||
return finalResult;
|
||||
}
|
||||
|
||||
public findLigatureRanges(text: string): [number, number][] {
|
||||
// Short circuit the process if there are no possible ligatures in the
|
||||
// font
|
||||
if (this._lookupTrees.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const cached = this._cache && this._cache.get(text);
|
||||
if (cached) {
|
||||
return Array.isArray(cached) ? cached : cached.contextRanges;
|
||||
}
|
||||
|
||||
const glyphIds: number[] = [];
|
||||
for (const char of text) {
|
||||
glyphIds.push(this._font.charToGlyphIndex(char));
|
||||
}
|
||||
|
||||
const result = this._findInternal(glyphIds);
|
||||
if (this._cache) {
|
||||
this._cache.set(text, result.ranges);
|
||||
}
|
||||
|
||||
return result.ranges;
|
||||
}
|
||||
|
||||
private _findInternal(sequence: number[]): { sequence: number[], ranges: [number, number][] } {
|
||||
const ranges: [number, number][] = [];
|
||||
|
||||
let nextLookup = this._getNextLookup(sequence, 0);
|
||||
while (nextLookup.index !== null) {
|
||||
const lookup = this._lookupTrees[nextLookup.index];
|
||||
if (lookup.processForward) {
|
||||
let lastGlyphIndex = nextLookup.last;
|
||||
for (let i = nextLookup.first; i < lastGlyphIndex; i++) {
|
||||
const result = walkTree(lookup.tree, sequence, i, i);
|
||||
if (result) {
|
||||
for (let j = 0; j < result.substitutions.length; j++) {
|
||||
const sub = result.substitutions[j];
|
||||
if (sub !== null) {
|
||||
sequence[i + j] = sub;
|
||||
}
|
||||
}
|
||||
|
||||
mergeRange(
|
||||
ranges,
|
||||
result.contextRange[0] + i,
|
||||
result.contextRange[1] + i
|
||||
);
|
||||
|
||||
// Substitutions can end up extending the search range
|
||||
if (i + result.length >= lastGlyphIndex) {
|
||||
lastGlyphIndex = i + result.length + 1;
|
||||
}
|
||||
|
||||
i += result.length - 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We don't need to do the lastGlyphIndex tracking here because
|
||||
// reverse processing isn't allowed to replace more than one
|
||||
// character at a time.
|
||||
for (let i = nextLookup.last - 1; i >= nextLookup.first; i--) {
|
||||
const result = walkTree(lookup.tree, sequence, i, i);
|
||||
if (result) {
|
||||
for (let j = 0; j < result.substitutions.length; j++) {
|
||||
const sub = result.substitutions[j];
|
||||
if (sub !== null) {
|
||||
sequence[i + j] = sub;
|
||||
}
|
||||
}
|
||||
|
||||
mergeRange(
|
||||
ranges,
|
||||
result.contextRange[0] + i,
|
||||
result.contextRange[1] + i
|
||||
);
|
||||
|
||||
i -= result.length - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nextLookup = this._getNextLookup(sequence, nextLookup.index + 1);
|
||||
}
|
||||
|
||||
return { sequence, ranges };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the lookup and glyph range for the first lookup that might
|
||||
* contain a match.
|
||||
*
|
||||
* @param sequence Input glyph sequence
|
||||
* @param start The first input to try
|
||||
*/
|
||||
private _getNextLookup(sequence: number[], start: number): { index: number | null, first: number, last: number } {
|
||||
const result: { index: number | null, first: number, last: number } = {
|
||||
index: null,
|
||||
first: Infinity,
|
||||
last: -1
|
||||
};
|
||||
|
||||
// Loop through each glyph and find the first valid lookup for it
|
||||
for (let i = 0; i < sequence.length; i++) {
|
||||
const lookups = this._glyphLookups[sequence[i]];
|
||||
if (!lookups) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let j = 0; j < lookups.length; j++) {
|
||||
const lookupIndex = lookups[j];
|
||||
if (lookupIndex >= start) {
|
||||
// Update the lookup information if it's the one we're
|
||||
// storing or earlier than it.
|
||||
if (result.index === null || lookupIndex <= result.index) {
|
||||
result.index = lookupIndex;
|
||||
|
||||
if (result.first > i) {
|
||||
result.first = i;
|
||||
}
|
||||
|
||||
result.last = i + 1;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the font from it's binary data. The returned value can be used to find
|
||||
* ligatures for the font.
|
||||
*
|
||||
* @param buffer ArrayBuffer of the font to load
|
||||
*/
|
||||
export function loadBuffer(buffer: ArrayBuffer, options?: IOptions): IFont {
|
||||
const font = opentype.parse(buffer);
|
||||
return new FontImpl(font, {
|
||||
cacheSize: 0,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
export { IFont as Font, ILigatureData as LigatureData, IOptions as Options };
|
||||
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import mergeTrees from './merge';
|
||||
|
||||
interface ILookupResult {
|
||||
contextRange: [number, number];
|
||||
index: number;
|
||||
subIndex: number;
|
||||
length: number;
|
||||
substitutions: number[];
|
||||
}
|
||||
|
||||
function lookup(substitutionGlyph: number, index?: number, subIndex?: number): ILookupResult {
|
||||
return {
|
||||
contextRange: [0, 1],
|
||||
index: index || 0,
|
||||
subIndex: subIndex || 0,
|
||||
length: 1,
|
||||
substitutions: [substitutionGlyph]
|
||||
};
|
||||
}
|
||||
|
||||
describe('addon-ligatures - merge', () => {
|
||||
describe('mergeTrees', () => {
|
||||
it('combines disjoint trees', () => {
|
||||
const result = mergeTrees([
|
||||
{
|
||||
individual: {
|
||||
'1': { lookup: lookup(1) }
|
||||
},
|
||||
range: []
|
||||
},
|
||||
{
|
||||
individual: {},
|
||||
range: [{
|
||||
entry: { lookup: lookup(2) },
|
||||
range: [2, 4]
|
||||
}]
|
||||
},
|
||||
{
|
||||
individual: {
|
||||
'5': { lookup: lookup(3) }
|
||||
},
|
||||
range: []
|
||||
},
|
||||
{
|
||||
individual: {},
|
||||
range: [{
|
||||
entry: { lookup: lookup(4) },
|
||||
range: [8, 10]
|
||||
}]
|
||||
}
|
||||
]);
|
||||
|
||||
assert.deepEqual(result, {
|
||||
individual: {
|
||||
'1': { lookup: lookup(1) },
|
||||
'5': { lookup: lookup(3) }
|
||||
},
|
||||
range: [{
|
||||
entry: { lookup: lookup(2) },
|
||||
range: [2, 4]
|
||||
}, {
|
||||
entry: { lookup: lookup(4) },
|
||||
range: [8, 10]
|
||||
}]
|
||||
});
|
||||
});
|
||||
|
||||
it('merges matching individual glyphs', () => {
|
||||
const result = mergeTrees([
|
||||
{
|
||||
individual: {
|
||||
'1': { lookup: lookup(1, 1) }
|
||||
},
|
||||
range: []
|
||||
},
|
||||
{
|
||||
individual: {
|
||||
'1': { lookup: lookup(2, 0) }
|
||||
},
|
||||
range: []
|
||||
},
|
||||
{
|
||||
individual: {
|
||||
'1': { lookup: lookup(3, 2) }
|
||||
},
|
||||
range: []
|
||||
}
|
||||
]);
|
||||
|
||||
assert.deepEqual(result, {
|
||||
individual: {
|
||||
'1': { lookup: lookup(2, 0) }
|
||||
},
|
||||
range: []
|
||||
});
|
||||
});
|
||||
|
||||
it('merges range glyphs overlapping individual glyphs', () => {
|
||||
const result = mergeTrees([
|
||||
{
|
||||
individual: {
|
||||
'1': { lookup: lookup(1, 0) }
|
||||
},
|
||||
range: []
|
||||
},
|
||||
{
|
||||
individual: {},
|
||||
range: [{
|
||||
entry: { lookup: lookup(2, 1) },
|
||||
range: [0, 4]
|
||||
}]
|
||||
}
|
||||
]);
|
||||
|
||||
assert.deepEqual(result, {
|
||||
individual: {
|
||||
'0': { lookup: lookup(2, 1) },
|
||||
'1': { lookup: lookup(1, 0) }
|
||||
},
|
||||
range: [{
|
||||
entry: { lookup: lookup(2, 1) },
|
||||
range: [2, 4]
|
||||
}]
|
||||
});
|
||||
});
|
||||
|
||||
it('merges individual glyphs overlapping range glyphs', () => {
|
||||
const result = mergeTrees([
|
||||
{
|
||||
individual: {},
|
||||
range: [{
|
||||
entry: { lookup: lookup(2, 1) },
|
||||
range: [0, 4]
|
||||
}]
|
||||
},
|
||||
{
|
||||
individual: {
|
||||
'1': { lookup: lookup(1, 0) }
|
||||
},
|
||||
range: []
|
||||
}
|
||||
]);
|
||||
|
||||
assert.deepEqual(result, {
|
||||
individual: {
|
||||
'0': { lookup: lookup(2, 1) },
|
||||
'1': { lookup: lookup(1, 0) }
|
||||
},
|
||||
range: [{
|
||||
entry: { lookup: lookup(2, 1) },
|
||||
range: [2, 4]
|
||||
}]
|
||||
});
|
||||
});
|
||||
|
||||
it('merges multiple overlapping ranges', () => {
|
||||
const result = mergeTrees([
|
||||
{
|
||||
individual: {},
|
||||
range: [{
|
||||
entry: { lookup: lookup(1, 2) },
|
||||
range: [0, 3]
|
||||
}, {
|
||||
entry: { lookup: lookup(2, 1) },
|
||||
range: [6, 12]
|
||||
}, {
|
||||
entry: { lookup: lookup(5, 3) },
|
||||
range: [15, 20]
|
||||
}, {
|
||||
entry: { lookup: lookup(7, 4) },
|
||||
range: [20, 22]
|
||||
}]
|
||||
},
|
||||
{
|
||||
individual: {},
|
||||
range: [{
|
||||
entry: { lookup: lookup(3, 0) },
|
||||
range: [2, 8]
|
||||
}, {
|
||||
entry: { lookup: lookup(4, 0) },
|
||||
range: [10, 13]
|
||||
}, {
|
||||
entry: { lookup: lookup(6, 0) },
|
||||
range: [16, 21]
|
||||
}]
|
||||
}
|
||||
]);
|
||||
|
||||
assert.deepEqual(result, {
|
||||
individual: {
|
||||
'2': { lookup: lookup(3, 0) },
|
||||
'12': { lookup: lookup(4, 0) },
|
||||
'15': { lookup: lookup(5, 3) },
|
||||
'20': { lookup: lookup(6, 0) },
|
||||
'21': { lookup: lookup(7, 4) }
|
||||
},
|
||||
range: [{
|
||||
entry: { lookup: lookup(1, 2) },
|
||||
range: [0, 2]
|
||||
}, {
|
||||
entry: { lookup: lookup(3, 0) },
|
||||
range: [6, 8]
|
||||
}, {
|
||||
entry: { lookup: lookup(3, 0) },
|
||||
range: [3, 6]
|
||||
}, {
|
||||
entry: { lookup: lookup(2, 1) },
|
||||
range: [8, 10]
|
||||
}, {
|
||||
entry: { lookup: lookup(4, 0) },
|
||||
range: [10, 12]
|
||||
}, {
|
||||
entry: { lookup: lookup(6, 0) },
|
||||
range: [16, 20]
|
||||
}]
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,372 @@
|
||||
import { ILookupTree, ILookupTreeEntry } from './types';
|
||||
|
||||
/**
|
||||
* Merges the provided trees into a single lookup tree. When conflicting lookups
|
||||
* are encountered between two trees, the one with the lower index, then the
|
||||
* lower subindex is chosen.
|
||||
*
|
||||
* @param trees Array of trees to merge. Entries in earlier trees are favored
|
||||
* over those in later trees when there is a choice.
|
||||
*/
|
||||
export default function mergeTrees(trees: ILookupTree[]): ILookupTree {
|
||||
const result: ILookupTree = {
|
||||
individual: {},
|
||||
range: []
|
||||
};
|
||||
|
||||
for (const tree of trees) {
|
||||
mergeSubtree(result, tree);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively merges the data for the mergeTree into the mainTree.
|
||||
*
|
||||
* @param mainTree The tree where the values should be merged
|
||||
* @param mergeTree The tree to be merged into the mainTree
|
||||
*/
|
||||
function mergeSubtree(mainTree: ILookupTree, mergeTree: ILookupTree): void {
|
||||
// Need to fix this recursively (and handle lookups)
|
||||
for (const [glyphId, value] of Object.entries(mergeTree.individual)) {
|
||||
// The main tree is guaranteed to have no overlaps between the
|
||||
// individual and range values, so if we match an invididual, there
|
||||
// must not be a range
|
||||
if (mainTree.individual[glyphId]) {
|
||||
mergeTreeEntry(mainTree.individual[glyphId], value);
|
||||
} else {
|
||||
let matched = false;
|
||||
for (const [index, { range, entry }] of mainTree.range.entries()) {
|
||||
const overlap = getIndividualOverlap(Number(glyphId), range);
|
||||
|
||||
// Don't overlap
|
||||
if (overlap.both === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
matched = true;
|
||||
|
||||
// If they overlap, we have to split the range and then
|
||||
// merge the overlap
|
||||
mainTree.individual[glyphId] = value;
|
||||
mergeTreeEntry(mainTree.individual[glyphId], cloneEntry(entry));
|
||||
|
||||
// When there's an overlap, we also have to fix up the range
|
||||
// that we had already processed
|
||||
mainTree.range.splice(index, 1);
|
||||
for (const glyph of overlap.second) {
|
||||
if (Array.isArray(glyph)) {
|
||||
mainTree.range.push({
|
||||
range: glyph,
|
||||
entry: cloneEntry(entry)
|
||||
});
|
||||
} else {
|
||||
mainTree.individual[glyph] = cloneEntry(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!matched) {
|
||||
mainTree.individual[glyphId] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const { range, entry } of mergeTree.range) {
|
||||
// Ranges are more complicated, because they can overlap with
|
||||
// multiple things, individual and range alike. We start by
|
||||
// eliminating ranges that are already present in another range
|
||||
let remainingRanges: (number | [number, number])[] = [range];
|
||||
|
||||
for (let index = 0; index < mainTree.range.length; index++) {
|
||||
const { range, entry: resultEntry } = mainTree.range[index];
|
||||
for (const [remainingIndex, remainingRange] of remainingRanges.entries()) {
|
||||
if (Array.isArray(remainingRange)) {
|
||||
const overlap = getRangeOverlap(remainingRange, range);
|
||||
if (overlap.both === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
mainTree.range.splice(index, 1);
|
||||
index--;
|
||||
|
||||
const entryToMerge: ILookupTreeEntry = cloneEntry(resultEntry);
|
||||
if (Array.isArray(overlap.both)) {
|
||||
mainTree.range.push({
|
||||
range: overlap.both,
|
||||
entry: entryToMerge
|
||||
});
|
||||
} else {
|
||||
mainTree.individual[overlap.both] = entryToMerge;
|
||||
}
|
||||
|
||||
mergeTreeEntry(entryToMerge, cloneEntry(entry));
|
||||
|
||||
for (const second of overlap.second) {
|
||||
if (Array.isArray(second)) {
|
||||
mainTree.range.push({
|
||||
range: second,
|
||||
entry: cloneEntry(resultEntry)
|
||||
});
|
||||
} else {
|
||||
mainTree.individual[second] = cloneEntry(resultEntry);
|
||||
}
|
||||
}
|
||||
|
||||
remainingRanges = overlap.first;
|
||||
} else {
|
||||
const overlap = getIndividualOverlap(remainingRange, range);
|
||||
if (overlap.both === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If they overlap, we have to split the range and then
|
||||
// merge the overlap
|
||||
mainTree.individual[remainingRange] = cloneEntry(entry);
|
||||
mergeTreeEntry(mainTree.individual[remainingRange], cloneEntry(resultEntry));
|
||||
|
||||
// When there's an overlap, we also have to fix up the range
|
||||
// that we had already processed
|
||||
mainTree.range.splice(index, 1);
|
||||
index--;
|
||||
|
||||
for (const glyph of overlap.second) {
|
||||
if (Array.isArray(glyph)) {
|
||||
mainTree.range.push({
|
||||
range: glyph,
|
||||
entry: cloneEntry(resultEntry)
|
||||
});
|
||||
} else {
|
||||
mainTree.individual[glyph] = cloneEntry(resultEntry);
|
||||
}
|
||||
}
|
||||
|
||||
remainingRanges.splice(remainingIndex, 1, ...overlap.first);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Next, we run the same against any individual glyphs
|
||||
for (const glyphId of Object.keys(mainTree.individual)) {
|
||||
for (const [remainingIndex, remainingRange] of remainingRanges.entries()) {
|
||||
if (Array.isArray(remainingRange)) {
|
||||
const overlap = getIndividualOverlap(Number(glyphId), remainingRange);
|
||||
if (overlap.both === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If they overlap, we have to merge the overlap
|
||||
mergeTreeEntry(mainTree.individual[glyphId], cloneEntry(entry));
|
||||
|
||||
// Update the remaining ranges
|
||||
remainingRanges.splice(remainingIndex, 1, ...overlap.second);
|
||||
break;
|
||||
} else {
|
||||
if (Number(glyphId) === remainingRange) {
|
||||
mergeTreeEntry(mainTree.individual[glyphId], cloneEntry(entry));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Any remaining ranges should just be added directly
|
||||
for (const remainingRange of remainingRanges) {
|
||||
if (Array.isArray(remainingRange)) {
|
||||
mainTree.range.push({
|
||||
range: remainingRange,
|
||||
entry: cloneEntry(entry)
|
||||
});
|
||||
} else {
|
||||
mainTree.individual[remainingRange] = cloneEntry(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively merges the entry forr the mergeTree into the mainTree
|
||||
*
|
||||
* @param mainTree The entry where the values should be merged
|
||||
* @param mergeTree The entry to merge into the mainTree
|
||||
*/
|
||||
function mergeTreeEntry(mainTree: ILookupTreeEntry, mergeTree: ILookupTreeEntry): void {
|
||||
if (
|
||||
mergeTree.lookup && (
|
||||
!mainTree.lookup ||
|
||||
mainTree.lookup.index > mergeTree.lookup.index ||
|
||||
(mainTree.lookup.index === mergeTree.lookup.index && mainTree.lookup.subIndex > mergeTree.lookup.subIndex)
|
||||
)
|
||||
) {
|
||||
mainTree.lookup = mergeTree.lookup;
|
||||
}
|
||||
|
||||
if (mergeTree.forward) {
|
||||
if (!mainTree.forward) {
|
||||
mainTree.forward = mergeTree.forward;
|
||||
} else {
|
||||
mergeSubtree(mainTree.forward, mergeTree.forward);
|
||||
}
|
||||
}
|
||||
|
||||
if (mergeTree.reverse) {
|
||||
if (!mainTree.reverse) {
|
||||
mainTree.reverse = mergeTree.reverse;
|
||||
} else {
|
||||
mergeSubtree(mainTree.reverse, mergeTree.reverse);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface IOverlap {
|
||||
first: (number | [number, number])[];
|
||||
second: (number | [number, number])[];
|
||||
both: number | [number, number] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the overlap (if any) between two ranges. Returns the distinct
|
||||
* ranges for each range and the overlap (if any).
|
||||
*
|
||||
* @param first First range
|
||||
* @param second Second range
|
||||
*/
|
||||
function getRangeOverlap(first: [number, number], second: [number, number]): IOverlap {
|
||||
const result: IOverlap = {
|
||||
first: [],
|
||||
second: [],
|
||||
both: null
|
||||
};
|
||||
|
||||
// Both
|
||||
if (first[0] < second[1] && second[0] < first[1]) {
|
||||
const start = Math.max(first[0], second[0]);
|
||||
const end = Math.min(first[1], second[1]);
|
||||
result.both = rangeOrIndividual(start, end);
|
||||
}
|
||||
|
||||
// Before
|
||||
if (first[0] < second[0]) {
|
||||
const start = first[0];
|
||||
const end = Math.min(second[0], first[1]);
|
||||
result.first.push(rangeOrIndividual(start, end));
|
||||
} else if (second[0] < first[0]) {
|
||||
const start = second[0];
|
||||
const end = Math.min(second[1], first[0]);
|
||||
result.second.push(rangeOrIndividual(start, end));
|
||||
}
|
||||
|
||||
// After
|
||||
if (first[1] > second[1]) {
|
||||
const start = Math.max(first[0], second[1]);
|
||||
const end = first[1];
|
||||
result.first.push(rangeOrIndividual(start, end));
|
||||
} else if (second[1] > first[1]) {
|
||||
const start = Math.max(first[1], second[0]);
|
||||
const end = second[1];
|
||||
result.second.push(rangeOrIndividual(start, end));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the overlap (if any) between the individual glyph and the range
|
||||
* provided. Returns the glyphs and/or ranges that are unique to each provided
|
||||
* and the overlap (if any).
|
||||
*
|
||||
* @param first Individual glyph
|
||||
* @param second Range
|
||||
*/
|
||||
function getIndividualOverlap(first: number, second: [number, number]): IOverlap {
|
||||
// Disjoint
|
||||
if (first < second[0] || first > second[1]) {
|
||||
return {
|
||||
first: [first],
|
||||
second: [second],
|
||||
both: null
|
||||
};
|
||||
}
|
||||
|
||||
const result: IOverlap = {
|
||||
first: [],
|
||||
second: [],
|
||||
both: first
|
||||
};
|
||||
|
||||
if (second[0] < first) {
|
||||
result.second.push(rangeOrIndividual(second[0], first));
|
||||
}
|
||||
|
||||
if (second[1] > first) {
|
||||
result.second.push(rangeOrIndividual(first + 1, second[1]));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an individual glyph if the range is of size one or a range if it is
|
||||
* larger.
|
||||
*
|
||||
* @param start Beginning of the range (inclusive)
|
||||
* @param end End of the range (exclusive)
|
||||
*/
|
||||
function rangeOrIndividual(start: number, end: number): number | [number, number] {
|
||||
if (end - start === 1) {
|
||||
return start;
|
||||
}
|
||||
return [start, end];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones an individual lookup tree entry.
|
||||
*
|
||||
* @param entry Lookup tree entry to clone
|
||||
*/
|
||||
function cloneEntry(entry: ILookupTreeEntry): ILookupTreeEntry {
|
||||
const result: ILookupTreeEntry = {};
|
||||
|
||||
if (entry.forward) {
|
||||
result.forward = cloneTree(entry.forward);
|
||||
}
|
||||
|
||||
if (entry.reverse) {
|
||||
result.reverse = cloneTree(entry.reverse);
|
||||
}
|
||||
|
||||
if (entry.lookup) {
|
||||
result.lookup = {
|
||||
contextRange: entry.lookup.contextRange.slice() as [number, number],
|
||||
index: entry.lookup.index,
|
||||
length: entry.lookup.length,
|
||||
subIndex: entry.lookup.subIndex,
|
||||
substitutions: entry.lookup.substitutions.slice()
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones a lookup tree.
|
||||
*
|
||||
* @param tree Lookup tree to clone
|
||||
*/
|
||||
function cloneTree(tree: ILookupTree): ILookupTree {
|
||||
const individual: { [glyphId: string]: ILookupTreeEntry } = {};
|
||||
for (const [glyphId, entry] of Object.entries(tree.individual)) {
|
||||
individual[glyphId] = cloneEntry(entry);
|
||||
}
|
||||
|
||||
return {
|
||||
individual,
|
||||
range: tree.range.map(({ range, entry }) => ({
|
||||
range: range.slice() as [number, number],
|
||||
entry: cloneEntry(entry)
|
||||
}))
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import mergeRange from './mergeRange';
|
||||
|
||||
describe('addon-ligatures - mergeRange', () => {
|
||||
it('inserts a new range before the existing ones', () => {
|
||||
const result = mergeRange([[1, 2], [2, 3]], 0, 1);
|
||||
assert.deepEqual(result, [[0, 1], [1, 2], [2, 3]]);
|
||||
});
|
||||
|
||||
it('inserts in between two ranges', () => {
|
||||
const result = mergeRange([[0, 2], [4, 6]], 2, 4);
|
||||
assert.deepEqual(result, [[0, 2], [2, 4], [4, 6]]);
|
||||
});
|
||||
|
||||
it('inserts after the last range', () => {
|
||||
const result = mergeRange([[0, 2], [4, 6]], 6, 8);
|
||||
assert.deepEqual(result, [[0, 2], [4, 6], [6, 8]]);
|
||||
});
|
||||
|
||||
it('extends the beginning of a range', () => {
|
||||
const result = mergeRange([[0, 2], [4, 6]], 3, 5);
|
||||
assert.deepEqual(result, [[0, 2], [3, 6]]);
|
||||
});
|
||||
|
||||
it('extends the end of a range', () => {
|
||||
const result = mergeRange([[0, 2], [4, 6]], 1, 4);
|
||||
assert.deepEqual(result, [[0, 4], [4, 6]]);
|
||||
});
|
||||
|
||||
it('extends the last range', () => {
|
||||
const result = mergeRange([[0, 2], [4, 6]], 5, 7);
|
||||
assert.deepEqual(result, [[0, 2], [4, 7]]);
|
||||
});
|
||||
|
||||
it('connects two ranges', () => {
|
||||
const result = mergeRange([[0, 2], [4, 6]], 1, 5);
|
||||
assert.deepEqual(result, [[0, 6]]);
|
||||
});
|
||||
|
||||
it('connects more than two ranges', () => {
|
||||
const result = mergeRange([[0, 2], [4, 6], [8, 10], [12, 14]], 1, 10);
|
||||
assert.deepEqual(result, [[0, 10], [12, 14]]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Merges the range defined by the provided start and end into the list of
|
||||
* existing ranges. The merge is done in place on the existing range for
|
||||
* performance and is also returned.
|
||||
*
|
||||
* @param ranges Existing range list
|
||||
* @param newRangeStart Start position of the range to merge, inclusive
|
||||
* @param newRangeEnd End position of range to merge, exclusive
|
||||
*/
|
||||
export default function mergeRange(ranges: [number, number][], newRangeStart: number, newRangeEnd: number): [number, number][] {
|
||||
let inRange = false;
|
||||
for (let i = 0; i < ranges.length; i++) {
|
||||
const range = ranges[i];
|
||||
if (!inRange) {
|
||||
if (newRangeEnd <= range[0]) {
|
||||
// Case 1: New range is before the search range
|
||||
ranges.splice(i, 0, [newRangeStart, newRangeEnd]);
|
||||
return ranges;
|
||||
}
|
||||
if (newRangeEnd <= range[1]) {
|
||||
// Case 2: New range is either wholly contained within the
|
||||
// search range or overlaps with the front of it
|
||||
range[0] = Math.min(newRangeStart, range[0]);
|
||||
return ranges;
|
||||
}
|
||||
if (newRangeStart < range[1]) {
|
||||
// Case 3: New range either wholly contains the search range
|
||||
// or overlaps with the end of it
|
||||
range[0] = Math.min(newRangeStart, range[0]);
|
||||
inRange = true;
|
||||
} else {
|
||||
// Case 4: New range starts after the search range
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
if (newRangeEnd <= range[0]) {
|
||||
// Case 5: New range extends from previous range but doesn't
|
||||
// reach the current one
|
||||
ranges[i - 1][1] = newRangeEnd;
|
||||
return ranges;
|
||||
}
|
||||
if (newRangeEnd <= range[1]) {
|
||||
// Case 6: New range extends from prvious range into the
|
||||
// current range
|
||||
ranges[i - 1][1] = Math.max(newRangeEnd, range[1]);
|
||||
ranges.splice(i, 1);
|
||||
inRange = false;
|
||||
return ranges;
|
||||
}
|
||||
// Case 7: New range extends from previous range past the
|
||||
// end of the current range
|
||||
ranges.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
if (inRange) {
|
||||
// Case 8: New range extends past the last existing range
|
||||
ranges[ranges.length - 1][1] = newRangeEnd;
|
||||
} else {
|
||||
// Case 9: New range starts after the last existing range
|
||||
ranges.push([newRangeStart, newRangeEnd]);
|
||||
}
|
||||
|
||||
return ranges;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { ChainingContextualSubstitutionTable, Lookup } from '../tables';
|
||||
import { ILookupTree } from '../types';
|
||||
|
||||
import { listGlyphsByIndex } from './coverage';
|
||||
import { processInputPosition, processLookaheadPosition, processBacktrackPosition, getInputTree, IEntryMeta } from './helper';
|
||||
|
||||
/**
|
||||
* Build lookup tree for GSUB lookup table 6, format 1.
|
||||
* https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#61-chaining-context-substitution-format-1-simple-glyph-contexts
|
||||
*
|
||||
* @param table JSON representation of the table
|
||||
* @param lookups List of lookup tables
|
||||
* @param tableIndex Index of this table in the overall lookup
|
||||
*/
|
||||
export default function buildTree(table: ChainingContextualSubstitutionTable.IFormat1, lookups: Lookup[], tableIndex: number): ILookupTree {
|
||||
const result: ILookupTree = {
|
||||
individual: {},
|
||||
range: []
|
||||
};
|
||||
|
||||
const firstGlyphs = listGlyphsByIndex(table.coverage);
|
||||
|
||||
for (const { glyphId, index } of firstGlyphs) {
|
||||
const chainRuleSet = table.chainRuleSets[index];
|
||||
|
||||
// If the chain rule set is null there's nothing to do with this table.
|
||||
if (!chainRuleSet) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const [subIndex, subTable] of chainRuleSet.entries()) {
|
||||
let currentEntries: IEntryMeta[] = getInputTree(
|
||||
result,
|
||||
subTable.lookupRecords,
|
||||
lookups,
|
||||
0,
|
||||
glyphId
|
||||
).map(({ entry, substitution }) => ({ entry, substitutions: [substitution] }));
|
||||
|
||||
// We walk forward, then backward
|
||||
for (const [index, glyph] of subTable.input.entries()) {
|
||||
currentEntries = processInputPosition(
|
||||
[glyph],
|
||||
index + 1,
|
||||
currentEntries,
|
||||
subTable.lookupRecords,
|
||||
lookups
|
||||
);
|
||||
}
|
||||
|
||||
for (const glyph of subTable.lookahead) {
|
||||
currentEntries = processLookaheadPosition(
|
||||
[glyph],
|
||||
currentEntries
|
||||
);
|
||||
}
|
||||
|
||||
for (const glyph of subTable.backtrack) {
|
||||
currentEntries = processBacktrackPosition(
|
||||
[glyph],
|
||||
currentEntries
|
||||
);
|
||||
}
|
||||
|
||||
// When we get to the end, insert the lookup information
|
||||
for (const { entry, substitutions } of currentEntries) {
|
||||
entry.lookup = {
|
||||
substitutions,
|
||||
length: subTable.input.length + 1,
|
||||
index: tableIndex,
|
||||
subIndex,
|
||||
contextRange: [
|
||||
-1 * subTable.backtrack.length,
|
||||
1 + subTable.input.length + subTable.lookahead.length
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { ChainingContextualSubstitutionTable, Lookup } from '../tables';
|
||||
import { ILookupTree } from '../types';
|
||||
import mergeTrees from '../merge';
|
||||
|
||||
import { listGlyphsByIndex } from './coverage';
|
||||
import getGlyphClass, { listClassGlyphs } from './classDef';
|
||||
import { processInputPosition, processLookaheadPosition, processBacktrackPosition, getInputTree, IEntryMeta } from './helper';
|
||||
|
||||
/**
|
||||
* Build lookup tree for GSUB lookup table 6, format 2.
|
||||
* https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#62-chaining-context-substitution-format-2-class-based-glyph-contexts
|
||||
*
|
||||
* @param table JSON representation of the table
|
||||
* @param lookups List of lookup tables
|
||||
* @param tableIndex Index of this table in the overall lookup
|
||||
*/
|
||||
export default function buildTree(table: ChainingContextualSubstitutionTable.IFormat2, lookups: Lookup[], tableIndex: number): ILookupTree {
|
||||
const results: ILookupTree[] = [];
|
||||
|
||||
const firstGlyphs = listGlyphsByIndex(table.coverage);
|
||||
|
||||
for (const { glyphId } of firstGlyphs) {
|
||||
const firstInputClass = getGlyphClass(table.inputClassDef, glyphId);
|
||||
for (const [glyphId, inputClass] of firstInputClass.entries()) {
|
||||
// istanbul ignore next - invalid font
|
||||
if (inputClass === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const classSet = table.chainClassSet[inputClass];
|
||||
|
||||
// If the class set is null there's nothing to do with this table.
|
||||
if (!classSet) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const [subIndex, subTable] of classSet.entries()) {
|
||||
const result: ILookupTree = {
|
||||
individual: {},
|
||||
range: []
|
||||
};
|
||||
|
||||
let currentEntries: IEntryMeta[] = getInputTree(
|
||||
result,
|
||||
subTable.lookupRecords,
|
||||
lookups,
|
||||
0,
|
||||
glyphId
|
||||
).map(({ entry, substitution }) => ({ entry, substitutions: [substitution] }));
|
||||
|
||||
for (const [index, classNum] of subTable.input.entries()) {
|
||||
currentEntries = processInputPosition(
|
||||
listClassGlyphs(table.inputClassDef, classNum),
|
||||
index + 1,
|
||||
currentEntries,
|
||||
subTable.lookupRecords,
|
||||
lookups
|
||||
);
|
||||
}
|
||||
|
||||
for (const classNum of subTable.lookahead) {
|
||||
currentEntries = processLookaheadPosition(
|
||||
listClassGlyphs(table.lookaheadClassDef, classNum),
|
||||
currentEntries
|
||||
);
|
||||
}
|
||||
|
||||
for (const classNum of subTable.backtrack) {
|
||||
currentEntries = processBacktrackPosition(
|
||||
listClassGlyphs(table.backtrackClassDef, classNum),
|
||||
currentEntries
|
||||
);
|
||||
}
|
||||
|
||||
// When we get to the end, all of the entries we've accumulated
|
||||
// should have a lookup defined
|
||||
for (const { entry, substitutions } of currentEntries) {
|
||||
entry.lookup = {
|
||||
substitutions,
|
||||
index: tableIndex,
|
||||
subIndex,
|
||||
length: subTable.input.length + 1,
|
||||
contextRange: [
|
||||
-1 * subTable.backtrack.length,
|
||||
1 + subTable.input.length + subTable.lookahead.length
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
results.push(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mergeTrees(results);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { ChainingContextualSubstitutionTable, Lookup } from '../tables';
|
||||
import { ILookupTree } from '../types';
|
||||
|
||||
import { listGlyphsByIndex } from './coverage';
|
||||
import { processInputPosition, processLookaheadPosition, processBacktrackPosition, getInputTree, IEntryMeta } from './helper';
|
||||
|
||||
/**
|
||||
* Build lookup tree for GSUB lookup table 6, format 3.
|
||||
* https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#63-chaining-context-substitution-format-3-coverage-based-glyph-contexts
|
||||
*
|
||||
* @param table JSON representation of the table
|
||||
* @param lookups List of lookup tables
|
||||
* @param tableIndex Index of this table in the overall lookup
|
||||
*/
|
||||
export default function buildTree(table: ChainingContextualSubstitutionTable.IFormat3, lookups: Lookup[], tableIndex: number): ILookupTree {
|
||||
const result: ILookupTree = {
|
||||
individual: {},
|
||||
range: []
|
||||
};
|
||||
|
||||
const firstGlyphs = listGlyphsByIndex(table.inputCoverage[0]);
|
||||
|
||||
for (const { glyphId } of firstGlyphs) {
|
||||
let currentEntries: IEntryMeta[] = getInputTree(
|
||||
result,
|
||||
table.lookupRecords,
|
||||
lookups,
|
||||
0,
|
||||
glyphId
|
||||
).map(({ entry, substitution }) => ({ entry, substitutions: [substitution] }));
|
||||
|
||||
for (const [index, coverage] of table.inputCoverage.slice(1).entries()) {
|
||||
currentEntries = processInputPosition(
|
||||
listGlyphsByIndex(coverage).map(glyph => glyph.glyphId),
|
||||
index + 1,
|
||||
currentEntries,
|
||||
table.lookupRecords,
|
||||
lookups
|
||||
);
|
||||
}
|
||||
|
||||
for (const coverage of table.lookaheadCoverage) {
|
||||
currentEntries = processLookaheadPosition(
|
||||
listGlyphsByIndex(coverage).map(glyph => glyph.glyphId),
|
||||
currentEntries
|
||||
);
|
||||
}
|
||||
|
||||
for (const coverage of table.backtrackCoverage) {
|
||||
currentEntries = processBacktrackPosition(
|
||||
listGlyphsByIndex(coverage).map(glyph => glyph.glyphId),
|
||||
currentEntries
|
||||
);
|
||||
}
|
||||
|
||||
// When we get to the end, all of the entries we've accumulated
|
||||
// should have a lookup defined
|
||||
for (const { entry, substitutions } of currentEntries) {
|
||||
entry.lookup = {
|
||||
substitutions,
|
||||
index: tableIndex,
|
||||
subIndex: 0,
|
||||
length: table.inputCoverage.length,
|
||||
contextRange: [
|
||||
-1 * table.backtrackCoverage.length,
|
||||
table.inputCoverage.length + table.lookaheadCoverage.length
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { IReverseChainingContextualSingleSubstitutionTable } from '../tables';
|
||||
import { ILookupTree, ILookupTreeEntry } from '../types';
|
||||
|
||||
import { listGlyphsByIndex } from './coverage';
|
||||
import { processLookaheadPosition, processBacktrackPosition, IEntryMeta } from './helper';
|
||||
|
||||
/**
|
||||
* Build lookup tree for GSUB lookup table 8, format 1.
|
||||
* https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#81-reverse-chaining-contextual-single-substitution-format-1-coverage-based-glyph-contexts
|
||||
*
|
||||
* @param table JSON representation of the table
|
||||
* @param tableIndex Index of this table in the overall lookup
|
||||
*/
|
||||
export default function buildTree(table: IReverseChainingContextualSingleSubstitutionTable, tableIndex: number): ILookupTree {
|
||||
const result: ILookupTree = {
|
||||
individual: {},
|
||||
range: []
|
||||
};
|
||||
|
||||
const glyphs = listGlyphsByIndex(table.coverage);
|
||||
|
||||
for (const { glyphId, index } of glyphs) {
|
||||
const initialEntry: ILookupTreeEntry = {};
|
||||
if (Array.isArray(glyphId)) {
|
||||
result.range.push({
|
||||
entry: initialEntry,
|
||||
range: glyphId
|
||||
});
|
||||
} else {
|
||||
result.individual[glyphId] = initialEntry;
|
||||
}
|
||||
|
||||
let currentEntries: IEntryMeta[] = [{
|
||||
entry: initialEntry,
|
||||
substitutions: [table.substitutes[index]]
|
||||
}];
|
||||
|
||||
// We walk forward, then backward
|
||||
for (const coverage of table.lookaheadCoverage) {
|
||||
currentEntries = processLookaheadPosition(
|
||||
listGlyphsByIndex(coverage).map(glyph => glyph.glyphId),
|
||||
currentEntries
|
||||
);
|
||||
}
|
||||
|
||||
for (const coverage of table.backtrackCoverage) {
|
||||
currentEntries = processBacktrackPosition(
|
||||
listGlyphsByIndex(coverage).map(glyph => glyph.glyphId),
|
||||
currentEntries
|
||||
);
|
||||
}
|
||||
|
||||
// When we get to the end, insert the lookup information
|
||||
for (const { entry, substitutions } of currentEntries) {
|
||||
entry.lookup = {
|
||||
substitutions,
|
||||
index: tableIndex,
|
||||
subIndex: 0,
|
||||
length: 1,
|
||||
contextRange: [
|
||||
-1 * table.backtrackCoverage.length,
|
||||
1 + table.lookaheadCoverage.length
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { ClassDefTable } from '../tables';
|
||||
|
||||
/**
|
||||
* Get the number of the class to which the glyph belongs, or null if it doesn't
|
||||
* belong to any of them.
|
||||
*
|
||||
* @param table JSON representation of the class def table
|
||||
* @param glyphId Index of the glyph to look for
|
||||
*/
|
||||
export default function getGlyphClass(table: ClassDefTable, glyphId: number | [number, number]): Map<number | [number, number], number | null> {
|
||||
switch (table.format) {
|
||||
// https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#class-definition-table-format-2
|
||||
case 2:
|
||||
if (Array.isArray(glyphId)) {
|
||||
return getRangeGlyphClass(table, glyphId);
|
||||
}
|
||||
return new Map([[
|
||||
glyphId,
|
||||
getIndividualGlyphClass(table, glyphId)
|
||||
]]);
|
||||
// https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#class-definition-table-format-1
|
||||
default:
|
||||
return new Map([[glyphId, null]]);
|
||||
}
|
||||
}
|
||||
|
||||
function getRangeGlyphClass(table: ClassDefTable.IFormat2, glyphId: [number, number]): Map<number | [number, number], number | null> {
|
||||
const classStart: number = glyphId[0];
|
||||
const currentClass: number | null = getIndividualGlyphClass(table, classStart);
|
||||
let search: number = glyphId[0] + 1;
|
||||
|
||||
const result = new Map<[number, number] | number, number | null>();
|
||||
|
||||
while (search < glyphId[1]) {
|
||||
const clazz = getIndividualGlyphClass(table, search);
|
||||
if (clazz !== currentClass) {
|
||||
if (search - classStart <= 1) {
|
||||
result.set(classStart, currentClass);
|
||||
} else {
|
||||
result.set([classStart, search], currentClass);
|
||||
}
|
||||
}
|
||||
search++;
|
||||
}
|
||||
|
||||
if (search - classStart <= 1) {
|
||||
result.set(classStart, currentClass);
|
||||
} else {
|
||||
result.set([classStart, search], currentClass);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getIndividualGlyphClass(table: ClassDefTable.IFormat2, glyphId: number): number | null {
|
||||
for (const range of table.ranges) {
|
||||
if (range.start <= glyphId && range.end >= glyphId) {
|
||||
return range.classId;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function listClassGlyphs(table: ClassDefTable, index: number): (number | [number, number])[] {
|
||||
switch (table.format) {
|
||||
case 2:
|
||||
const results: (number | [number, number])[] = [];
|
||||
for (const range of table.ranges) {
|
||||
if (range.classId !== index) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (range.end === range.start) {
|
||||
results.push(range.start);
|
||||
} else {
|
||||
results.push([range.start, range.end + 1]);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user