Merge remote-tracking branch 'upstream/master' into onSelectionChangeClear

This commit is contained in:
Daniel Imms
2026-01-08 16:51:54 -08:00
77 changed files with 3426 additions and 171 deletions
+5
View File
@@ -62,6 +62,9 @@ jobs:
./addons/addon-web-links/lib/* \
./addons/addon-web-links/out/* \
./addons/addon-web-links/out-*/* \
./addons/addon-web-fonts/lib/* \
./addons/addon-web-fonts/out/* \
./addons/addon-web-fonts/out-*/* \
./addons/addon-webgl/lib/* \
./addons/addon-webgl/out/* \
./addons/addon-webgl/out-*st/*
@@ -223,6 +226,8 @@ jobs:
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-unicode-graphemes
- name: Integration tests (addon-unicode11)
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-unicode11
- name: Integration tests (addon-web-fonts)
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-web-fonts
- name: Integration tests (addon-web-links)
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-web-links
- name: Integration tests (addon-webgl)
-7
View File
@@ -1,7 +0,0 @@
Antonis Kalipetis <akalipetis@sourcelair.com>
Antonis Kalipetis <akalipetis@sourcelair.com> <akalipetis@gmail.com>
Daniel Imms <daimms@microsoft.com> <tyriar@tyriar.com>
Paris Kasidiaris <paris@sourcelair.com>
Paris Kasidiaris <pariskasidiaris@gmail.com>
Thanasis Daglis <thanasis@sourcelair.com>
Thanasis Daglis <thanasis@sourcelair.com> <ath.daglis@gmail.com>
-1
View File
@@ -1 +0,0 @@
NODE_PATH=./out
-5
View File
@@ -1,5 +0,0 @@
require:
- source-map-support/register
reporter: spec
color: true
check-leaks: true
+5
View File
@@ -0,0 +1,5 @@
{
"recommendations": [
"dbaeumer.vscode-eslint"
]
}
-13
View File
@@ -1,17 +1,4 @@
{
"chat.tools.terminal.autoApprove": {
"npm run build": true,
"npm run esbuild": true,
"npm run dev": true,
"npm run test-integration": true,
"npm run test-integration-chromium": true,
"npm run test-integration-firefox": true,
"npm run test-integration-webkit": true,
"npm run lint": true,
"npm run lint-fix": true,
"npm run lint-api": true,
"npm run test-unit": true,
},
"typescript.preferences.importModuleSpecifier": "non-relative",
"typescript.preferences.quoteStyle": "single"
}
+27 -28
View File
@@ -1,27 +1,27 @@
# [![xterm.js logo](images/logo-full.png)](https://xtermjs.org)
# [![xterm.js](images/logo-full.png)](https://xtermjs.org)
Xterm.js is a front-end component written in TypeScript that lets applications bring fully-featured terminals to their users in the browser. It's used by popular projects such as VS Code, Hyper and Theia.
Xterm.js is a frontend component that enables applications to bring fully-featured terminals to their users in the browser. It's used by popular projects such as [VS Code](https://code.visualstudio.com/) (and its forks), [Tabby](https://tabby.sh/) and [Hyper](https://hyper.is/).
## Features
- **Terminal apps just work**: Xterm.js works with most terminal apps such as `bash`, `vim`, and `tmux`, including support for curses-based apps and mouse events.
- **Performant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer.
- **Performant**: Xterm.js is *really* fast and includes an optional GPU-accelerated renderer.
- **Rich Unicode support**: Supports CJK, emojis, and IMEs.
- **Self-contained**: Requires zero dependencies to work.
- **Accessible**: Screen reader and minimum contrast ratio support can be turned on.
- **And much more**: Links, theming, addons, well documented API, etc.
- **Self-contained**: The core library has zero dependencies.
- **Accessible**: Screen reader mode and minimum contrast ratio support can be turned on.
- **And much more**: Links, theming, custom glyphs, addons, well documented API, etc.
## What xterm.js is not
- Xterm.js is not a terminal application that you can download and use on your computer.
- Xterm.js is not `bash`. Xterm.js can be connected to processes like `bash` and let you interact with them (provide input, receive output).
- Xterm.js is not `bash`. Xterm.js can be connected to processes like `bash` and let you interact with them (provide input, receive output) through a library like [node-pty](https://github.com/microsoft/node-pty).
## Getting Started
First, you need to install the module, we ship exclusively through [npm](https://www.npmjs.com/), so you need that installed and then add xterm.js as a dependency by running:
First, you need to install the module. We ship exclusively through [npm](https://www.npmjs.com), so you need that installed and then add [@xterm/xterm](https://www.npmjs.com/package/@xterm/xterm) as a dependency by running:
```bash
npm install @xterm/xterm
npm install --save @xterm/xterm
```
To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to the head of your HTML page. Then create a `<div id="terminal"></div>` onto which xterm can attach itself. Finally, instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`.
@@ -54,12 +54,10 @@ import { Terminal } from '@xterm/xterm';
### Addons
⚠️ *This section describes the new addon format introduced in v3.14.0, see [here](https://github.com/xtermjs/xterm.js/blob/3.14.2/README.md#addons) for the instructions on the old format*
Addons are separate modules that extend the `Terminal` by building on the [xterm.js API](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts). To use an addon, you first need to install it in your project:
```bash
npm i -S @xterm/addon-web-links
npm install --save @xterm/addon-web-links
```
Then import the addon, instantiate it and call `Terminal.loadAddon`:
@@ -80,50 +78,51 @@ The xterm.js team maintains the following addons, but anyone can build them:
- [`@xterm/addon-clipboard`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-clipboard): Access the browser's clipboard
- [`@xterm/addon-fit`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-fit): Fits the terminal to the containing element
- [`@xterm/addon-image`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-image): Adds image support
- [`@xterm/addon-ligatures`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-ligatures): Enables rendering of ligatures
- [`@xterm/addon-progress`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-progress): Adds support for the progress API (`OSC 9;4`)
- [`@xterm/addon-search`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-search): Adds search functionality
- [`@xterm/addon-serialize`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-serialize): Serializes the terminal's buffer to a VT sequences or HTML
- [`@xterm/addon-serialize`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-serialize): Serializes the terminal's buffer to VT sequences or HTML
- [`@xterm/addon-unicode-graphemes`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-unicode-graphemes): Enhanced unicode support including grapheme clustering (experimental)
- [`@xterm/addon-unicode11`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-unicode11): Updates character widths to their unicode11 values
- [`@xterm/addon-web-fonts`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-web-fonts): Easily integrate web fonts
- [`@xterm/addon-web-links`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-web-links): Adds web link detection and interaction
- [`@xterm/addon-webgl`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-webgl): Renders xterm.js using a `canvas` element's webgl2 context
## Browser Support
Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Specifically the latest versions of *Chrome*, *Edge*, *Firefox*, and *Safari*.
Xterm.js works seamlessly in [Electron](https://electronjs.org/) apps and may even work on earlier versions of the browsers. These are the versions we strive to keep working.
Since xterm.js is typically implemented as a developer tool, generally only modern evergreen browsers are supported officially. Specifically the latest versions of *Chrome*, *Edge*, *Firefox*, and *Safari*. Xterm.js also works seamlessly in [Electron](https://electronjs.org/) apps and may even work on earlier versions of the browsers. These are the versions we strive to keep working.
### Node.js Support
We also publish [`xterm-headless`](https://www.npmjs.com/package/xterm-headless) which is a stripped down version of xterm.js that runs in Node.js. An example use case for this is to keep track of a terminal's state where the process is running and using the serialize addon so it can get all state restored upon reconnection.
We also publish [`xterm-headless`](https://www.npmjs.com/package/xterm-headless) which is a stripped down version of xterm.js that runs headless in Node.js. An example use case for this is to keep track of a terminal's state where the process is running and using the [serialize addon](https://www.npmjs.com/package/@xterm/addon-serialize) so it can get all state restored upon reconnection.
## API
The full API for xterm.js is contained within the [TypeScript declaration file](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), use the branch/tag picker in GitHub (`w`) to navigate to the correct version of the API.
Note that some APIs are marked *experimental*, these are added to enable experimentation with new ideas without committing to support it like a normal [semver](https://semver.org/) API. Note that these APIs can change radically between versions, so be sure to read release notes if you plan on using experimental APIs.
Some APIs may be marked with *experimental*, these are added to enable experimentation with new ideas without committing to support it like a normal [semver](https://semver.org/) API. Note that these APIs can change radically between versions, so be sure to read release notes if you plan on using experimental APIs.
## Releases
Xterm.js follows a monthly release cycle roughly.
All current and past releases are available on this repo's [Releases page](https://github.com/sourcelair/xterm.js/releases), you can view the [high-level roadmap on the wiki](https://github.com/xtermjs/xterm.js/wiki/Roadmap) and see what we're working on now by looking through [Milestones](https://github.com/sourcelair/xterm.js/milestones).
Stable releases are done on an as needed basis. All current and past releases are available on this repo's [releases page](https://github.com/sourcelair/xterm.js/releases), you can see what's planned for upcoming releases looking through the repository [milestones](https://github.com/sourcelair/xterm.js/milestones).
### Beta builds
Our CI releases beta builds to npm for every change that goes into master. Install the latest beta build with:
Beta releases are continuously published off the `master` branch. Install the latest beta build with:
```bash
npm install -S @xterm/xterm@beta
npm install --save @xterm/xterm@beta
```
These should generally be stable, but some bugs may slip in. We recommend using the beta build primarily to test out new features and to verify bug fixes.
The principal implementation (VS Code) typically uses the latest or near the latest beta build. Generally they are quite stable but can potentially contain bugs or breaking changes. If stability is very important we recommend using the beta build primarily to test out new features and to verify bug fixes, unless you're tracking what's landing and are comfortable taking that risk.
## Contributing
You can read the [guide on the wiki](https://github.com/xtermjs/xterm.js/wiki/Contributing) to learn how to contribute and set up xterm.js for development.
Read [CONTRIBUTING.md](https://github.com/xtermjs/xterm.js/blob/master/CONTRIBUTING.md) to learn how to contribute to the project.
## Real-world uses
Xterm.js is used in several world-class applications to provide great terminal experiences.
Xterm.js is used in many world-class applications to provide great terminal experiences.
- [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on xterm.js.
- [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile, and powerful open source code editor that provides an integrated terminal based on xterm.js.
@@ -232,12 +231,12 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**EmuDevz**](https://afska.github.io/emudevz): A free coding game where players learn how to build an emulator from scratch.
- [And much more...](https://github.com/xtermjs/xterm.js/network/dependents?package_id=UGFja2FnZS0xNjYzMjc4OQ%3D%3D)
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it on our list. Note: Please add any new contributions to the end of the list only.
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it on our list. Please add any new contributions to the end of the list.
## License Agreement
If you contribute code to this project, you implicitly allow your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work.
Copyright (c) 2017-2022, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)<br>
Copyright (c) 2017-2026, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)<br>
Copyright (c) 2014-2017, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License)<br>
Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
-1
View File
@@ -3,7 +3,6 @@ node_modules/
coverage/
lib/
fonts/
.env
.vscode/
+25 -1
View File
@@ -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.
+5 -2
View File
@@ -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"
}
+1 -1
View File
@@ -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,40 @@
import { ILookupTree, IFlattenedLookupTree, ILookupTreeEntry, IFlattenedLookupTreeEntry } from './types';
export default function flatten(tree: ILookupTree, visited: Map<ILookupTreeEntry, IFlattenedLookupTreeEntry> = new Map()): IFlattenedLookupTree {
const result: IFlattenedLookupTree = {};
for (const [glyphId, entry] of Object.entries(tree.individual)) {
result[glyphId] = flattenEntry(entry, visited);
}
for (const { range, entry } of tree.range) {
const flattened = flattenEntry(entry, visited);
for (let glyphId = range[0]; glyphId < range[1]; glyphId++) {
result[glyphId] = flattened;
}
}
return result;
}
function flattenEntry(entry: ILookupTreeEntry, visited: Map<ILookupTreeEntry, IFlattenedLookupTreeEntry>): IFlattenedLookupTreeEntry {
if (visited.has(entry)) {
return visited.get(entry)!;
}
const result: IFlattenedLookupTreeEntry = {};
visited.set(entry, result);
if (entry.forward) {
result.forward = flatten(entry.forward, visited);
}
if (entry.reverse) {
result.reverse = flatten(entry.reverse, visited);
}
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,393 @@
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: []
};
const mergedEntries = new WeakMap<ILookupTreeEntry, Set<ILookupTreeEntry>>();
for (const tree of trees) {
mergeSubtree(result, tree, mergedEntries);
}
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
* @param mergedEntries WeakMap to track already merged entry pairs
*/
function mergeSubtree(mainTree: ILookupTree, mergeTree: ILookupTree, mergedEntries: WeakMap<ILookupTreeEntry, Set<ILookupTreeEntry>>): 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, mergedEntries);
} 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), mergedEntries);
// 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), mergedEntries);
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), mergedEntries);
// 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), mergedEntries);
// Update the remaining ranges
remainingRanges.splice(remainingIndex, 1, ...overlap.second);
break;
} else {
if (Number(glyphId) === remainingRange) {
mergeTreeEntry(mainTree.individual[glyphId], cloneEntry(entry), mergedEntries);
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
* @param mergedEntries WeakMap to track already merged entry pairs
*/
function mergeTreeEntry(mainTree: ILookupTreeEntry, mergeTree: ILookupTreeEntry, mergedEntries: WeakMap<ILookupTreeEntry, Set<ILookupTreeEntry>>): void {
// Check if we've already merged this pair
let mergedSet = mergedEntries.get(mainTree);
if (mergedSet?.has(mergeTree)) {
return;
}
if (!mergedSet) {
mergedSet = new Set();
mergedEntries.set(mainTree, mergedSet);
}
mergedSet.add(mergeTree);
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, mergedEntries);
}
}
if (mergeTree.reverse) {
if (!mainTree.reverse) {
mainTree.reverse = mergeTree.reverse;
} else {
mergeSubtree(mainTree.reverse, mergeTree.reverse, mergedEntries);
}
}
}
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
* @param visited Map to track already cloned entries (prevents infinite loops)
*/
function cloneEntry(entry: ILookupTreeEntry, visited: Map<ILookupTreeEntry, ILookupTreeEntry> = new Map()): ILookupTreeEntry {
if (visited.has(entry)) {
return visited.get(entry)!;
}
const result: ILookupTreeEntry = {};
visited.set(entry, result);
if (entry.forward) {
result.forward = cloneTree(entry.forward, visited);
}
if (entry.reverse) {
result.reverse = cloneTree(entry.reverse, visited);
}
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
* @param visited Map to track already cloned entries (prevents infinite loops)
*/
function cloneTree(tree: ILookupTree, visited: Map<ILookupTreeEntry, ILookupTreeEntry> = new Map()): ILookupTree {
const individual: { [glyphId: string]: ILookupTreeEntry } = {};
for (const [glyphId, entry] of Object.entries(tree.individual)) {
individual[glyphId] = cloneEntry(entry, visited);
}
return {
individual,
range: tree.range.map(({ range, entry }) => ({
range: range.slice() as [number, number],
entry: cloneEntry(entry, visited)
}))
};
}

Some files were not shown because too many files have changed in this diff Show More