From 15664de472b3b556e7976766b414c9d1f22b81d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 3 Oct 2024 21:50:34 +0200 Subject: [PATCH 01/38] initial --- .eslintrc.json | 2 + addons/addon-web-fonts/LICENSE | 19 ++ addons/addon-web-fonts/README.md | 137 ++++++++ addons/addon-web-fonts/package.json | 29 ++ addons/addon-web-fonts/src/WebFontsAddon.ts | 299 ++++++++++++++++++ addons/addon-web-fonts/src/tsconfig.json | 28 ++ .../test/WebLinksAddon.test.ts | 185 +++++++++++ .../addon-web-fonts/test/playwright.config.ts | 35 ++ addons/addon-web-fonts/test/tsconfig.json | 42 +++ addons/addon-web-fonts/tsconfig.json | 8 + .../typings/addon-web-fonts.d.ts | 18 ++ addons/addon-web-fonts/webpack.config.js | 33 ++ demo/client.ts | 33 +- demo/style.css | 112 +++++++ demo/tsconfig.json | 1 + tsconfig.all.json | 1 + 16 files changed, 970 insertions(+), 12 deletions(-) create mode 100644 addons/addon-web-fonts/LICENSE create mode 100644 addons/addon-web-fonts/README.md create mode 100644 addons/addon-web-fonts/package.json create mode 100644 addons/addon-web-fonts/src/WebFontsAddon.ts create mode 100644 addons/addon-web-fonts/src/tsconfig.json create mode 100644 addons/addon-web-fonts/test/WebLinksAddon.test.ts create mode 100644 addons/addon-web-fonts/test/playwright.config.ts create mode 100644 addons/addon-web-fonts/test/tsconfig.json create mode 100644 addons/addon-web-fonts/tsconfig.json create mode 100644 addons/addon-web-fonts/typings/addon-web-fonts.d.ts create mode 100644 addons/addon-web-fonts/webpack.config.js diff --git a/.eslintrc.json b/.eslintrc.json index 8c4759820..3d9322d36 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -33,6 +33,8 @@ "addons/addon-unicode-graphemes/src/tsconfig.json", "addons/addon-unicode-graphemes/test/tsconfig.json", "addons/addon-unicode-graphemes/benchmark/tsconfig.json", + "addons/addon-web-fonts/src/tsconfig.json", + "addons/addon-web-fonts/test/tsconfig.json", "addons/addon-web-links/src/tsconfig.json", "addons/addon-web-links/test/tsconfig.json", "addons/addon-webgl/src/tsconfig.json", diff --git a/addons/addon-web-fonts/LICENSE b/addons/addon-web-fonts/LICENSE new file mode 100644 index 000000000..447eb79f4 --- /dev/null +++ b/addons/addon-web-fonts/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2024, 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. diff --git a/addons/addon-web-fonts/README.md b/addons/addon-web-fonts/README.md new file mode 100644 index 000000000..bf506cf1c --- /dev/null +++ b/addons/addon-web-fonts/README.md @@ -0,0 +1,137 @@ +## @xterm/addon-web-fonts + +Addon to use webfonts with [xterm.js](https://github.com/xtermjs/xterm.js). This addon requires xterm.js v5+. + +### Install + +```bash +npm install --save @xterm/addon-web-fonts +``` + +### Issue with Webfonts + +Webfonts are announced by CSS `font-face` rules (or its Javascript `FontFace` counterparts). Since font files tend to be quite big assets, browser engines often postpone their loading to an actual styling request of a codepoint matching a font file's `unicode-range`. In short - font files will not be loaded until really needed. + +xterm.js on the other hand heavily relies on exact measurements of character glyphs to layout its output. This is done by determining the glyph width (DOM renderer) or by creating a glyph texture (WebGl renderer) for every output character. +For performance reasons both is done in synchronous code and cached. This logic only works properly, +if a font glyph is available on its first usage, or a wrong glyph from a fallback font chosen by the browser will be used instead. + +For webfonts and xterm.js this means, that we cannot rely on the default loading strategy of the browser, but have to preload the font files before using that font in xterm.js. + + +### Static Preloading for the Rescue? + +If you dont mind higher initial loading times of the embedding document, you can tell the browser to preload the needed font files by placing the following link elements in the document's head above any other CSS/Javascript: +```html + + + ... + +``` +Downside of this approach is the much higher initial loading time showing as a white page. Browsers also will resort to system fonts, if the preloading takes too long, so with a slow connection or a very big font this solves literally nothing. + + +### Preloading with WebFontsAddon + +The webfonts addon offers several ways to deal with the loading of font assets without leaving the terminal in an unusable state. + + +Recap - normally boostrapping of a new terminal involves these basic steps: + +```typescript +import { Terminal } from '@xterm/xterm'; +import { XYAddon } from '@xterm/addon-xy'; + +// create a `Terminal` instance with some options, e.g. a custom font family +const terminal = new Terminal({fontFamily: 'monospace'}); + +// create and load all addons you want to use, e.g. fit addon +const xyAddon = new XYAddon(); +terminal.loadAddon(xyAddon); + +// finally: call `open` of the terminal instance +terminal.open(your_terminal_div_element); // <-- critical path for webfonts +// more boostrapping goes here ... +``` + +This synchronous code is guaranteed to work in all browsers, as the font `monospace` will always be available. +It will also work that way with any installed system font, but breaks horribly for webfonts. The actual culprit here is the call to `terminal.open`, which attaches the terminal to the DOM and starts the renderer with all the glyph caching mentioned above, while the webfont is not fully available yet. + +To fix that, the webfonts addon provides a waiting condition: +```typescript +import { Terminal } from '@xterm/xterm'; +import { XYAddon } from '@xterm/addon-xy'; +import { WebFontsAddon } from '@xterm/addon-web-fonts'; + +// create a `Terminal` instance, now with webfonts +const terminal = new Terminal({fontFamily: '"Web Mono 1", "Super Powerline", monospace'}); +const xyAddon = new XYAddon(); +terminal.loadAddon(xyAddon); + +const webFontsAddon = new WebFontsAddon(); +terminal.loadAddon(webFontsAddon); + +// wait for webfonts to be fully loaded +await WebFontsAddon.loadFonts(['Web Mono 1', 'Super Powerline']).then(() => { + terminal.open(your_terminal_div_element); + // more boostrapping goes here ... +}); +``` +Here `loadFonts` will look up the font face objects in `document.fonts` and load them before continuing. +For this to work, you have to make sure, that the CSS `font-face` rules for these webfonts are loaded +on the initial document load (more precise - by the time this code runs). + +Please note, that this code cannot run synchronous anymore, so you will have to split your +bootstrapping code into several stages. If thats too much of a hassle, you can also move the whole +bootstrapping under that waiting condition (`loadFonts` is actually a static method): +```typescript +import { Terminal } from '@xterm/xterm'; +import { XYAddon } from '@xterm/addon-xy'; +import { WebFontsAddon } from '@xterm/addon-web-fonts'; + +WebFontsAddon.loadFonts(['Web Mono 1', 'Super Powerline']).then(() => { + // create a `Terminal` instance, now with webfonts + const terminal = new Terminal({fontFamily: '"Web Mono 1", "Super Powerline", monospace'}); + const xyAddon = new XYAddon(); + terminal.loadAddon(xyAddon); + + const webFontsAddon = new WebFontsAddon(); + terminal.loadAddon(webFontsAddon); + + terminal.open(your_terminal_div_element); + // more boostrapping goes here ... +}); +``` + +### Webfont Loading at Runtime + +Given you have a terminal already running and want to change the font family to a different not yet loaded webfont. +That can be achieved like this: +```typescript +// either create font face objects in javascript +const ff1 = new FontFace('New Web Mono', url1, ...); +const ff2 = new FontFace('New Web Mono', url2, ...); +// and await their loading +await WebFontsAddon.loadFonts([ff1, ff2]).then(() => { + // apply new webfont to terminal + terminal.options.fontFamily = 'New Web Mono'; + // since the new font might have slighly different metrics, + // also run the fit addon here (or any other custom resize logic) + fitAddon.fit(); +}); + +// or alternatively use CSS to add new font-face rules, e.g. +document.styleSheets[0].insertRule( + "@font-face { font-family: 'New Web Mono'; src: url(newfont.woff); }", 0); +// and await the new font family name +await WebFontsAddon.loadFonts(['New Web Mono']).then(() => { + // apply new webfont to terminal + terminal.options.fontFamily = 'New Web Mono'; + // since the new font might have slighly different metrics, + // also run the fit addon here (or any other custom resize logic) + fitAddon.fit(); +}); +``` + + +See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/addon-web-fonts/typings/addon-web-fonts.d.ts) for more advanced usage. diff --git a/addons/addon-web-fonts/package.json b/addons/addon-web-fonts/package.json new file mode 100644 index 000000000..1daac3768 --- /dev/null +++ b/addons/addon-web-fonts/package.json @@ -0,0 +1,29 @@ +{ + "name": "@xterm/addon-web-fonts", + "version": "0.1.0", + "author": { + "name": "The xterm.js authors", + "url": "https://xtermjs.org/" + }, + "main": "lib/addon-web-fonts.js", + "module": "lib/addon-web-fonts.mjs", + "types": "typings/addon-web-fonts.d.ts", + "repository": "https://github.com/xtermjs/xterm.js/tree/master/addons/addon-web-fonts", + "license": "MIT", + "keywords": [ + "terminal", + "xterm", + "xterm.js" + ], + "scripts": { + "build": "../../node_modules/.bin/tsc -p .", + "prepackage": "npm run build", + "package": "../../node_modules/.bin/webpack", + "prepublishOnly": "npm run package", + "start": "node ../../demo/start" + }, + "peerDependencies": { + "@xterm/xterm": "^5.0.0" + }, + "dependencies": {} +} diff --git a/addons/addon-web-fonts/src/WebFontsAddon.ts b/addons/addon-web-fonts/src/WebFontsAddon.ts new file mode 100644 index 000000000..91800abdb --- /dev/null +++ b/addons/addon-web-fonts/src/WebFontsAddon.ts @@ -0,0 +1,299 @@ +/** + * Copyright (c) 2024 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import type { Terminal, ITerminalAddon } from '@xterm/xterm'; +import type { WebFontsAddon as IWebFontsApi } from '@xterm/addon-web-fonts'; + + +/** + * Unquote family name. + */ +function unquote(s: string): string { + if (s[0] === '"' && s[s.length - 1] === '"') return s.slice(1, -1); + if (s[0] === "'" && s[s.length - 1] === "'") return s.slice(1, -1); + return s; +} + + +/** + * Quote family name. + * @see https://mathiasbynens.be/notes/unquoted-font-family + */ +function quote(s: string): string { + const pos = s.match(/([-_a-zA-Z0-9\xA0-\u{10FFFF}]+)/u); + const neg = s.match(/^(-?\d|--)/m); + if (!neg && pos && pos[1] === s) return s; + return `"${s.replace('"', '\\"')}"`; +} + + +function splitFamily(family: string | undefined): string[] { + if (!family) return []; + return family.split(',').map(e => unquote(e.trim())); +} + + +function createFamily(families: string[]): string { + return families.map(quote).join(', '); +} + + +function _loadFonts(fonts?: (string | FontFace)[]): Promise { + let ffs = Array.from(document.fonts); + if (!fonts || !fonts.length) { + return Promise.all(ffs.map(ff => ff.load())); + } + let toLoad: FontFace[] = []; + let ffsHashed = ffs.map(ff => WebFontsAddon.hashFontFace(ff)); + for (const font of fonts) { + if (font instanceof FontFace) { + const fontHashed = WebFontsAddon.hashFontFace(font); + const idx = ffsHashed.indexOf(fontHashed); + if (idx === -1) { + document.fonts.add(font); + ffs.push(font); + ffsHashed.push(fontHashed); + toLoad.push(font); + } else { + toLoad.push(ffs[idx]); + } + } else { + // string as font + const familyFiltered = ffs.filter(ff => font === unquote(ff.family)); + toLoad = toLoad.concat(familyFiltered); + if (!familyFiltered.length) { + console.warn(`font family "${font}" not registered in document.fonts`); + } + } + } + return Promise.all(toLoad.map(ff => ff.load())); +} + + + +export class WebFontsAddon implements ITerminalAddon, IWebFontsApi { + constructor(public forceInitialRelayout: boolean = true) { } + public dispose(): void { } + + public activate(terminal: Terminal): void { + if (this.forceInitialRelayout) { + document.fonts.ready.then(() => this.relayout(terminal)); + } + } + + /** + * Force a terminal re-layout by altering `options.FontFamily`. + * + * Found webfonts in `fontFamily` are temporarily removed until the webfont + * resources are fully loaded. + * + * This method is meant as a fallback fix for sloppy integrations, + * that wrongly placed a webfont at the terminal contructor options. + * It is likely to lead to terminal flickering in all browsers (FOUT). + * + * To avoid triggering this fallback in your integration, make sure to have + * the needed webfonts loaded at the time `terminal.open` is called. + */ + public relayout(terminal: Terminal): void { + const family = terminal.options.fontFamily; + const families = splitFamily(family); + const webFamilies = WebFontsAddon.getFontFamilies(); + const dirty: string[] = []; + const clean: string[] = []; + for (const fam of families) + (webFamilies.indexOf(fam) !== -1 ? dirty : clean).push(fam); + if (dirty.length) { + _loadFonts(dirty).then(() => { + terminal.options.fontFamily = clean.length ? createFamily(clean) : 'monospace'; + terminal.options.fontFamily = family; + }); + } + } + + /** + * Hash a font face from it properties. + * Used in `loadFonts` to avoid bloating + * `document.fonts` from multiple calls. + */ + public static hashFontFace(ff: FontFace): string { + return JSON.stringify([ + unquote(ff.family), + ff.stretch, + ff.style, + ff.unicodeRange, + ff.weight, + ]) + } + + /** + * Return font families known in `document.fonts`. + */ + public static getFontFamilies(): string[] { + return Array.from(new Set(Array.from(document.fonts).map(e => unquote(e.family)))); + } + + /** + * Wait for webfont resources to be loaded. + * + * Without any argument, all fonts currently listed in + * `document.fonts` will be loaded. + * For a more fine-grained loading strategy you can populate + * the `fonts` argument with: + * - font families : loads all fontfaces in `document.fonts` + * matching the family names + * - fontface objects : loads given fontfaces and adds them to + * `document.fonts` + * + * The returned promise will resolve, when all loading is done. + */ + public static loadFonts(fonts?: (string | FontFace)[]): Promise { + return document.fonts.ready.then(() => _loadFonts(fonts)); + } +} + + + + + + + + +// TODO: place into test cases +/* +(window as any).__roboto = [ + // cyrillic-ext + new FontFace( + 'Roboto Mono', + "url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm3CWWoKC.woff2) format('woff2')", + { + style: 'italic', + weight: '100 700', + display: 'swap', + unicodeRange: 'U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F' + } + ), + // cyrillic + new FontFace( + 'Roboto Mono', + "url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm3mWWoKC.woff2) format('woff2')", + { + style: 'italic', + weight: '100 700', + display: 'swap', + unicodeRange: 'U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116' + } + ), + // greek + new FontFace( + 'Roboto Mono', + "url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm36WWoKC.woff2) format('woff2')", + { + style: 'italic', + weight: '100 700', + display: 'swap', + unicodeRange: 'U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF' + } + ), + // vietnamese + new FontFace( + 'Roboto Mono', + "url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm3KWWoKC.woff2) format('woff2')", + { + style: 'italic', + weight: '100 700', + display: 'swap', + unicodeRange: 'U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB' + } + ), + // latin-ext + new FontFace( + 'Roboto Mono', + "url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm3OWWoKC.woff2) format('woff2')", + { + style: 'italic', + weight: '100 700', + display: 'swap', + unicodeRange: 'U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF' + } + ), + // latin + new FontFace( + 'Roboto Mono', + "url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm32WWg.woff2) format('woff2')", + { + style: 'italic', + weight: '100 700', + display: 'swap', + unicodeRange: 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD' + } + ), + // cyrillic-ext + new FontFace( + 'Roboto Mono', + "url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhGq3-OXg.woff2) format('woff2')", + { + style: 'normal', + weight: '100 700', + display: 'swap', + unicodeRange: 'U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F' + } + ), + // cyrillic + new FontFace( + 'Roboto Mono', + "url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhPq3-OXg.woff2) format('woff2')", + { + style: 'normal', + weight: '100 700', + display: 'swap', + unicodeRange: 'U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116' + } + ), + // greek + new FontFace( + 'Roboto Mono', + "url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhIq3-OXg.woff2) format('woff2')", + { + style: 'normal', + weight: '100 700', + display: 'swap', + unicodeRange: 'U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF' + } + ), + // vietnamese + new FontFace( + 'Roboto Mono', + "url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhEq3-OXg.woff2) format('woff2')", + { + style: 'normal', + weight: '100 700', + display: 'swap', + unicodeRange: 'U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB' + } + ), + // latin-ext + new FontFace( + 'Roboto Mono', + "url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhFq3-OXg.woff2) format('woff2')", + { + style: 'normal', + weight: '100 700', + display: 'swap', + unicodeRange: 'U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF' + } + ), + // latin + new FontFace( + 'Roboto Mono', + "url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhLq38.woff2) format('woff2')", + { + style: 'normal', + weight: '100 700', + display: 'swap', + unicodeRange: 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD' + } + ), +]; +*/ \ No newline at end of file diff --git a/addons/addon-web-fonts/src/tsconfig.json b/addons/addon-web-fonts/src/tsconfig.json new file mode 100644 index 000000000..cd297308f --- /dev/null +++ b/addons/addon-web-fonts/src/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es2021", + "lib": [ + "dom", + "es2015", + "dom.iterable" + ], + "rootDir": ".", + "outDir": "../out", + "sourceMap": true, + "removeComments": true, + "strict": true, + "types": [ + "../../../node_modules/@types/mocha" + ], + "paths": { + "@xterm/addon-web-fonts": [ + "../typings/addon-web-fonts.d.ts" + ] + } + }, + "include": [ + "./**/*", + "../../../typings/xterm.d.ts" + ] +} diff --git a/addons/addon-web-fonts/test/WebLinksAddon.test.ts b/addons/addon-web-fonts/test/WebLinksAddon.test.ts new file mode 100644 index 000000000..8682c5335 --- /dev/null +++ b/addons/addon-web-fonts/test/WebLinksAddon.test.ts @@ -0,0 +1,185 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ +import test from '@playwright/test'; +import { deepStrictEqual, strictEqual } from 'assert'; +import { readFile } from 'fs'; +import { resolve } from 'path'; +import { ITestContext, createTestContext, openTerminal, pollFor, timeout } from '../../../test/playwright/TestUtils'; + +interface ILinkStateData { + uri?: string; + range?: { + start: { + x: number; + y: number; + }; + end: { + x: number; + y: number; + }; + }; +} + + +let ctx: ITestContext; +test.beforeAll(async ({ browser }) => { + ctx = await createTestContext(browser); + await openTerminal(ctx, { cols: 40 }); +}); +test.afterAll(async () => await ctx.page.close()); + +test.describe('WebLinksAddon', () => { + + test.beforeEach(async () => { + await ctx.page.evaluate(` + window.term.reset() + window._linkaddon?.dispose(); + window._linkaddon = new WebLinksAddon(); + window.term.loadAddon(window._linkaddon); + `); + }); + + const countryTlds = [ + '.ac', '.ad', '.ae', '.af', '.ag', '.ai', '.al', '.am', '.ao', '.aq', '.ar', '.as', '.at', + '.au', '.aw', '.ax', '.az', '.ba', '.bb', '.bd', '.be', '.bf', '.bg', '.bh', '.bi', '.bj', + '.bm', '.bn', '.bo', '.bq', '.br', '.bs', '.bt', '.bw', '.by', '.bz', '.ca', '.cc', '.cd', + '.cf', '.cg', '.ch', '.ci', '.ck', '.cl', '.cm', '.cn', '.co', '.cr', '.cu', '.cv', '.cw', + '.cx', '.cy', '.cz', '.de', '.dj', '.dk', '.dm', '.do', '.dz', '.ec', '.ee', '.eg', '.eh', + '.er', '.es', '.et', '.eu', '.fi', '.fj', '.fk', '.fm', '.fo', '.fr', '.ga', '.gd', '.ge', + '.gf', '.gg', '.gh', '.gi', '.gl', '.gm', '.gn', '.gp', '.gq', '.gr', '.gs', '.gt', '.gu', + '.gw', '.gy', '.hk', '.hm', '.hn', '.hr', '.ht', '.hu', '.id', '.ie', '.il', '.im', '.in', + '.io', '.iq', '.ir', '.is', '.it', '.je', '.jm', '.jo', '.jp', '.ke', '.kg', '.kh', '.ki', + '.km', '.kn', '.kp', '.kr', '.kw', '.ky', '.kz', '.la', '.lb', '.lc', '.li', '.lk', '.lr', + '.ls', '.lt', '.lu', '.lv', '.ly', '.ma', '.mc', '.md', '.me', '.mg', '.mh', '.mk', '.ml', + '.mm', '.mn', '.mo', '.mp', '.mq', '.mr', '.ms', '.mt', '.mu', '.mv', '.mw', '.mx', '.my', + '.mz', '.na', '.nc', '.ne', '.nf', '.ng', '.ni', '.nl', '.no', '.np', '.nr', '.nu', '.nz', + '.om', '.pa', '.pe', '.pf', '.pg', '.ph', '.pk', '.pl', '.pm', '.pn', '.pr', '.ps', '.pt', + '.pw', '.py', '.qa', '.re', '.ro', '.rs', '.ru', '.rw', '.sa', '.sb', '.sc', '.sd', '.se', + '.sg', '.sh', '.si', '.sk', '.sl', '.sm', '.sn', '.so', '.sr', '.ss', '.st', '.su', '.sv', + '.sx', '.sy', '.sz', '.tc', '.td', '.tf', '.tg', '.th', '.tj', '.tk', '.tl', '.tm', '.tn', + '.to', '.tr', '.tt', '.tv', '.tw', '.tz', '.ua', '.ug', '.uk', '.us', '.uy', '.uz', '.va', + '.vc', '.ve', '.vg', '.vi', '.vn', '.vu', '.wf', '.ws', '.ye', '.yt', '.za', '.zm', '.zw' + ]; + for (const tld of countryTlds) { + test(tld, async () => await testHostName(`foo${tld}`)); + } + test(`.com`, async () => await testHostName(`foo.com`)); + for (const tld of countryTlds) { + test(`.com${tld}`, async () => await testHostName(`foo.com${tld}`)); + } + + test.describe('correct buffer offsets & uri', () => { + test.beforeEach(async () => { + await ctx.page.evaluate(` + window._linkStateData = {uri:''}; + window._linkaddon._options.hover = (event, uri, range) => { window._linkStateData = { uri, range }; }; + `); + }); + test('all half width', async () => { + await ctx.proxy.write('aaa http://example.com aaa http://example.com aaa'); + await resetAndHover(5, 0); + await evalLinkStateData('http://example.com', { start: { x: 5, y: 1 }, end: { x: 22, y: 1 } }); + await resetAndHover(1, 1); + await evalLinkStateData('http://example.com', { start: { x: 28, y: 1 }, end: { x: 5, y: 2 } }); + }); + test('url after full width', async () => { + await ctx.proxy.write('¥¥¥ http://example.com ¥¥¥ http://example.com aaa'); + await resetAndHover(8, 0); + await evalLinkStateData('http://example.com', { start: { x: 8, y: 1 }, end: { x: 25, y: 1 } }); + await resetAndHover(1, 1); + await evalLinkStateData('http://example.com', { start: { x: 34, y: 1 }, end: { x: 11, y: 2 } }); + }); + test('full width within url and before', async () => { + await ctx.proxy.write('¥¥¥ https://ko.wikipedia.org/wiki/위키백과:대문 aaa https://ko.wikipedia.org/wiki/위키백과:대문 ¥¥¥'); + await resetAndHover(8, 0); + await evalLinkStateData('https://ko.wikipedia.org/wiki/위키백과:대문', { start: { x: 8, y: 1 }, end: { x: 11, y: 2 } }); + await resetAndHover(1, 1); + await evalLinkStateData('https://ko.wikipedia.org/wiki/위키백과:대문', { start: { x: 8, y: 1 }, end: { x: 11, y: 2 } }); + await resetAndHover(17, 1); + await evalLinkStateData('https://ko.wikipedia.org/wiki/위키백과:대문', { start: { x: 17, y: 2 }, end: { x: 19, y: 3 } }); + }); + test('name + password url after full width and combining', async () => { + await ctx.proxy.write('¥¥¥cafe\u0301 http://test:password@example.com/some_path'); + await resetAndHover(12, 0); + await evalLinkStateData('http://test:password@example.com/some_path', { start: { x: 12, y: 1 }, end: { x: 13, y: 2 } }); + await resetAndHover(5, 1); + await evalLinkStateData('http://test:password@example.com/some_path', { start: { x: 12, y: 1 }, end: { x: 13, y: 2 } }); + }); + test('url encoded params work properly', async () => { + await ctx.proxy.write('¥¥¥cafe\u0301 http://test:password@example.com/some_path?param=1%202%3'); + await resetAndHover(12, 0); + await evalLinkStateData('http://test:password@example.com/some_path?param=1%202%3', { start: { x: 12, y: 1 }, end: { x: 27, y: 2 } }); + await resetAndHover(5, 1); + await evalLinkStateData('http://test:password@example.com/some_path?param=1%202%3', { start: { x: 12, y: 1 }, end: { x: 27, y: 2 } }); + }); + }); + + // issue #4964 + test('uppercase in protocol and host, default ports', async () => { + await ctx.proxy.write( + ` HTTP://EXAMPLE.COM \r\n` + + ` HTTPS://Example.com \r\n` + + ` HTTP://Example.com:80 \r\n` + + ` HTTP://Example.com:80/staysUpper \r\n` + + ` HTTP://Ab:xY@abc.com:80/staysUpper \r\n` + ); + await pollForLinkAtCell(3, 0, `HTTP://EXAMPLE.COM`); + await pollForLinkAtCell(3, 1, `HTTPS://Example.com`); + await pollForLinkAtCell(3, 2, `HTTP://Example.com:80`); + await pollForLinkAtCell(3, 3, `HTTP://Example.com:80/staysUpper`); + await pollForLinkAtCell(3, 4, `HTTP://Ab:xY@abc.com:80/staysUpper`); + }); +}); + +async function testHostName(hostname: string): Promise { + await ctx.proxy.write( + ` http://${hostname} \r\n` + + ` http://${hostname}/a~b#c~d?e~f \r\n` + + ` http://${hostname}/colon:test \r\n` + + ` http://${hostname}/colon:test: \r\n` + + `"http://${hostname}/"\r\n` + + `\'http://${hostname}/\'\r\n` + + `http://${hostname}/subpath/+/id` + ); + await pollForLinkAtCell(3, 0, `http://${hostname}`); + await pollForLinkAtCell(3, 1, `http://${hostname}/a~b#c~d?e~f`); + await pollForLinkAtCell(3, 2, `http://${hostname}/colon:test`); + await pollForLinkAtCell(3, 3, `http://${hostname}/colon:test`); + await pollForLinkAtCell(2, 4, `http://${hostname}/`); + await pollForLinkAtCell(2, 5, `http://${hostname}/`); + await pollForLinkAtCell(1, 6, `http://${hostname}/subpath/+/id`); +} + +async function pollForLinkAtCell(col: number, row: number, value: string): Promise { + await ctx.page.mouse.move(...(await cellPos(col, row))); + await pollFor(ctx.page, `!!Array.from(document.querySelectorAll('.xterm-rows > :nth-child(${row+1}) > span[style]')).filter(el => el.style.textDecoration == 'underline').length`, true); + const text = await ctx.page.evaluate(`Array.from(document.querySelectorAll('.xterm-rows > :nth-child(${row+1}) > span[style]')).filter(el => el.style.textDecoration == 'underline').map(el => el.textContent).join('');`); + deepStrictEqual(text, value); +} + +async function resetAndHover(col: number, row: number): Promise { + await ctx.page.mouse.move(0, 0); + await ctx.page.evaluate(`window._linkStateData = {uri:''};`); + await new Promise(r => setTimeout(r, 200)); + await ctx.page.mouse.move(...(await cellPos(col, row))); + await pollFor(ctx.page, `!!window._linkStateData.uri.length`, true); +} + +async function evalLinkStateData(uri: string, range: any): Promise { + const data: ILinkStateData = await ctx.page.evaluate(`window._linkStateData`); + strictEqual(data.uri, uri); + deepStrictEqual(data.range, range); +} + +async function cellPos(col: number, row: number): Promise<[number, number]> { + const coords: any = await ctx.page.evaluate(` + (function() { + const rect = window.term.element.getBoundingClientRect(); + const dim = term._core._renderService.dimensions; + return {left: rect.left, top: rect.top, bottom: rect.bottom, right: rect.right, width: dim.css.cell.width, height: dim.css.cell.height}; + })(); + `); + return [col * coords.width + coords.left + 2, row * coords.height + coords.top + 2]; +} diff --git a/addons/addon-web-fonts/test/playwright.config.ts b/addons/addon-web-fonts/test/playwright.config.ts new file mode 100644 index 000000000..22834be11 --- /dev/null +++ b/addons/addon-web-fonts/test/playwright.config.ts @@ -0,0 +1,35 @@ +import { PlaywrightTestConfig } from '@playwright/test'; + +const config: PlaywrightTestConfig = { + testDir: '.', + timeout: 10000, + projects: [ + { + name: 'ChromeStable', + use: { + browserName: 'chromium', + channel: 'chrome' + } + }, + { + name: 'FirefoxStable', + use: { + browserName: 'firefox' + } + }, + { + name: 'WebKit', + use: { + browserName: 'webkit' + } + } + ], + reporter: 'list', + webServer: { + command: 'npm run start', + port: 3000, + timeout: 120000, + reuseExistingServer: !process.env.CI + } +}; +export default config; diff --git a/addons/addon-web-fonts/test/tsconfig.json b/addons/addon-web-fonts/test/tsconfig.json new file mode 100644 index 000000000..120fccdc3 --- /dev/null +++ b/addons/addon-web-fonts/test/tsconfig.json @@ -0,0 +1,42 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es2021", + "lib": [ + "es2021", + ], + // "downlevelIteration": true, + "rootDir": ".", + "outDir": "../out-test", + "sourceMap": true, + "removeComments": true, + "baseUrl": ".", + "paths": { + "common/*": [ + "../../../src/common/*" + ], + "browser/*": [ + "../../../src/browser/*" + ] + }, + "strict": true, + "types": [ + "../../../node_modules/@types/node" + ] + }, + "include": [ + "./**/*", + "../../../typings/xterm.d.ts" + ], + "references": [ + { + "path": "../../../src/common" + }, + { + "path": "../../../src/browser" + }, + { + "path": "../../../test/playwright" + } + ] +} diff --git a/addons/addon-web-fonts/tsconfig.json b/addons/addon-web-fonts/tsconfig.json new file mode 100644 index 000000000..2d820dd1a --- /dev/null +++ b/addons/addon-web-fonts/tsconfig.json @@ -0,0 +1,8 @@ +{ + "files": [], + "include": [], + "references": [ + { "path": "./src" }, + { "path": "./test" } + ] +} diff --git a/addons/addon-web-fonts/typings/addon-web-fonts.d.ts b/addons/addon-web-fonts/typings/addon-web-fonts.d.ts new file mode 100644 index 000000000..42cab9b63 --- /dev/null +++ b/addons/addon-web-fonts/typings/addon-web-fonts.d.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + + +import { Terminal, ITerminalAddon, IViewportRange } from '@xterm/xterm'; + +declare module '@xterm/addon-web-fonts' { + /** + * An xterm.js addon that enables web links. + */ + export class WebFontsAddon implements ITerminalAddon { + constructor(); + public activate(terminal: Terminal): void; + public dispose(): void; + } +} diff --git a/addons/addon-web-fonts/webpack.config.js b/addons/addon-web-fonts/webpack.config.js new file mode 100644 index 000000000..c73a60ba5 --- /dev/null +++ b/addons/addon-web-fonts/webpack.config.js @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +const path = require('path'); + +const addonName = 'WebFontsAddon'; +const mainFile = 'addon-web-fonts.js'; + +module.exports = { + entry: `./out/${addonName}.js`, + devtool: 'source-map', + module: { + rules: [ + { + test: /\.js$/, + use: ["source-map-loader"], + enforce: "pre", + exclude: /node_modules/ + } + ] + }, + output: { + filename: mainFile, + path: path.resolve('./lib'), + library: addonName, + libraryTarget: 'umd', + // Force usage of globalThis instead of global / self. (This is cross-env compatible) + globalObject: 'globalThis', + }, + mode: 'production' +}; diff --git a/demo/client.ts b/demo/client.ts index 49ba67438..d125bfeed 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -23,6 +23,7 @@ import { FitAddon } from '@xterm/addon-fit'; import { LigaturesAddon } from '@xterm/addon-ligatures'; import { SearchAddon, ISearchOptions } from '@xterm/addon-search'; import { SerializeAddon } from '@xterm/addon-serialize'; +import { WebFontsAddon } from '@xterm/addon-web-fonts'; import { WebLinksAddon } from '@xterm/addon-web-links'; import { WebglAddon } from '@xterm/addon-webgl'; import { Unicode11Addon } from '@xterm/addon-unicode11'; @@ -37,6 +38,7 @@ export interface IWindowWithTerminal extends Window { ImageAddon?: typeof ImageAddon; // eslint-disable-line @typescript-eslint/naming-convention SearchAddon?: typeof SearchAddon; // eslint-disable-line @typescript-eslint/naming-convention SerializeAddon?: typeof SerializeAddon; // eslint-disable-line @typescript-eslint/naming-convention + WebFontsAddon?: typeof WebFontsAddon; // eslint-disable-line @typescript-eslint/naming-convention WebLinksAddon?: typeof WebLinksAddon; // eslint-disable-line @typescript-eslint/naming-convention WebglAddon?: typeof WebglAddon; // eslint-disable-line @typescript-eslint/naming-convention Unicode11Addon?: typeof Unicode11Addon; // eslint-disable-line @typescript-eslint/naming-convention @@ -52,7 +54,7 @@ let socket; let pid; let autoResize: boolean = true; -type AddonType = 'attach' | 'clipboard' | 'fit' | 'image' | 'search' | 'serialize' | 'unicode11' | 'unicodeGraphemes' | 'webLinks' | 'webgl' | 'ligatures'; +type AddonType = 'attach' | 'clipboard' | 'fit' | 'image' | 'search' | 'serialize' | 'unicode11' | 'unicodeGraphemes' | 'webFonts' | 'webLinks' | 'webgl' | 'ligatures'; interface IDemoAddon { name: T; @@ -65,11 +67,12 @@ interface IDemoAddon { T extends 'ligatures' ? typeof LigaturesAddon : T extends 'search' ? typeof SearchAddon : T extends 'serialize' ? typeof SerializeAddon : - T extends 'webLinks' ? typeof WebLinksAddon : - T extends 'unicode11' ? typeof Unicode11Addon : - T extends 'unicodeGraphemes' ? typeof UnicodeGraphemesAddon : - T extends 'webgl' ? typeof WebglAddon : - never + T extends 'webFonts' ? typeof WebFontsAddon : + T extends 'webLinks' ? typeof WebLinksAddon : + T extends 'unicode11' ? typeof Unicode11Addon : + T extends 'unicodeGraphemes' ? typeof UnicodeGraphemesAddon : + T extends 'webgl' ? typeof WebglAddon : + never ); instance?: ( T extends 'attach' ? AttachAddon : @@ -79,11 +82,12 @@ interface IDemoAddon { T extends 'ligatures' ? LigaturesAddon : T extends 'search' ? SearchAddon : T extends 'serialize' ? SerializeAddon : - T extends 'webLinks' ? WebLinksAddon : - T extends 'unicode11' ? Unicode11Addon : - T extends 'unicodeGraphemes' ? UnicodeGraphemesAddon : - T extends 'webgl' ? WebglAddon : - never + T extends 'webFonts' ? WebFontsAddon : + T extends 'webLinks' ? WebLinksAddon : + T extends 'unicode11' ? Unicode11Addon : + T extends 'unicodeGraphemes' ? UnicodeGraphemesAddon : + T extends 'webgl' ? WebglAddon : + never ); } @@ -94,6 +98,7 @@ const addons: { [T in AddonType]: IDemoAddon } = { image: { name: 'image', ctor: ImageAddon, canChange: true }, search: { name: 'search', ctor: SearchAddon, canChange: true }, serialize: { name: 'serialize', ctor: SerializeAddon, canChange: true }, + webFonts: { name: 'webFonts', ctor: WebFontsAddon, canChange: true }, webLinks: { name: 'webLinks', ctor: WebLinksAddon, canChange: true }, webgl: { name: 'webgl', ctor: WebglAddon, canChange: true }, unicode11: { name: 'unicode11', ctor: Unicode11Addon, canChange: true }, @@ -169,6 +174,7 @@ const disposeRecreateButtonHandler: () => void = () => { addons.unicode11.instance = undefined; addons.unicodeGraphemes.instance = undefined; addons.ligatures.instance = undefined; + addons.webFonts.instance = undefined; addons.webLinks.instance = undefined; addons.webgl.instance = undefined; document.getElementById('dispose').innerHTML = 'Recreate Terminal'; @@ -218,6 +224,7 @@ if (document.location.pathname === '/test') { window.Unicode11Addon = Unicode11Addon; window.UnicodeGraphemesAddon = UnicodeGraphemesAddon; window.LigaturesAddon = LigaturesAddon; + window.WebFontsAddon = WebFontsAddon; window.WebLinksAddon = WebLinksAddon; window.WebglAddon = WebglAddon; } else { @@ -261,7 +268,7 @@ function createTerminal(): void { backend: 'conpty', buildNumber: 22621 } : undefined, - fontFamily: '"Fira Code", courier-new, courier, monospace, "Powerline Extra Symbols"', + fontFamily: '"Roboto Mono", "Fira Code", courier-new, courier, monospace, "Powerline Extra Symbols"', theme: xtermjsTheme } as ITerminalOptions); @@ -278,12 +285,14 @@ function createTerminal(): void { } catch (e) { console.warn(e); } + addons.webFonts.instance = new WebFontsAddon(); addons.webLinks.instance = new WebLinksAddon(); typedTerm.loadAddon(addons.fit.instance); typedTerm.loadAddon(addons.image.instance); typedTerm.loadAddon(addons.search.instance); typedTerm.loadAddon(addons.serialize.instance); typedTerm.loadAddon(addons.unicodeGraphemes.instance); + typedTerm.loadAddon(addons.webFonts.instance); typedTerm.loadAddon(addons.webLinks.instance); typedTerm.loadAddon(addons.clipboard.instance); diff --git a/demo/style.css b/demo/style.css index 1fb8f7ac1..3e32b42fa 100644 --- a/demo/style.css +++ b/demo/style.css @@ -1,3 +1,115 @@ +/* cyrillic-ext */ +@font-face { + font-family: 'Roboto Mono'; + font-style: italic; + font-weight: 100 700; + font-display: swap; + src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm3CWWoKC.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Roboto Mono'; + font-style: italic; + font-weight: 100 700; + font-display: swap; + src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm3mWWoKC.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek */ +@font-face { + font-family: 'Roboto Mono'; + font-style: italic; + font-weight: 100 700; + font-display: swap; + src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm36WWoKC.woff2) format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'Roboto Mono'; + font-style: italic; + font-weight: 100 700; + font-display: swap; + src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm3KWWoKC.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Roboto Mono'; + font-style: italic; + font-weight: 100 700; + font-display: swap; + src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm3OWWoKC.woff2) format('woff2'); + unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Roboto Mono'; + font-style: italic; + font-weight: 100 700; + font-display: swap; + src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm32WWg.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 100 700; + font-display: swap; + src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhGq3-OXg.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 100 700; + font-display: swap; + src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhPq3-OXg.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek */ +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 100 700; + font-display: swap; + src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhIq3-OXg.woff2) format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 100 700; + font-display: swap; + src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhEq3-OXg.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 100 700; + font-display: swap; + src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhFq3-OXg.woff2) format('woff2'); + unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 100 700; + font-display: swap; + src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhLq38.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + + + + body { font-family: helvetica, sans-serif, arial; font-size: 1em; diff --git a/demo/tsconfig.json b/demo/tsconfig.json index 5569bd1d4..db8939fa8 100644 --- a/demo/tsconfig.json +++ b/demo/tsconfig.json @@ -12,6 +12,7 @@ "@xterm/addon-image": ["../addons/addon-image"], "@xterm/addon-search": ["../addons/addon-search"], "@xterm/addon-serialize": ["../addons/addon-serialize"], + "@xterm/addon-web-fonts": ["../addons/addon-web-fonts"], "@xterm/addon-web-links": ["../addons/addon-web-links"], "@xterm/addon-webgl": ["../addons/addon-webgl"], "@xterm/addon-unicode11": ["../addons/addon-unicode11"], diff --git a/tsconfig.all.json b/tsconfig.all.json index 7ca3b7a79..9bc8896ec 100644 --- a/tsconfig.all.json +++ b/tsconfig.all.json @@ -15,6 +15,7 @@ { "path": "./addons/addon-serialize" }, { "path": "./addons/addon-unicode11" }, { "path": "./addons/addon-unicode-graphemes" }, + { "path": "./addons/addon-web-fonts" }, { "path": "./addons/addon-web-links" }, { "path": "./addons/addon-webgl" } ] From 41640d94022b21710689de95fcce65f770884b46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 3 Oct 2024 22:07:10 +0200 Subject: [PATCH 02/38] make linter happy --- addons/addon-web-fonts/src/WebFontsAddon.ts | 25 +++++++++++---------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/addons/addon-web-fonts/src/WebFontsAddon.ts b/addons/addon-web-fonts/src/WebFontsAddon.ts index 91800abdb..3cce50928 100644 --- a/addons/addon-web-fonts/src/WebFontsAddon.ts +++ b/addons/addon-web-fonts/src/WebFontsAddon.ts @@ -12,7 +12,7 @@ import type { WebFontsAddon as IWebFontsApi } from '@xterm/addon-web-fonts'; */ function unquote(s: string): string { if (s[0] === '"' && s[s.length - 1] === '"') return s.slice(1, -1); - if (s[0] === "'" && s[s.length - 1] === "'") return s.slice(1, -1); + if (s[0] === '\'' && s[s.length - 1] === '\'') return s.slice(1, -1); return s; } @@ -41,12 +41,12 @@ function createFamily(families: string[]): string { function _loadFonts(fonts?: (string | FontFace)[]): Promise { - let ffs = Array.from(document.fonts); + const ffs = Array.from(document.fonts); if (!fonts || !fonts.length) { return Promise.all(ffs.map(ff => ff.load())); } let toLoad: FontFace[] = []; - let ffsHashed = ffs.map(ff => WebFontsAddon.hashFontFace(ff)); + const ffsHashed = ffs.map(ff => WebFontsAddon.hashFontFace(ff)); for (const font of fonts) { if (font instanceof FontFace) { const fontHashed = WebFontsAddon.hashFontFace(font); @@ -85,14 +85,14 @@ export class WebFontsAddon implements ITerminalAddon, IWebFontsApi { /** * Force a terminal re-layout by altering `options.FontFamily`. - * + * * Found webfonts in `fontFamily` are temporarily removed until the webfont * resources are fully loaded. - * + * * This method is meant as a fallback fix for sloppy integrations, * that wrongly placed a webfont at the terminal contructor options. * It is likely to lead to terminal flickering in all browsers (FOUT). - * + * * To avoid triggering this fallback in your integration, make sure to have * the needed webfonts loaded at the time `terminal.open` is called. */ @@ -102,8 +102,9 @@ export class WebFontsAddon implements ITerminalAddon, IWebFontsApi { const webFamilies = WebFontsAddon.getFontFamilies(); const dirty: string[] = []; const clean: string[] = []; - for (const fam of families) + for (const fam of families) { (webFamilies.indexOf(fam) !== -1 ? dirty : clean).push(fam); + } if (dirty.length) { _loadFonts(dirty).then(() => { terminal.options.fontFamily = clean.length ? createFamily(clean) : 'monospace'; @@ -123,8 +124,8 @@ export class WebFontsAddon implements ITerminalAddon, IWebFontsApi { ff.stretch, ff.style, ff.unicodeRange, - ff.weight, - ]) + ff.weight + ]); } /** @@ -145,7 +146,7 @@ export class WebFontsAddon implements ITerminalAddon, IWebFontsApi { * matching the family names * - fontface objects : loads given fontfaces and adds them to * `document.fonts` - * + * * The returned promise will resolve, when all loading is done. */ public static loadFonts(fonts?: (string | FontFace)[]): Promise { @@ -159,7 +160,7 @@ export class WebFontsAddon implements ITerminalAddon, IWebFontsApi { - +/* eslint-disable */ // TODO: place into test cases /* (window as any).__roboto = [ @@ -296,4 +297,4 @@ export class WebFontsAddon implements ITerminalAddon, IWebFontsApi { } ), ]; -*/ \ No newline at end of file +*/ From 15a63bae228eb8f17d72e5c8db43c2b819298673 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 3 Oct 2024 22:10:40 +0200 Subject: [PATCH 03/38] change to global replacement --- addons/addon-web-fonts/src/WebFontsAddon.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/addon-web-fonts/src/WebFontsAddon.ts b/addons/addon-web-fonts/src/WebFontsAddon.ts index 3cce50928..e31326026 100644 --- a/addons/addon-web-fonts/src/WebFontsAddon.ts +++ b/addons/addon-web-fonts/src/WebFontsAddon.ts @@ -25,7 +25,7 @@ function quote(s: string): string { const pos = s.match(/([-_a-zA-Z0-9\xA0-\u{10FFFF}]+)/u); const neg = s.match(/^(-?\d|--)/m); if (!neg && pos && pos[1] === s) return s; - return `"${s.replace('"', '\\"')}"`; + return `"${s.replace(/"/g, '\\"')}"`; } From 9a536f758d5258ac690d5f96fa35f491759c132c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 3 Oct 2024 22:23:31 +0200 Subject: [PATCH 04/38] fix integration test runner --- bin/esbuild.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/esbuild.mjs b/bin/esbuild.mjs index 9ed69edab..95b3856e6 100644 --- a/bin/esbuild.mjs +++ b/bin/esbuild.mjs @@ -138,6 +138,7 @@ if (config.addon) { "@xterm/addon-image": "./addons/addon-image/lib/addon-image.mjs", "@xterm/addon-search": "./addons/addon-search/lib/addon-search.mjs", "@xterm/addon-serialize": "./addons/addon-serialize/lib/addon-serialize.mjs", + "@xterm/addon-web-fonts": "./addons/addon-web-fonts/lib/addon-web-fonts.mjs", "@xterm/addon-web-links": "./addons/addon-web-links/lib/addon-web-links.mjs", "@xterm/addon-webgl": "./addons/addon-webgl/lib/addon-webgl.mjs", "@xterm/addon-unicode11": "./addons/addon-unicode11/lib/addon-unicode11.mjs", From 809b9aa1ad0ba1083cba9935cdc0fd73fa743574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 4 Oct 2024 00:15:49 +0200 Subject: [PATCH 05/38] fix test runner --- .github/workflows/ci.yml | 3 + .../test/WebFontsAddon.test.ts | 19 ++ .../test/WebLinksAddon.test.ts | 185 ------------------ bin/test_integration.js | 1 + 4 files changed, 23 insertions(+), 185 deletions(-) create mode 100644 addons/addon-web-fonts/test/WebFontsAddon.test.ts delete mode 100644 addons/addon-web-fonts/test/WebLinksAddon.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2f39e6b8..eb9066d98 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,6 +59,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/* diff --git a/addons/addon-web-fonts/test/WebFontsAddon.test.ts b/addons/addon-web-fonts/test/WebFontsAddon.test.ts new file mode 100644 index 000000000..09bb01a98 --- /dev/null +++ b/addons/addon-web-fonts/test/WebFontsAddon.test.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2024 The xterm.js authors. All rights reserved. + * @license MIT + */ +import test from '@playwright/test'; +import { deepStrictEqual, strictEqual } from 'assert'; +import { ITestContext, createTestContext, openTerminal, pollFor, timeout } from '../../../test/playwright/TestUtils'; + + +let ctx: ITestContext; +test.beforeAll(async ({ browser }) => { + ctx = await createTestContext(browser); + await openTerminal(ctx, { cols: 40 }); +}); +test.afterAll(async () => await ctx.page.close()); + +test.describe('WebFontsAddon', () => { + test('nothing', () => {}); +}); diff --git a/addons/addon-web-fonts/test/WebLinksAddon.test.ts b/addons/addon-web-fonts/test/WebLinksAddon.test.ts deleted file mode 100644 index 8682c5335..000000000 --- a/addons/addon-web-fonts/test/WebLinksAddon.test.ts +++ /dev/null @@ -1,185 +0,0 @@ -/** - * Copyright (c) 2019 The xterm.js authors. All rights reserved. - * @license MIT - */ -import test from '@playwright/test'; -import { deepStrictEqual, strictEqual } from 'assert'; -import { readFile } from 'fs'; -import { resolve } from 'path'; -import { ITestContext, createTestContext, openTerminal, pollFor, timeout } from '../../../test/playwright/TestUtils'; - -interface ILinkStateData { - uri?: string; - range?: { - start: { - x: number; - y: number; - }; - end: { - x: number; - y: number; - }; - }; -} - - -let ctx: ITestContext; -test.beforeAll(async ({ browser }) => { - ctx = await createTestContext(browser); - await openTerminal(ctx, { cols: 40 }); -}); -test.afterAll(async () => await ctx.page.close()); - -test.describe('WebLinksAddon', () => { - - test.beforeEach(async () => { - await ctx.page.evaluate(` - window.term.reset() - window._linkaddon?.dispose(); - window._linkaddon = new WebLinksAddon(); - window.term.loadAddon(window._linkaddon); - `); - }); - - const countryTlds = [ - '.ac', '.ad', '.ae', '.af', '.ag', '.ai', '.al', '.am', '.ao', '.aq', '.ar', '.as', '.at', - '.au', '.aw', '.ax', '.az', '.ba', '.bb', '.bd', '.be', '.bf', '.bg', '.bh', '.bi', '.bj', - '.bm', '.bn', '.bo', '.bq', '.br', '.bs', '.bt', '.bw', '.by', '.bz', '.ca', '.cc', '.cd', - '.cf', '.cg', '.ch', '.ci', '.ck', '.cl', '.cm', '.cn', '.co', '.cr', '.cu', '.cv', '.cw', - '.cx', '.cy', '.cz', '.de', '.dj', '.dk', '.dm', '.do', '.dz', '.ec', '.ee', '.eg', '.eh', - '.er', '.es', '.et', '.eu', '.fi', '.fj', '.fk', '.fm', '.fo', '.fr', '.ga', '.gd', '.ge', - '.gf', '.gg', '.gh', '.gi', '.gl', '.gm', '.gn', '.gp', '.gq', '.gr', '.gs', '.gt', '.gu', - '.gw', '.gy', '.hk', '.hm', '.hn', '.hr', '.ht', '.hu', '.id', '.ie', '.il', '.im', '.in', - '.io', '.iq', '.ir', '.is', '.it', '.je', '.jm', '.jo', '.jp', '.ke', '.kg', '.kh', '.ki', - '.km', '.kn', '.kp', '.kr', '.kw', '.ky', '.kz', '.la', '.lb', '.lc', '.li', '.lk', '.lr', - '.ls', '.lt', '.lu', '.lv', '.ly', '.ma', '.mc', '.md', '.me', '.mg', '.mh', '.mk', '.ml', - '.mm', '.mn', '.mo', '.mp', '.mq', '.mr', '.ms', '.mt', '.mu', '.mv', '.mw', '.mx', '.my', - '.mz', '.na', '.nc', '.ne', '.nf', '.ng', '.ni', '.nl', '.no', '.np', '.nr', '.nu', '.nz', - '.om', '.pa', '.pe', '.pf', '.pg', '.ph', '.pk', '.pl', '.pm', '.pn', '.pr', '.ps', '.pt', - '.pw', '.py', '.qa', '.re', '.ro', '.rs', '.ru', '.rw', '.sa', '.sb', '.sc', '.sd', '.se', - '.sg', '.sh', '.si', '.sk', '.sl', '.sm', '.sn', '.so', '.sr', '.ss', '.st', '.su', '.sv', - '.sx', '.sy', '.sz', '.tc', '.td', '.tf', '.tg', '.th', '.tj', '.tk', '.tl', '.tm', '.tn', - '.to', '.tr', '.tt', '.tv', '.tw', '.tz', '.ua', '.ug', '.uk', '.us', '.uy', '.uz', '.va', - '.vc', '.ve', '.vg', '.vi', '.vn', '.vu', '.wf', '.ws', '.ye', '.yt', '.za', '.zm', '.zw' - ]; - for (const tld of countryTlds) { - test(tld, async () => await testHostName(`foo${tld}`)); - } - test(`.com`, async () => await testHostName(`foo.com`)); - for (const tld of countryTlds) { - test(`.com${tld}`, async () => await testHostName(`foo.com${tld}`)); - } - - test.describe('correct buffer offsets & uri', () => { - test.beforeEach(async () => { - await ctx.page.evaluate(` - window._linkStateData = {uri:''}; - window._linkaddon._options.hover = (event, uri, range) => { window._linkStateData = { uri, range }; }; - `); - }); - test('all half width', async () => { - await ctx.proxy.write('aaa http://example.com aaa http://example.com aaa'); - await resetAndHover(5, 0); - await evalLinkStateData('http://example.com', { start: { x: 5, y: 1 }, end: { x: 22, y: 1 } }); - await resetAndHover(1, 1); - await evalLinkStateData('http://example.com', { start: { x: 28, y: 1 }, end: { x: 5, y: 2 } }); - }); - test('url after full width', async () => { - await ctx.proxy.write('¥¥¥ http://example.com ¥¥¥ http://example.com aaa'); - await resetAndHover(8, 0); - await evalLinkStateData('http://example.com', { start: { x: 8, y: 1 }, end: { x: 25, y: 1 } }); - await resetAndHover(1, 1); - await evalLinkStateData('http://example.com', { start: { x: 34, y: 1 }, end: { x: 11, y: 2 } }); - }); - test('full width within url and before', async () => { - await ctx.proxy.write('¥¥¥ https://ko.wikipedia.org/wiki/위키백과:대문 aaa https://ko.wikipedia.org/wiki/위키백과:대문 ¥¥¥'); - await resetAndHover(8, 0); - await evalLinkStateData('https://ko.wikipedia.org/wiki/위키백과:대문', { start: { x: 8, y: 1 }, end: { x: 11, y: 2 } }); - await resetAndHover(1, 1); - await evalLinkStateData('https://ko.wikipedia.org/wiki/위키백과:대문', { start: { x: 8, y: 1 }, end: { x: 11, y: 2 } }); - await resetAndHover(17, 1); - await evalLinkStateData('https://ko.wikipedia.org/wiki/위키백과:대문', { start: { x: 17, y: 2 }, end: { x: 19, y: 3 } }); - }); - test('name + password url after full width and combining', async () => { - await ctx.proxy.write('¥¥¥cafe\u0301 http://test:password@example.com/some_path'); - await resetAndHover(12, 0); - await evalLinkStateData('http://test:password@example.com/some_path', { start: { x: 12, y: 1 }, end: { x: 13, y: 2 } }); - await resetAndHover(5, 1); - await evalLinkStateData('http://test:password@example.com/some_path', { start: { x: 12, y: 1 }, end: { x: 13, y: 2 } }); - }); - test('url encoded params work properly', async () => { - await ctx.proxy.write('¥¥¥cafe\u0301 http://test:password@example.com/some_path?param=1%202%3'); - await resetAndHover(12, 0); - await evalLinkStateData('http://test:password@example.com/some_path?param=1%202%3', { start: { x: 12, y: 1 }, end: { x: 27, y: 2 } }); - await resetAndHover(5, 1); - await evalLinkStateData('http://test:password@example.com/some_path?param=1%202%3', { start: { x: 12, y: 1 }, end: { x: 27, y: 2 } }); - }); - }); - - // issue #4964 - test('uppercase in protocol and host, default ports', async () => { - await ctx.proxy.write( - ` HTTP://EXAMPLE.COM \r\n` + - ` HTTPS://Example.com \r\n` + - ` HTTP://Example.com:80 \r\n` + - ` HTTP://Example.com:80/staysUpper \r\n` + - ` HTTP://Ab:xY@abc.com:80/staysUpper \r\n` - ); - await pollForLinkAtCell(3, 0, `HTTP://EXAMPLE.COM`); - await pollForLinkAtCell(3, 1, `HTTPS://Example.com`); - await pollForLinkAtCell(3, 2, `HTTP://Example.com:80`); - await pollForLinkAtCell(3, 3, `HTTP://Example.com:80/staysUpper`); - await pollForLinkAtCell(3, 4, `HTTP://Ab:xY@abc.com:80/staysUpper`); - }); -}); - -async function testHostName(hostname: string): Promise { - await ctx.proxy.write( - ` http://${hostname} \r\n` + - ` http://${hostname}/a~b#c~d?e~f \r\n` + - ` http://${hostname}/colon:test \r\n` + - ` http://${hostname}/colon:test: \r\n` + - `"http://${hostname}/"\r\n` + - `\'http://${hostname}/\'\r\n` + - `http://${hostname}/subpath/+/id` - ); - await pollForLinkAtCell(3, 0, `http://${hostname}`); - await pollForLinkAtCell(3, 1, `http://${hostname}/a~b#c~d?e~f`); - await pollForLinkAtCell(3, 2, `http://${hostname}/colon:test`); - await pollForLinkAtCell(3, 3, `http://${hostname}/colon:test`); - await pollForLinkAtCell(2, 4, `http://${hostname}/`); - await pollForLinkAtCell(2, 5, `http://${hostname}/`); - await pollForLinkAtCell(1, 6, `http://${hostname}/subpath/+/id`); -} - -async function pollForLinkAtCell(col: number, row: number, value: string): Promise { - await ctx.page.mouse.move(...(await cellPos(col, row))); - await pollFor(ctx.page, `!!Array.from(document.querySelectorAll('.xterm-rows > :nth-child(${row+1}) > span[style]')).filter(el => el.style.textDecoration == 'underline').length`, true); - const text = await ctx.page.evaluate(`Array.from(document.querySelectorAll('.xterm-rows > :nth-child(${row+1}) > span[style]')).filter(el => el.style.textDecoration == 'underline').map(el => el.textContent).join('');`); - deepStrictEqual(text, value); -} - -async function resetAndHover(col: number, row: number): Promise { - await ctx.page.mouse.move(0, 0); - await ctx.page.evaluate(`window._linkStateData = {uri:''};`); - await new Promise(r => setTimeout(r, 200)); - await ctx.page.mouse.move(...(await cellPos(col, row))); - await pollFor(ctx.page, `!!window._linkStateData.uri.length`, true); -} - -async function evalLinkStateData(uri: string, range: any): Promise { - const data: ILinkStateData = await ctx.page.evaluate(`window._linkStateData`); - strictEqual(data.uri, uri); - deepStrictEqual(data.range, range); -} - -async function cellPos(col: number, row: number): Promise<[number, number]> { - const coords: any = await ctx.page.evaluate(` - (function() { - const rect = window.term.element.getBoundingClientRect(); - const dim = term._core._renderService.dimensions; - return {left: rect.left, top: rect.top, bottom: rect.bottom, right: rect.right, width: dim.css.cell.width, height: dim.css.cell.height}; - })(); - `); - return [col * coords.width + coords.left + 2, row * coords.height + coords.top + 2]; -} diff --git a/bin/test_integration.js b/bin/test_integration.js index 936467cc1..b7773e575 100644 --- a/bin/test_integration.js +++ b/bin/test_integration.js @@ -29,6 +29,7 @@ const addons = [ 'serialize', 'unicode-graphemes', 'unicode11', + 'web-fonts', 'web-links', 'webgl', ]; From b104bcd144b2816fa1bc7b464a502c4713a119f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 4 Oct 2024 00:42:38 +0200 Subject: [PATCH 06/38] remove await remnants --- addons/addon-web-fonts/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/addons/addon-web-fonts/README.md b/addons/addon-web-fonts/README.md index bf506cf1c..bd1dedf4b 100644 --- a/addons/addon-web-fonts/README.md +++ b/addons/addon-web-fonts/README.md @@ -72,7 +72,7 @@ const webFontsAddon = new WebFontsAddon(); terminal.loadAddon(webFontsAddon); // wait for webfonts to be fully loaded -await WebFontsAddon.loadFonts(['Web Mono 1', 'Super Powerline']).then(() => { +WebFontsAddon.loadFonts(['Web Mono 1', 'Super Powerline']).then(() => { terminal.open(your_terminal_div_element); // more boostrapping goes here ... }); @@ -112,7 +112,7 @@ That can be achieved like this: const ff1 = new FontFace('New Web Mono', url1, ...); const ff2 = new FontFace('New Web Mono', url2, ...); // and await their loading -await WebFontsAddon.loadFonts([ff1, ff2]).then(() => { +WebFontsAddon.loadFonts([ff1, ff2]).then(() => { // apply new webfont to terminal terminal.options.fontFamily = 'New Web Mono'; // since the new font might have slighly different metrics, @@ -124,7 +124,7 @@ await WebFontsAddon.loadFonts([ff1, ff2]).then(() => { document.styleSheets[0].insertRule( "@font-face { font-family: 'New Web Mono'; src: url(newfont.woff); }", 0); // and await the new font family name -await WebFontsAddon.loadFonts(['New Web Mono']).then(() => { +WebFontsAddon.loadFonts(['New Web Mono']).then(() => { // apply new webfont to terminal terminal.options.fontFamily = 'New Web Mono'; // since the new font might have slighly different metrics, From 23bef9913290707e65323ad1ee620ebe0c310e5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 4 Oct 2024 02:06:02 +0200 Subject: [PATCH 07/38] cleanup API --- .github/workflows/ci.yml | 2 + addons/addon-web-fonts/README.md | 12 +- addons/addon-web-fonts/src/WebFontsAddon.ts | 136 +++++++++--------- .../typings/addon-web-fonts.d.ts | 55 ++++++- 4 files changed, 127 insertions(+), 78 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb9066d98..f7616b7c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -223,6 +223,8 @@ jobs: run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-unicode-graphemes - name: Integration tests (addon-unicode11) run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-unicode11 + - name: Integration tests (addon-web-fonts) + run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-web-fonts - name: Integration tests (addon-web-links) run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-web-links - name: Integration tests (addon-webgl) diff --git a/addons/addon-web-fonts/README.md b/addons/addon-web-fonts/README.md index bd1dedf4b..e910c21e5 100644 --- a/addons/addon-web-fonts/README.md +++ b/addons/addon-web-fonts/README.md @@ -72,7 +72,7 @@ const webFontsAddon = new WebFontsAddon(); terminal.loadAddon(webFontsAddon); // wait for webfonts to be fully loaded -WebFontsAddon.loadFonts(['Web Mono 1', 'Super Powerline']).then(() => { +webFontsAddon.loadFonts(['Web Mono 1', 'Super Powerline']).then(() => { terminal.open(your_terminal_div_element); // more boostrapping goes here ... }); @@ -83,13 +83,13 @@ on the initial document load (more precise - by the time this code runs). Please note, that this code cannot run synchronous anymore, so you will have to split your bootstrapping code into several stages. If thats too much of a hassle, you can also move the whole -bootstrapping under that waiting condition (`loadFonts` is actually a static method): +bootstrapping under that waiting condition (import `loadFonts` for a static variant): ```typescript import { Terminal } from '@xterm/xterm'; import { XYAddon } from '@xterm/addon-xy'; -import { WebFontsAddon } from '@xterm/addon-web-fonts'; +import { WebFontsAddon, loadFonts } from '@xterm/addon-web-fonts'; -WebFontsAddon.loadFonts(['Web Mono 1', 'Super Powerline']).then(() => { +loadFonts(['Web Mono 1', 'Super Powerline']).then(() => { // create a `Terminal` instance, now with webfonts const terminal = new Terminal({fontFamily: '"Web Mono 1", "Super Powerline", monospace'}); const xyAddon = new XYAddon(); @@ -112,7 +112,7 @@ That can be achieved like this: const ff1 = new FontFace('New Web Mono', url1, ...); const ff2 = new FontFace('New Web Mono', url2, ...); // and await their loading -WebFontsAddon.loadFonts([ff1, ff2]).then(() => { +loadFonts([ff1, ff2]).then(() => { // apply new webfont to terminal terminal.options.fontFamily = 'New Web Mono'; // since the new font might have slighly different metrics, @@ -124,7 +124,7 @@ WebFontsAddon.loadFonts([ff1, ff2]).then(() => { document.styleSheets[0].insertRule( "@font-face { font-family: 'New Web Mono'; src: url(newfont.woff); }", 0); // and await the new font family name -WebFontsAddon.loadFonts(['New Web Mono']).then(() => { +loadFonts(['New Web Mono']).then(() => { // apply new webfont to terminal terminal.options.fontFamily = 'New Web Mono'; // since the new font might have slighly different metrics, diff --git a/addons/addon-web-fonts/src/WebFontsAddon.ts b/addons/addon-web-fonts/src/WebFontsAddon.ts index e31326026..219a64afe 100644 --- a/addons/addon-web-fonts/src/WebFontsAddon.ts +++ b/addons/addon-web-fonts/src/WebFontsAddon.ts @@ -8,7 +8,7 @@ import type { WebFontsAddon as IWebFontsApi } from '@xterm/addon-web-fonts'; /** - * Unquote family name. + * Unquote a font family name. */ function unquote(s: string): string { if (s[0] === '"' && s[s.length - 1] === '"') return s.slice(1, -1); @@ -18,7 +18,7 @@ function unquote(s: string): string { /** - * Quote family name. + * Quote a font family name conditionally. * @see https://mathiasbynens.be/notes/unquoted-font-family */ function quote(s: string): string { @@ -40,16 +40,46 @@ function createFamily(families: string[]): string { } +/** + * Hash a font face from it properties. + * Used in `loadFonts` to avoid bloating + * `document.fonts` from multiple calls. + */ +function hashFontFace(ff: FontFace): string { + return JSON.stringify([ + unquote(ff.family), + ff.stretch, + ff.style, + ff.unicodeRange, + ff.weight + ]); +} + + +/** + * Wait for webfont resources to be loaded. + * + * Without any argument, all fonts currently listed in + * `document.fonts` will be loaded. + * For a more fine-grained loading strategy you can populate + * the `fonts` argument with: + * - font families : loads all fontfaces in `document.fonts` + * matching the family names + * - fontface objects : loads given fontfaces and adds them to + * `document.fonts` + * + * The returned promise will resolve, when all loading is done. + */ function _loadFonts(fonts?: (string | FontFace)[]): Promise { const ffs = Array.from(document.fonts); if (!fonts || !fonts.length) { return Promise.all(ffs.map(ff => ff.load())); } let toLoad: FontFace[] = []; - const ffsHashed = ffs.map(ff => WebFontsAddon.hashFontFace(ff)); + const ffsHashed = ffs.map(ff => hashFontFace(ff)); for (const font of fonts) { if (font instanceof FontFace) { - const fontHashed = WebFontsAddon.hashFontFace(font); + const fontHashed = hashFontFace(font); const idx = ffsHashed.indexOf(fontHashed); if (idx === -1) { document.fonts.add(font); @@ -72,85 +102,53 @@ function _loadFonts(fonts?: (string | FontFace)[]): Promise { } +export async function loadFonts(fonts?: (string | FontFace)[]): Promise { + await document.fonts.ready; + return _loadFonts(fonts); +} + export class WebFontsAddon implements ITerminalAddon, IWebFontsApi { - constructor(public forceInitialRelayout: boolean = true) { } - public dispose(): void { } + private _term: Terminal | undefined; - public activate(terminal: Terminal): void { + constructor(public forceInitialRelayout: boolean = true) { } + + public dispose(): void { + this._term = undefined; + } + + public activate(term: Terminal): void { + this._term = term; if (this.forceInitialRelayout) { - document.fonts.ready.then(() => this.relayout(terminal)); + document.fonts.ready.then(() => this.relayout()); } } - /** - * Force a terminal re-layout by altering `options.FontFamily`. - * - * Found webfonts in `fontFamily` are temporarily removed until the webfont - * resources are fully loaded. - * - * This method is meant as a fallback fix for sloppy integrations, - * that wrongly placed a webfont at the terminal contructor options. - * It is likely to lead to terminal flickering in all browsers (FOUT). - * - * To avoid triggering this fallback in your integration, make sure to have - * the needed webfonts loaded at the time `terminal.open` is called. - */ - public relayout(terminal: Terminal): void { - const family = terminal.options.fontFamily; + public async loadFonts(fonts?: (string | FontFace)[]): Promise { + return loadFonts(fonts); + } + + public async relayout(): Promise { + if (!this._term) { + return; + } + await document.fonts.ready; + const family = this._term.options.fontFamily; const families = splitFamily(family); - const webFamilies = WebFontsAddon.getFontFamilies(); + const webFamilies = Array.from(new Set(Array.from(document.fonts).map(e => unquote(e.family)))); const dirty: string[] = []; const clean: string[] = []; for (const fam of families) { (webFamilies.indexOf(fam) !== -1 ? dirty : clean).push(fam); } - if (dirty.length) { - _loadFonts(dirty).then(() => { - terminal.options.fontFamily = clean.length ? createFamily(clean) : 'monospace'; - terminal.options.fontFamily = family; - }); + if (!dirty.length) { + return; + } + await _loadFonts(dirty); + if (this._term) { + this._term.options.fontFamily = clean.length ? createFamily(clean) : 'monospace'; + this._term.options.fontFamily = family; } - } - - /** - * Hash a font face from it properties. - * Used in `loadFonts` to avoid bloating - * `document.fonts` from multiple calls. - */ - public static hashFontFace(ff: FontFace): string { - return JSON.stringify([ - unquote(ff.family), - ff.stretch, - ff.style, - ff.unicodeRange, - ff.weight - ]); - } - - /** - * Return font families known in `document.fonts`. - */ - public static getFontFamilies(): string[] { - return Array.from(new Set(Array.from(document.fonts).map(e => unquote(e.family)))); - } - - /** - * Wait for webfont resources to be loaded. - * - * Without any argument, all fonts currently listed in - * `document.fonts` will be loaded. - * For a more fine-grained loading strategy you can populate - * the `fonts` argument with: - * - font families : loads all fontfaces in `document.fonts` - * matching the family names - * - fontface objects : loads given fontfaces and adds them to - * `document.fonts` - * - * The returned promise will resolve, when all loading is done. - */ - public static loadFonts(fonts?: (string | FontFace)[]): Promise { - return document.fonts.ready.then(() => _loadFonts(fonts)); } } diff --git a/addons/addon-web-fonts/typings/addon-web-fonts.d.ts b/addons/addon-web-fonts/typings/addon-web-fonts.d.ts index 42cab9b63..c353a421e 100644 --- a/addons/addon-web-fonts/typings/addon-web-fonts.d.ts +++ b/addons/addon-web-fonts/typings/addon-web-fonts.d.ts @@ -4,15 +4,64 @@ */ -import { Terminal, ITerminalAddon, IViewportRange } from '@xterm/xterm'; +import { Terminal, ITerminalAddon } from '@xterm/xterm'; declare module '@xterm/addon-web-fonts' { + /** - * An xterm.js addon that enables web links. + * Addon to use webfonts in xterm.js */ export class WebFontsAddon implements ITerminalAddon { - constructor(); + /** + * @param forceInitialRelayout Force an initial relayout, if a webfont was found. + */ + constructor(forceInitialRelayout?: boolean); public activate(terminal: Terminal): void; public dispose(): void; + + /** + * Wait for webfont resources to be loaded. + * + * Without any argument, all fonts currently listed in + * `document.fonts` will be loaded. + * For a more fine-grained loading strategy you can populate + * the `fonts` argument with: + * - font families : loads all fontfaces in `document.fonts` + * matching the family names + * - fontface objects : loads given fontfaces and adds them to + * `document.fonts` + * + * The returned promise will resolve, when all loading is done. + */ + public loadFonts(fonts?: (string | FontFace)[]): Promise; + + /** + * Force a terminal relayout by altering `options.FontFamily`. + * + * Found webfonts in `fontFamily` are temporarily removed until the webfont + * resources are fully loaded. + * + * Call this method, if a terminal with webfonts is stuck with broken + * glyph metrics. + * + * Returns a promise on completion. + */ + public relayout(): Promise; } + + /** + * Wait for webfont resources to be loaded. + * + * Without any argument, all fonts currently listed in + * `document.fonts` will be loaded. + * For a more fine-grained loading strategy you can populate + * the `fonts` argument with: + * - font families : loads all fontfaces in `document.fonts` + * matching the family names + * - fontface objects : loads given fontfaces and adds them to + * `document.fonts` + * + * The returned promise will resolve, when all loading is done. + */ + function loadFonts(fonts?: (string | FontFace)[]): Promise; } From 956c7cbcba5dd5e0300c84203e074f32c0c66abc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 4 Oct 2024 04:00:20 +0200 Subject: [PATCH 08/38] demo tests --- .../typings/addon-web-fonts.d.ts | 4 +- addons/addon-web-fonts/webpack.config.js | 2 +- demo/bpdots.regular.otf | Bin 0 -> 48296 bytes demo/client.ts | 23 +++- demo/index.html | 4 + demo/kongtext.regular.ttf | Bin 0 -> 10280 bytes demo/server.js | 3 + demo/style.css | 112 ------------------ 8 files changed, 31 insertions(+), 117 deletions(-) create mode 100644 demo/bpdots.regular.otf create mode 100644 demo/kongtext.regular.ttf diff --git a/addons/addon-web-fonts/typings/addon-web-fonts.d.ts b/addons/addon-web-fonts/typings/addon-web-fonts.d.ts index c353a421e..9be3de216 100644 --- a/addons/addon-web-fonts/typings/addon-web-fonts.d.ts +++ b/addons/addon-web-fonts/typings/addon-web-fonts.d.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * Copyright (c) 2024 The xterm.js authors. All rights reserved. * @license MIT */ @@ -13,7 +13,7 @@ declare module '@xterm/addon-web-fonts' { */ export class WebFontsAddon implements ITerminalAddon { /** - * @param forceInitialRelayout Force an initial relayout, if a webfont was found. + * @param forceInitialRelayout Force initial relayout, if a webfont was found (default true). */ constructor(forceInitialRelayout?: boolean); public activate(terminal: Terminal): void; diff --git a/addons/addon-web-fonts/webpack.config.js b/addons/addon-web-fonts/webpack.config.js index c73a60ba5..f75d2d501 100644 --- a/addons/addon-web-fonts/webpack.config.js +++ b/addons/addon-web-fonts/webpack.config.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * Copyright (c) 2024 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/demo/bpdots.regular.otf b/demo/bpdots.regular.otf new file mode 100644 index 0000000000000000000000000000000000000000..e5b5d1dcb74e06e7719aefbe81fd356e6257c1a7 GIT binary patch literal 48296 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 ro_Ih?aL-oOgzLY=e term.focus()); document.getElementById('event-blur').addEventListener('click', ()=> term.blur()); } + +function testWebfonts() { + document.getElementById('webfont-kongtime').addEventListener('click', async () => { + const ff = new FontFace('Kongtext', "url(/kongtext.regular.ttf) format('truetype')"); + await loadFonts([ff]); + term.options.fontFamily = 'Kongtext'; + term.options.lineHeight = 1.3; + addons.fit.instance?.fit(); + }); + document.getElementById('webfont-bpdots').addEventListener('click', async () => { + document.styleSheets[0].insertRule("@font-face { font-family: 'BPdots'; src: url(/bpdots.regular.otf) format('opentype'); weight: 400 }", 0); + await loadFonts(['BPdots']); + term.options.fontFamily = 'BPdots'; + term.options.lineHeight = 1.3; + term.options.fontSize = 20; + addons.fit.instance?.fit(); + }); +} diff --git a/demo/index.html b/demo/index.html index 064518475..a56734819 100644 --- a/demo/index.html +++ b/demo/index.html @@ -116,6 +116,10 @@
Events Test
+ +
Webfonts
+
+
diff --git a/demo/kongtext.regular.ttf b/demo/kongtext.regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..5e4d65fd1e059a159342d62f86c374da152df7f5 GIT binary patch literal 10280 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 res.sendFile(__dirname + '/kongtext.regular.ttf')); + app.get('/bpdots.regular.otf', (req, res) => res.sendFile(__dirname + '/bpdots.regular.otf')); + app.use('/dist', express.static(__dirname + '/dist')); app.use('/src', express.static(__dirname + '/src')); diff --git a/demo/style.css b/demo/style.css index 3e32b42fa..1fb8f7ac1 100644 --- a/demo/style.css +++ b/demo/style.css @@ -1,115 +1,3 @@ -/* cyrillic-ext */ -@font-face { - font-family: 'Roboto Mono'; - font-style: italic; - font-weight: 100 700; - font-display: swap; - src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm3CWWoKC.woff2) format('woff2'); - unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; -} -/* cyrillic */ -@font-face { - font-family: 'Roboto Mono'; - font-style: italic; - font-weight: 100 700; - font-display: swap; - src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm3mWWoKC.woff2) format('woff2'); - unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; -} -/* greek */ -@font-face { - font-family: 'Roboto Mono'; - font-style: italic; - font-weight: 100 700; - font-display: swap; - src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm36WWoKC.woff2) format('woff2'); - unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; -} -/* vietnamese */ -@font-face { - font-family: 'Roboto Mono'; - font-style: italic; - font-weight: 100 700; - font-display: swap; - src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm3KWWoKC.woff2) format('woff2'); - unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; -} -/* latin-ext */ -@font-face { - font-family: 'Roboto Mono'; - font-style: italic; - font-weight: 100 700; - font-display: swap; - src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm3OWWoKC.woff2) format('woff2'); - unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; -} -/* latin */ -@font-face { - font-family: 'Roboto Mono'; - font-style: italic; - font-weight: 100 700; - font-display: swap; - src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm32WWg.woff2) format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} -/* cyrillic-ext */ -@font-face { - font-family: 'Roboto Mono'; - font-style: normal; - font-weight: 100 700; - font-display: swap; - src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhGq3-OXg.woff2) format('woff2'); - unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; -} -/* cyrillic */ -@font-face { - font-family: 'Roboto Mono'; - font-style: normal; - font-weight: 100 700; - font-display: swap; - src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhPq3-OXg.woff2) format('woff2'); - unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; -} -/* greek */ -@font-face { - font-family: 'Roboto Mono'; - font-style: normal; - font-weight: 100 700; - font-display: swap; - src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhIq3-OXg.woff2) format('woff2'); - unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; -} -/* vietnamese */ -@font-face { - font-family: 'Roboto Mono'; - font-style: normal; - font-weight: 100 700; - font-display: swap; - src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhEq3-OXg.woff2) format('woff2'); - unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; -} -/* latin-ext */ -@font-face { - font-family: 'Roboto Mono'; - font-style: normal; - font-weight: 100 700; - font-display: swap; - src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhFq3-OXg.woff2) format('woff2'); - unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; -} -/* latin */ -@font-face { - font-family: 'Roboto Mono'; - font-style: normal; - font-weight: 100 700; - font-display: swap; - src: url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhLq38.woff2) format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} - - - - body { font-family: helvetica, sans-serif, arial; font-size: 1em; From 7907f13774ef6c0023f3277d5d72577d8e126717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 4 Oct 2024 04:14:10 +0200 Subject: [PATCH 09/38] add license notes for fonts --- demo/font-licenses.txt | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 demo/font-licenses.txt diff --git a/demo/font-licenses.txt b/demo/font-licenses.txt new file mode 100644 index 000000000..5b1b5ba66 --- /dev/null +++ b/demo/font-licenses.txt @@ -0,0 +1,41 @@ + +BPdots is licensed under the Creative Commons Attribution-NoDerivs License (CC BY-ND). + + +kongtext license text: + +Thanks for downloading one of codeman38's retro video game fonts, +as seen on Memepool, BoingBoing, and all around the blogosphere. + +So, you're wondering what the license is for these fonts? Pretty simple; +it's based upon that used for Bitstream's Vera font set . + +Basically, here are the key points summarized, in as little legalese as possible; +I hate reading license agreements as much as you probably do: + +With one specific exception, you have full permission to bundle these fonts in +your own free or commercial projects-- and by projects, I'm referring to not +just software but also electronic documents and print publications. + +So what's the exception? Simple: you can't re-sell these fonts +in a commercial font collection. I've seen too many font CDs for sale in stores +that are just a repackaging of thousands of freeware fonts found on the internet, +and in my mind, that's quite a bit like highway robbery. Note that this *only* +applies to products that are font collections in and of themselves; +you may freely bundle these fonts with an operating system, application program, +or the like. + +Feel free to modify these fonts and even to release the modified versions, +as long as you change the original font names (to ensure consistency among +people with the font installed) and as long as you give credit somewhere +in the font file to codeman38 or zone38.net. I may even incorporate these changes +into a later version of my fonts if you wish to send me the modifed fonts via e-mail. + +Also, feel free to mirror these fonts on your own site, as long as you make it +reasonably clear that these fonts are not your own work. I'm not asking for much; +linking to zone38.net or even just mentioning the nickname codeman38 should be enough. + +Well, that pretty much sums it up... so without further ado, +install and enjoy these fonts from the golden age of video games. + +[ codeman38 | cody@zone38.net | http://www.zone38.net/ ] From b0389a2ed173bf3b9df32a1b6f5f8c461b14d887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 4 Oct 2024 05:31:47 +0200 Subject: [PATCH 10/38] future proof demo --- demo/client.ts | 4 +++- demo/index.html | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index d5e9fc08c..732efda5b 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -1430,12 +1430,14 @@ function testEvents(): void { } function testWebfonts() { - document.getElementById('webfont-kongtime').addEventListener('click', async () => { + document.getElementById('webfont-kongtext').addEventListener('click', async () => { const ff = new FontFace('Kongtext', "url(/kongtext.regular.ttf) format('truetype')"); await loadFonts([ff]); term.options.fontFamily = 'Kongtext'; term.options.lineHeight = 1.3; addons.fit.instance?.fit(); + setTimeout(() => term.write('\x1b[?12h\x1b]12;#776CF9\x07\x1b[38;2;119;108;249;48;2;21;8;150m\x1b[2J\x1b[2;5H**** COMMODORE 64 BASIC V2 ****\r\n\r\n 64K RAM SYSTEM 38911 BASIC BYTES FREE\r\n\r\nREADY.\r\nLOAD '), 1000); + setTimeout(() => {term.write('🤣\x1b[m\x1b[99;1H'); term.input('\r');}, 5000); }); document.getElementById('webfont-bpdots').addEventListener('click', async () => { document.styleSheets[0].insertRule("@font-face { font-family: 'BPdots'; src: url(/bpdots.regular.otf) format('opentype'); weight: 400 }", 0); diff --git a/demo/index.html b/demo/index.html index a56734819..691871ee0 100644 --- a/demo/index.html +++ b/demo/index.html @@ -118,8 +118,8 @@
Webfonts
-
-
+
+
From c93623cb3bcd5d53fbc5c51333b2e7cac0b0e927 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 4 Oct 2024 14:24:36 +0200 Subject: [PATCH 11/38] CI tests --- .../test/WebFontsAddon.test.ts | 105 +++++++++++++++++- 1 file changed, 103 insertions(+), 2 deletions(-) diff --git a/addons/addon-web-fonts/test/WebFontsAddon.test.ts b/addons/addon-web-fonts/test/WebFontsAddon.test.ts index 09bb01a98..1315b1ee6 100644 --- a/addons/addon-web-fonts/test/WebFontsAddon.test.ts +++ b/addons/addon-web-fonts/test/WebFontsAddon.test.ts @@ -6,7 +6,6 @@ import test from '@playwright/test'; import { deepStrictEqual, strictEqual } from 'assert'; import { ITestContext, createTestContext, openTerminal, pollFor, timeout } from '../../../test/playwright/TestUtils'; - let ctx: ITestContext; test.beforeAll(async ({ browser }) => { ctx = await createTestContext(browser); @@ -15,5 +14,107 @@ test.beforeAll(async ({ browser }) => { test.afterAll(async () => await ctx.page.close()); test.describe('WebFontsAddon', () => { - test('nothing', () => {}); + + test.beforeEach(async () => { + // make sure that we start with no webfonts in the document + const empty = await await getDocumentFonts(); + deepStrictEqual(empty, []); + }); + test.afterEach(async () => { + // for font loading tests to work, we have to remove added rules and fonts + // to work around the quite aggressive font caching done by the browsers + await ctx.page.evaluate(` + document.styleSheets[0].deleteRule(1); + document.styleSheets[0].deleteRule(0); + document.fonts.clear(); + `); + }); + + test.describe('font loading at runtime', () => { + test('loadFonts (JS)', async () => { + await ctx.page.evaluate(` + const ff1 = new FontFace('Kongtext', "url(/kongtext.regular.ttf) format('truetype')"); + const ff2 = new FontFace('BPdots', "url(/bpdots.regular.otf) format('opentype')"); + loadFonts([ff1, ff2]); + `); + deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'loaded' }, { family: 'BPdots', status: 'loaded' }]); + }); + test('loadFonts (CSS, unquoted)', async () => { + await ctx.page.evaluate(` + document.styleSheets[0].insertRule("@font-face {font-family: Kongtext; src: url(/kongtext.regular.ttf) format('truetype')}", 0); + document.styleSheets[0].insertRule("@font-face {font-family: BPdots; src: url(/bpdots.regular.otf) format('opentype')}", 1); + loadFonts(['Kongtext', 'BPdots']); + `); + deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'loaded' }, { family: 'BPdots', status: 'loaded' }]); + }); + test('loadFonts (CSS, quoted)', async ({ browser }) => { + // NOTE: firefox preserves family quotes from CSS rules in fontface, all other browsers unquote them + await ctx.page.evaluate(` + document.styleSheets[0].insertRule("@font-face {font-family: 'Kongtext'; src: url(/kongtext.regular.ttf) format('truetype')}", 0); + document.styleSheets[0].insertRule("@font-face {font-family: 'BPdots'; src: url(/bpdots.regular.otf) format('opentype')}", 1); + loadFonts(['Kongtext', 'BPdots']); + `); + if (browser.browserType().name() === 'firefox') { + deepStrictEqual(await getDocumentFonts(), [{ family: '"Kongtext"', status: 'loaded' }, { family: '"BPdots"', status: 'loaded' }]); + } else { + deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'loaded' }, { family: 'BPdots', status: 'loaded' }]); + } + }); + test('FontFace hashing', async () => { + // multiple calls of `loadFonts` with the same objects shall not bloat document.fonts + await ctx.page.evaluate(` + const ff1 = new FontFace('Kongtext', "url(/kongtext.regular.ttf) format('truetype')"); + const ff2 = new FontFace('BPdots', "url(/bpdots.regular.otf) format('opentype')"); + loadFonts([ff1, ff2]); + loadFonts([ff1, ff2]); + loadFonts([ff1, ff2]).then(() => loadFonts([ff1, ff2])); + `); + deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'loaded' }, { family: 'BPdots', status: 'loaded' }]); + }); + + test('autoload & relayout from ctor', async ({ browser }) => { + // to make this test work, we exclude the default measurement char W (x57) by restricting unicode-range + // now the browser will postpone font loading until codepoint is hit --> wrong glyph metrics on first usage + const data = await ctx.page.evaluate(` + document.styleSheets[0].insertRule("@font-face {font-family: Kongtext; src: url(/kongtext.regular.ttf) format('truetype'); unicode-range: U+00A0-00FF}", 0); + `); + deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'unloaded' }]); + + // broken case: webfont in ctor without addon usage + await ctx.page.evaluate(` + window.helperTerm = new Terminal({fontFamily: '"Kongtext", ' + term.options.fontFamily}); + window.helperTerm.open(term.element); + `); + + // safari loads the font, firefox & chrome dont + if (browser.browserType().name() === 'webkit') { + deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'loaded' }]); + } else { + deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'unloaded' }]); + } + + // good case: addon fixes layout for webfont in ctor + // the relayout happens async, so wait a bit with a promise + await ctx.page.evaluate(` + window.helperTerm.dispose(); + window.helperTerm = new Terminal({fontFamily: '"Kongtext", ' + term.options.fontFamily}); + window._webfontsAddon = new WebFontsAddon(); + window.helperTerm.loadAddon(window._webfontsAddon); + window.helperTerm.open(term.element); + `); + await timeout(100); + deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'loaded' }]); + + // cleanup this messy test case + await ctx.page.evaluate(` + window.helperTerm.dispose(); + window._webfontsAddon.dispose(); + `); + }); + }); + }); + +async function getDocumentFonts(): Promise { + return ctx.page.evaluate(`Array.from(document.fonts).map(ff => ({family: ff.family, status: ff.status}))`); +} From 104c63d7e035c4297432081a36e9e3ae1af80be8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 4 Oct 2024 17:52:45 +0200 Subject: [PATCH 12/38] cleanup files --- addons/addon-web-fonts/src/WebFontsAddon.ts | 145 -------------------- 1 file changed, 145 deletions(-) diff --git a/addons/addon-web-fonts/src/WebFontsAddon.ts b/addons/addon-web-fonts/src/WebFontsAddon.ts index 219a64afe..4a9f7e7ab 100644 --- a/addons/addon-web-fonts/src/WebFontsAddon.ts +++ b/addons/addon-web-fonts/src/WebFontsAddon.ts @@ -151,148 +151,3 @@ export class WebFontsAddon implements ITerminalAddon, IWebFontsApi { } } } - - - - - - - -/* eslint-disable */ -// TODO: place into test cases -/* -(window as any).__roboto = [ - // cyrillic-ext - new FontFace( - 'Roboto Mono', - "url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm3CWWoKC.woff2) format('woff2')", - { - style: 'italic', - weight: '100 700', - display: 'swap', - unicodeRange: 'U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F' - } - ), - // cyrillic - new FontFace( - 'Roboto Mono', - "url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm3mWWoKC.woff2) format('woff2')", - { - style: 'italic', - weight: '100 700', - display: 'swap', - unicodeRange: 'U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116' - } - ), - // greek - new FontFace( - 'Roboto Mono', - "url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm36WWoKC.woff2) format('woff2')", - { - style: 'italic', - weight: '100 700', - display: 'swap', - unicodeRange: 'U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF' - } - ), - // vietnamese - new FontFace( - 'Roboto Mono', - "url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm3KWWoKC.woff2) format('woff2')", - { - style: 'italic', - weight: '100 700', - display: 'swap', - unicodeRange: 'U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB' - } - ), - // latin-ext - new FontFace( - 'Roboto Mono', - "url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm3OWWoKC.woff2) format('woff2')", - { - style: 'italic', - weight: '100 700', - display: 'swap', - unicodeRange: 'U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF' - } - ), - // latin - new FontFace( - 'Roboto Mono', - "url(https://fonts.gstatic.com/s/robotomono/v23/L0x7DF4xlVMF-BfR8bXMIjhOm32WWg.woff2) format('woff2')", - { - style: 'italic', - weight: '100 700', - display: 'swap', - unicodeRange: 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD' - } - ), - // cyrillic-ext - new FontFace( - 'Roboto Mono', - "url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhGq3-OXg.woff2) format('woff2')", - { - style: 'normal', - weight: '100 700', - display: 'swap', - unicodeRange: 'U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F' - } - ), - // cyrillic - new FontFace( - 'Roboto Mono', - "url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhPq3-OXg.woff2) format('woff2')", - { - style: 'normal', - weight: '100 700', - display: 'swap', - unicodeRange: 'U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116' - } - ), - // greek - new FontFace( - 'Roboto Mono', - "url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhIq3-OXg.woff2) format('woff2')", - { - style: 'normal', - weight: '100 700', - display: 'swap', - unicodeRange: 'U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF' - } - ), - // vietnamese - new FontFace( - 'Roboto Mono', - "url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhEq3-OXg.woff2) format('woff2')", - { - style: 'normal', - weight: '100 700', - display: 'swap', - unicodeRange: 'U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB' - } - ), - // latin-ext - new FontFace( - 'Roboto Mono', - "url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhFq3-OXg.woff2) format('woff2')", - { - style: 'normal', - weight: '100 700', - display: 'swap', - unicodeRange: 'U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF' - } - ), - // latin - new FontFace( - 'Roboto Mono', - "url(https://fonts.gstatic.com/s/robotomono/v23/L0x5DF4xlVMF-BfR8bXMIjhLq38.woff2) format('woff2')", - { - style: 'normal', - weight: '100 700', - display: 'swap', - unicodeRange: 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD' - } - ), -]; -*/ From 085a2750387f8c446b9443d2d0a9d7187e38db9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 6 Oct 2024 23:53:30 +0200 Subject: [PATCH 13/38] fix promise return, reject on missing family name, doc polish --- addons/addon-web-fonts/README.md | 157 +++++++++++++----- addons/addon-web-fonts/src/WebFontsAddon.ts | 13 +- .../typings/addon-web-fonts.d.ts | 6 +- 3 files changed, 129 insertions(+), 47 deletions(-) diff --git a/addons/addon-web-fonts/README.md b/addons/addon-web-fonts/README.md index e910c21e5..ce43cbe5c 100644 --- a/addons/addon-web-fonts/README.md +++ b/addons/addon-web-fonts/README.md @@ -1,6 +1,7 @@ ## @xterm/addon-web-fonts -Addon to use webfonts with [xterm.js](https://github.com/xtermjs/xterm.js). This addon requires xterm.js v5+. +Addon to use webfonts with [xterm.js](https://github.com/xtermjs/xterm.js). +This addon requires xterm.js v5+. ### Install @@ -10,33 +11,43 @@ npm install --save @xterm/addon-web-fonts ### Issue with Webfonts -Webfonts are announced by CSS `font-face` rules (or its Javascript `FontFace` counterparts). Since font files tend to be quite big assets, browser engines often postpone their loading to an actual styling request of a codepoint matching a font file's `unicode-range`. In short - font files will not be loaded until really needed. +Webfonts are announced by CSS `font-face` rules (or its Javascript `FontFace` counterparts). +Since font files tend to be quite big assets, browser engines often postpone their loading +to an actual styling request of a codepoint matching a font file's `unicode-range`. +In short - font files will not be loaded until really needed. -xterm.js on the other hand heavily relies on exact measurements of character glyphs to layout its output. This is done by determining the glyph width (DOM renderer) or by creating a glyph texture (WebGl renderer) for every output character. -For performance reasons both is done in synchronous code and cached. This logic only works properly, -if a font glyph is available on its first usage, or a wrong glyph from a fallback font chosen by the browser will be used instead. +xterm.js on the other hand heavily relies on exact measurement of character glyphs +to layout its output. This is done by determining the glyph width (DOM renderer) or +by creating a glyph texture (WebGl renderer) for every output character. +For performance reasons both is done in synchronous code and cached. +This logic only works properly, if a font glyph is available on its first usage, +otherwise the browser will pick a glyph from a fallback font messing up the metrics. -For webfonts and xterm.js this means, that we cannot rely on the default loading strategy of the browser, but have to preload the font files before using that font in xterm.js. +For webfonts and xterm.js this means that we cannot rely on the default loading strategy +of the browser, but have to preload the font files before using that font in xterm.js. ### Static Preloading for the Rescue? -If you dont mind higher initial loading times of the embedding document, you can tell the browser to preload the needed font files by placing the following link elements in the document's head above any other CSS/Javascript: +If you dont mind higher initial loading times with a white page shown, +you can tell the browser to preload the needed font files by placing the following +link elements in the document's head above any other CSS/Javascript: ```html ... ``` -Downside of this approach is the much higher initial loading time showing as a white page. Browsers also will resort to system fonts, if the preloading takes too long, so with a slow connection or a very big font this solves literally nothing. +Browsers also will resort to system fonts, if the preloading takes too long. +So this solution has only a very limited scope. -### Preloading with WebFontsAddon +### Loading with WebFontsAddon -The webfonts addon offers several ways to deal with the loading of font assets without leaving the terminal in an unusable state. +The webfonts addon offers several ways to deal with font assets loading +without leaving the terminal in an unusable state. - -Recap - normally boostrapping of a new terminal involves these basic steps: +Recap - normally boostrapping of a new terminal involves these basic steps (Typescript): ```typescript import { Terminal } from '@xterm/xterm'; @@ -46,18 +57,20 @@ import { XYAddon } from '@xterm/addon-xy'; const terminal = new Terminal({fontFamily: 'monospace'}); // create and load all addons you want to use, e.g. fit addon -const xyAddon = new XYAddon(); -terminal.loadAddon(xyAddon); +const xyInstance = new XYAddon(); +terminal.loadAddon(xyInstance); // finally: call `open` of the terminal instance terminal.open(your_terminal_div_element); // <-- critical path for webfonts // more boostrapping goes here ... ``` +This code is guaranteed to work in all browsers synchronously, as the identifier `monospace` +will always be available. It will also work synchronously with any installed system font, +but breaks horribly with webfonts. The actual culprit here is the call to `terminal.open`, +which attaches the terminal to the DOM and starts the renderer with all the glyph caching +mentioned above, while the webfont is not yet fully available. -This synchronous code is guaranteed to work in all browsers, as the font `monospace` will always be available. -It will also work that way with any installed system font, but breaks horribly for webfonts. The actual culprit here is the call to `terminal.open`, which attaches the terminal to the DOM and starts the renderer with all the glyph caching mentioned above, while the webfont is not fully available yet. - -To fix that, the webfonts addon provides a waiting condition: +To fix that, the webfonts addon provides a waiting condition (Typescript): ```typescript import { Terminal } from '@xterm/xterm'; import { XYAddon } from '@xterm/addon-xy'; @@ -65,48 +78,55 @@ import { WebFontsAddon } from '@xterm/addon-web-fonts'; // create a `Terminal` instance, now with webfonts const terminal = new Terminal({fontFamily: '"Web Mono 1", "Super Powerline", monospace'}); -const xyAddon = new XYAddon(); -terminal.loadAddon(xyAddon); +const xyInstance = new XYAddon(); +terminal.loadAddon(xyInstance); -const webFontsAddon = new WebFontsAddon(); -terminal.loadAddon(webFontsAddon); +const webFontsInstance = new WebFontsAddon(); +terminal.loadAddon(webFontsInstance); // wait for webfonts to be fully loaded -webFontsAddon.loadFonts(['Web Mono 1', 'Super Powerline']).then(() => { +webFontsInstance.loadFonts(['Web Mono 1', 'Super Powerline']).then(() => { terminal.open(your_terminal_div_element); // more boostrapping goes here ... }); ``` -Here `loadFonts` will look up the font face objects in `document.fonts` and load them before continuing. -For this to work, you have to make sure, that the CSS `font-face` rules for these webfonts are loaded -on the initial document load (more precise - by the time this code runs). +Here `loadFonts` will look up the font face objects in `document.fonts` +and load them before continuing. For this to work, you have to make sure, +that the CSS `font-face` rules for these webfonts are loaded beforehand, +otherwise `loadFonts` will not find the font family names (promise will be +rejected for missing font family names). -Please note, that this code cannot run synchronous anymore, so you will have to split your -bootstrapping code into several stages. If thats too much of a hassle, you can also move the whole -bootstrapping under that waiting condition (import `loadFonts` for a static variant): +Please note, that this cannot run synchronous anymore, so you will have to split your +bootstrapping code into several stages. If that is too much of a hassle, +you can also move the whole bootstrapping under the waiting condition by using +the static loader instead (Typescript): ```typescript import { Terminal } from '@xterm/xterm'; import { XYAddon } from '@xterm/addon-xy'; -import { WebFontsAddon, loadFonts } from '@xterm/addon-web-fonts'; +// import static loader +import { loadFonts } from '@xterm/addon-web-fonts'; loadFonts(['Web Mono 1', 'Super Powerline']).then(() => { // create a `Terminal` instance, now with webfonts const terminal = new Terminal({fontFamily: '"Web Mono 1", "Super Powerline", monospace'}); - const xyAddon = new XYAddon(); - terminal.loadAddon(xyAddon); + const xyInstance = new XYAddon(); + terminal.loadAddon(xyInstance); - const webFontsAddon = new WebFontsAddon(); - terminal.loadAddon(webFontsAddon); + // optional when using static loader + const webfontsInstance = new WebFontsAddon(); + terminal.loadAddon(webfontsInstance); terminal.open(your_terminal_div_element); // more boostrapping goes here ... }); ``` +With the static loader creating and loading of the actual addon can be omitted, +as fonts are already loaded before any terminal setup happens. ### Webfont Loading at Runtime -Given you have a terminal already running and want to change the font family to a different not yet loaded webfont. -That can be achieved like this: +Given you have a terminal already running and want to change the font family +to a different not yet loaded webfont: ```typescript // either create font face objects in javascript const ff1 = new FontFace('New Web Mono', url1, ...); @@ -133,5 +153,68 @@ loadFonts(['New Web Mono']).then(() => { }); ``` +### Forced Layout Update -See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/addon-web-fonts/typings/addon-web-fonts.d.ts) for more advanced usage. +If you have the addon loaded into your terminal, you can force the terminal to update +the layout with the method `WebFontsAddon.relayout`. This might come handy, +if the terminal shows webfont related output issue for unknown reasons: +```typescript +... +// given - terminal shows weird font issues, run: +webFontsInstance.relayout().then(() => { + // also run resize logic here, e.g. fit addon + fitAddon.fit(); +}); +``` +Note that this method is only meant as a quickfix on a running terminal to keep it +in a working condition. A production-ready integration should never rely on it, +better fix the real root cause (most likely not properly awaiting the font loader +higher up in the code). + + +### Webfonts from Fontsource + +The addon has been tested to work with webfonts from fontsource. +Javascript example for `vite` with ESM import: +```javascript +import { Terminal } from '@xterm/xterm'; +import { FitAddon } from '@xterm/addon-fit'; +import { loadFonts } from '@xterm/addon-web-fonts'; +import '@xterm/xterm/css/xterm.css'; +import '@fontsource/roboto-mono'; +import '@fontsource/roboto-mono/400.css'; +import '@fontsource/roboto-mono/400-italic.css'; +import '@fontsource/roboto-mono/700.css'; +import '@fontsource/roboto-mono/700-italic.css'; + +async function main() { + let fontFamily = '"Roboto Mono", monospace'; + try { + await loadFonts(['Roboto Mono']); + } catch (e) { + fontFamily = 'monospace'; + } + + const terminal = new Terminal({ fontFamily }); + const fitAddon = new FitAddon(); + terminal.loadAddon(fitAddon); + terminal.open(document.getElementById('your-xterm-container-div')); + fitAddon.fit(); + + // sync writing shows up in Roboto Mono w'o FOUT + // and a fallback to monospace + terminal.write('put any unicode char here'); +} + +main(); +``` +The fontsource packages download the font files to your project folder to be delivered +from there later on. For security sensitive projects this should be the preferred way, +as it brings the font files under your control. + +The example furthermore contains proper exception handling with a fallback +(skipped in all other examples for better readability). + +--- + +Also see the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/addon-web-fonts/typings/addon-web-fonts.d.ts). diff --git a/addons/addon-web-fonts/src/WebFontsAddon.ts b/addons/addon-web-fonts/src/WebFontsAddon.ts index 4a9f7e7ab..cc0ccd1f5 100644 --- a/addons/addon-web-fonts/src/WebFontsAddon.ts +++ b/addons/addon-web-fonts/src/WebFontsAddon.ts @@ -94,7 +94,7 @@ function _loadFonts(fonts?: (string | FontFace)[]): Promise { const familyFiltered = ffs.filter(ff => font === unquote(ff.family)); toLoad = toLoad.concat(familyFiltered); if (!familyFiltered.length) { - console.warn(`font family "${font}" not registered in document.fonts`); + return Promise.reject(`font family "${font}" not registered in document.fonts`); } } } @@ -102,16 +102,15 @@ function _loadFonts(fonts?: (string | FontFace)[]): Promise { } -export async function loadFonts(fonts?: (string | FontFace)[]): Promise { - await document.fonts.ready; - return _loadFonts(fonts); +export function loadFonts(fonts?: (string | FontFace)[]): Promise { + return document.fonts.ready.then(() => _loadFonts(fonts)); } export class WebFontsAddon implements ITerminalAddon, IWebFontsApi { private _term: Terminal | undefined; - constructor(public forceInitialRelayout: boolean = true) { } + constructor(public initialRelayout: boolean = true) { } public dispose(): void { this._term = undefined; @@ -119,12 +118,12 @@ export class WebFontsAddon implements ITerminalAddon, IWebFontsApi { public activate(term: Terminal): void { this._term = term; - if (this.forceInitialRelayout) { + if (this.initialRelayout) { document.fonts.ready.then(() => this.relayout()); } } - public async loadFonts(fonts?: (string | FontFace)[]): Promise { + public loadFonts(fonts?: (string | FontFace)[]): Promise { return loadFonts(fonts); } diff --git a/addons/addon-web-fonts/typings/addon-web-fonts.d.ts b/addons/addon-web-fonts/typings/addon-web-fonts.d.ts index 9be3de216..c7a159ed1 100644 --- a/addons/addon-web-fonts/typings/addon-web-fonts.d.ts +++ b/addons/addon-web-fonts/typings/addon-web-fonts.d.ts @@ -13,9 +13,9 @@ declare module '@xterm/addon-web-fonts' { */ export class WebFontsAddon implements ITerminalAddon { /** - * @param forceInitialRelayout Force initial relayout, if a webfont was found (default true). + * @param initialRelayout Force initial relayout, if a webfont was found (default true). */ - constructor(forceInitialRelayout?: boolean); + constructor(initialRelayout?: boolean); public activate(terminal: Terminal): void; public dispose(): void; @@ -44,7 +44,7 @@ declare module '@xterm/addon-web-fonts' { * Call this method, if a terminal with webfonts is stuck with broken * glyph metrics. * - * Returns a promise on completion. + * The returned promise will resolve, when font loading and layouting are done. */ public relayout(): Promise; } From 363fc84fd27933111893199ff45a100c573cbc6d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 3 Jan 2026 09:02:06 -0800 Subject: [PATCH 14/38] Merge font-ligatures into addon-ligatures Fixes #5568 --- addons/addon-ligatures/LICENSE | 30 +- addons/addon-ligatures/package.json | 7 +- addons/addon-ligatures/src/font.ts | 2 +- .../src/fontLigatures/flatten.ts | 35 ++ .../src/fontLigatures/index.ts | 262 ++++++++++++ .../src/fontLigatures/merge.ts | 372 ++++++++++++++++++ .../src/fontLigatures/mergeRange.ts | 64 +++ .../src/fontLigatures/processors/6-1.ts | 82 ++++ .../src/fontLigatures/processors/6-2.ts | 96 +++++ .../src/fontLigatures/processors/6-3.ts | 73 ++++ .../src/fontLigatures/processors/8-1.ts | 69 ++++ .../src/fontLigatures/processors/classDef.ts | 85 ++++ .../src/fontLigatures/processors/coverage.ts | 43 ++ .../src/fontLigatures/processors/helper.ts | 163 ++++++++ .../fontLigatures/processors/substitution.ts | 62 +++ .../src/fontLigatures/tables.ts | 112 ++++++ .../src/fontLigatures/types.ts | 86 ++++ .../addon-ligatures/src/fontLigatures/walk.ts | 67 ++++ addons/addon-ligatures/src/index.ts | 2 +- addons/addon-ligatures/test/index.test.ts | 360 +++++++++++++++++ addons/addon-ligatures/test/merge.test.ts | 226 +++++++++++ .../addon-ligatures/test/mergeRange.test.ts | 50 +++ addons/addon-ligatures/test/parse.test.ts | 2 +- package-lock.json | 74 ++-- 24 files changed, 2385 insertions(+), 39 deletions(-) create mode 100644 addons/addon-ligatures/src/fontLigatures/flatten.ts create mode 100644 addons/addon-ligatures/src/fontLigatures/index.ts create mode 100644 addons/addon-ligatures/src/fontLigatures/merge.ts create mode 100644 addons/addon-ligatures/src/fontLigatures/mergeRange.ts create mode 100644 addons/addon-ligatures/src/fontLigatures/processors/6-1.ts create mode 100644 addons/addon-ligatures/src/fontLigatures/processors/6-2.ts create mode 100644 addons/addon-ligatures/src/fontLigatures/processors/6-3.ts create mode 100644 addons/addon-ligatures/src/fontLigatures/processors/8-1.ts create mode 100644 addons/addon-ligatures/src/fontLigatures/processors/classDef.ts create mode 100644 addons/addon-ligatures/src/fontLigatures/processors/coverage.ts create mode 100644 addons/addon-ligatures/src/fontLigatures/processors/helper.ts create mode 100644 addons/addon-ligatures/src/fontLigatures/processors/substitution.ts create mode 100644 addons/addon-ligatures/src/fontLigatures/tables.ts create mode 100644 addons/addon-ligatures/src/fontLigatures/types.ts create mode 100644 addons/addon-ligatures/src/fontLigatures/walk.ts create mode 100644 addons/addon-ligatures/test/index.test.ts create mode 100644 addons/addon-ligatures/test/merge.test.ts create mode 100644 addons/addon-ligatures/test/mergeRange.test.ts diff --git a/addons/addon-ligatures/LICENSE b/addons/addon-ligatures/LICENSE index b442934b7..b7ca5139a 100644 --- a/addons/addon-ligatures/LICENSE +++ b/addons/addon-ligatures/LICENSE @@ -1,6 +1,30 @@ -MIT License - -Copyright (c) 2018 +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 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 diff --git a/addons/addon-ligatures/package.json b/addons/addon-ligatures/package.json index 85a19c36e..5150f3838 100644 --- a/addons/addon-ligatures/package.json +++ b/addons/addon-ligatures/package.json @@ -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" } diff --git a/addons/addon-ligatures/src/font.ts b/addons/addon-ligatures/src/font.ts index ed7910cf4..196651ac4 100644 --- a/addons/addon-ligatures/src/font.ts +++ b/addons/addon-ligatures/src/font.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Font, loadBuffer } from 'font-ligatures'; +import { Font, loadBuffer } from './fontLigatures/index'; import parse from './parse'; diff --git a/addons/addon-ligatures/src/fontLigatures/flatten.ts b/addons/addon-ligatures/src/fontLigatures/flatten.ts new file mode 100644 index 000000000..38e99c3ad --- /dev/null +++ b/addons/addon-ligatures/src/fontLigatures/flatten.ts @@ -0,0 +1,35 @@ +import { LookupTree, FlattenedLookupTree, LookupTreeEntry, FlattenedLookupTreeEntry } from './types'; + +export default function flatten(tree: LookupTree): FlattenedLookupTree { + const result: FlattenedLookupTree = {}; + 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: LookupTreeEntry): FlattenedLookupTreeEntry { + const result: FlattenedLookupTreeEntry = {}; + + 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; +} diff --git a/addons/addon-ligatures/src/fontLigatures/index.ts b/addons/addon-ligatures/src/fontLigatures/index.ts new file mode 100644 index 000000000..e91eea92b --- /dev/null +++ b/addons/addon-ligatures/src/fontLigatures/index.ts @@ -0,0 +1,262 @@ +import * as opentype from 'opentype.js'; +import LRUCache = require('lru-cache'); + +import { Font, LigatureData, FlattenedLookupTree, LookupTree, Options } 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 Font { + private _font: opentype.Font; + private _lookupTrees: { tree: FlattenedLookupTree; processForward: boolean; }[] = []; + private _glyphLookups: { [glyphId: string]: number[] } = {}; + private _cache?: LRUCache; + + constructor(font: opentype.Font, options: Required) { + this._font = font; + + if (options.cacheSize > 0) { + this._cache = new LRUCache({ + max: options.cacheSize, + length: ((val: LigatureData | [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: LookupTree[] = []; + 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); + } + } + } + + findLigatures(text: string): LigatureData { + 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: LigatureData = { + inputGlyphs: glyphIds, + outputGlyphs: result.sequence, + contextRanges: result.ranges + }; + if (this._cache) { + this._cache.set(text, finalResult); + } + + return finalResult; + } + + 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?: Options): Font { + const font = opentype.parse(buffer); + return new FontImpl(font, { + cacheSize: 0, + ...options + }); +} + +export { Font, LigatureData, Options }; diff --git a/addons/addon-ligatures/src/fontLigatures/merge.ts b/addons/addon-ligatures/src/fontLigatures/merge.ts new file mode 100644 index 000000000..d0b1f23e9 --- /dev/null +++ b/addons/addon-ligatures/src/fontLigatures/merge.ts @@ -0,0 +1,372 @@ +import { LookupTree, LookupTreeEntry } 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: LookupTree[]): LookupTree { + const result: LookupTree = { + 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: LookupTree, mergeTree: LookupTree): 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: LookupTreeEntry = 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: LookupTreeEntry, mergeTree: LookupTreeEntry): 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 Overlap { + 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]): Overlap { + const result: Overlap = { + 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]): Overlap { + // Disjoint + if (first < second[0] || first > second[1]) { + return { + first: [first], + second: [second], + both: null + }; + } + + const result: Overlap = { + 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; + } else { + return [start, end]; + } +} + +/** + * Clones an individual lookup tree entry. + * + * @param entry Lookup tree entry to clone + */ +function cloneEntry(entry: LookupTreeEntry): LookupTreeEntry { + const result: LookupTreeEntry = {}; + + 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: LookupTree): LookupTree { + const individual: { [glyphId: string]: LookupTreeEntry; } = {}; + 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) + })) + }; +} diff --git a/addons/addon-ligatures/src/fontLigatures/mergeRange.ts b/addons/addon-ligatures/src/fontLigatures/mergeRange.ts new file mode 100644 index 000000000..7c872a8a3 --- /dev/null +++ b/addons/addon-ligatures/src/fontLigatures/mergeRange.ts @@ -0,0 +1,64 @@ +/** + * 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; + } else 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; + } else 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; + } else 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; + } else { + // 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; +} diff --git a/addons/addon-ligatures/src/fontLigatures/processors/6-1.ts b/addons/addon-ligatures/src/fontLigatures/processors/6-1.ts new file mode 100644 index 000000000..2f0dad180 --- /dev/null +++ b/addons/addon-ligatures/src/fontLigatures/processors/6-1.ts @@ -0,0 +1,82 @@ +import { ChainingContextualSubstitutionTable, Lookup } from '../tables'; +import { LookupTree } from '../types'; + +import { listGlyphsByIndex } from './coverage'; +import { processInputPosition, processLookaheadPosition, processBacktrackPosition, getInputTree, EntryMeta } 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.Format1, lookups: Lookup[], tableIndex: number): LookupTree { + const result: LookupTree = { + 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: EntryMeta[] = 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; +} diff --git a/addons/addon-ligatures/src/fontLigatures/processors/6-2.ts b/addons/addon-ligatures/src/fontLigatures/processors/6-2.ts new file mode 100644 index 000000000..9cd8eaf33 --- /dev/null +++ b/addons/addon-ligatures/src/fontLigatures/processors/6-2.ts @@ -0,0 +1,96 @@ +import { ChainingContextualSubstitutionTable, Lookup } from '../tables'; +import { LookupTree } from '../types'; +import mergeTrees from '../merge'; + +import { listGlyphsByIndex } from './coverage'; +import getGlyphClass, { listClassGlyphs } from './classDef'; +import { processInputPosition, processLookaheadPosition, processBacktrackPosition, getInputTree, EntryMeta } 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.Format2, lookups: Lookup[], tableIndex: number): LookupTree { + const results: LookupTree[] = []; + + 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: LookupTree = { + individual: {}, + range: [] + }; + + let currentEntries: EntryMeta[] = 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); +} diff --git a/addons/addon-ligatures/src/fontLigatures/processors/6-3.ts b/addons/addon-ligatures/src/fontLigatures/processors/6-3.ts new file mode 100644 index 000000000..fc671e7b7 --- /dev/null +++ b/addons/addon-ligatures/src/fontLigatures/processors/6-3.ts @@ -0,0 +1,73 @@ +import { ChainingContextualSubstitutionTable, Lookup } from '../tables'; +import { LookupTree } from '../types'; + +import { listGlyphsByIndex } from './coverage'; +import { processInputPosition, processLookaheadPosition, processBacktrackPosition, getInputTree, EntryMeta } 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.Format3, lookups: Lookup[], tableIndex: number): LookupTree { + const result: LookupTree = { + individual: {}, + range: [] + }; + + const firstGlyphs = listGlyphsByIndex(table.inputCoverage[0]); + + for (const { glyphId } of firstGlyphs) { + let currentEntries: EntryMeta[] = 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; +} diff --git a/addons/addon-ligatures/src/fontLigatures/processors/8-1.ts b/addons/addon-ligatures/src/fontLigatures/processors/8-1.ts new file mode 100644 index 000000000..620d2e20a --- /dev/null +++ b/addons/addon-ligatures/src/fontLigatures/processors/8-1.ts @@ -0,0 +1,69 @@ +import { ReverseChainingContextualSingleSubstitutionTable } from '../tables'; +import { LookupTree, LookupTreeEntry } from '../types'; + +import { listGlyphsByIndex } from './coverage'; +import { processLookaheadPosition, processBacktrackPosition, EntryMeta } 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: ReverseChainingContextualSingleSubstitutionTable, tableIndex: number): LookupTree { + const result: LookupTree = { + individual: {}, + range: [] + }; + + const glyphs = listGlyphsByIndex(table.coverage); + + for (const { glyphId, index } of glyphs) { + const initialEntry: LookupTreeEntry = {}; + if (Array.isArray(glyphId)) { + result.range.push({ + entry: initialEntry, + range: glyphId + }); + } else { + result.individual[glyphId] = initialEntry; + } + + let currentEntries: EntryMeta[] = [{ + 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; +} diff --git a/addons/addon-ligatures/src/fontLigatures/processors/classDef.ts b/addons/addon-ligatures/src/fontLigatures/processors/classDef.ts new file mode 100644 index 000000000..3f2dc21ec --- /dev/null +++ b/addons/addon-ligatures/src/fontLigatures/processors/classDef.ts @@ -0,0 +1,85 @@ +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 { + 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); + } else { + 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.Format2, glyphId: [number, number]): Map { + let classStart: number = glyphId[0]; + let 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.Format2, 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 []; + } +} diff --git a/addons/addon-ligatures/src/fontLigatures/processors/coverage.ts b/addons/addon-ligatures/src/fontLigatures/processors/coverage.ts new file mode 100644 index 000000000..0ee3f42d6 --- /dev/null +++ b/addons/addon-ligatures/src/fontLigatures/processors/coverage.ts @@ -0,0 +1,43 @@ +import { CoverageTable } from '../tables'; + +/** + * Get the index of the given glyph in the coverage table, or null if it is not + * present in the table. + * + * @param table JSON representation of the coverage table + * @param glyphId Index of the glyph to look for + */ +export default function getCoverageGlyphIndex(table: CoverageTable, glyphId: number): number | null { + switch (table.format) { + // https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#coverage-format-1 + case 1: + const index = table.glyphs.indexOf(glyphId); + return index !== -1 + ? index + : null; + // https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#coverage-format-2 + case 2: + const range = table.ranges + .find(range => range.start <= glyphId && range.end >= glyphId); + return range + ? range.index + : null; + } +} + +export function listGlyphsByIndex(table: CoverageTable): { glyphId: number | [number, number]; index: number; }[] { + switch (table.format) { + case 1: + return table.glyphs.map((glyphId, index) => ({ glyphId, index })); + case 2: + let results: { glyphId: number | [number, number]; index: number; }[] = []; + for (const [index, range] of table.ranges.entries()) { + if (range.end === range.start) { + results.push({ glyphId: range.start, index }); + } else { + results.push({ glyphId: [range.start, range.end + 1], index }); + } + } + return results; + } +} diff --git a/addons/addon-ligatures/src/fontLigatures/processors/helper.ts b/addons/addon-ligatures/src/fontLigatures/processors/helper.ts new file mode 100644 index 000000000..7186a22de --- /dev/null +++ b/addons/addon-ligatures/src/fontLigatures/processors/helper.ts @@ -0,0 +1,163 @@ +import { LookupTreeEntry, LookupTree } from '../types'; +import { SubstitutionLookupRecord, Lookup } from '../tables'; + +import { getIndividualSubstitutionGlyph, getRangeSubstitutionGlyphs } from './substitution'; + +export interface EntryMeta { + entry: LookupTreeEntry; + substitutions: (number | null)[]; +} + +export function processInputPosition( + glyphs: (number | [number, number])[], + position: number, + currentEntries: EntryMeta[], + lookupRecords: SubstitutionLookupRecord[], + lookups: Lookup[] +): EntryMeta[] { + const nextEntries: EntryMeta[] = []; + for (const currentEntry of currentEntries) { + currentEntry.entry.forward = { + individual: {}, + range: [] + }; + for (const glyph of glyphs) { + nextEntries.push(...getInputTree( + currentEntry.entry.forward, + lookupRecords, + lookups, + position, + glyph + ).map(({ entry, substitution }) => ({ + entry, + substitutions: [...currentEntry.substitutions, substitution] + }))); + } + } + + return nextEntries; +} + +export function processLookaheadPosition( + glyphs: (number | [number, number])[], + currentEntries: EntryMeta[] +): EntryMeta[] { + const nextEntries: EntryMeta[] = []; + for (const currentEntry of currentEntries) { + for (const glyph of glyphs) { + const entry: LookupTreeEntry = {}; + if (!currentEntry.entry.forward) { + currentEntry.entry.forward = { + individual: {}, + range: [] + }; + } + nextEntries.push({ + entry, + substitutions: currentEntry.substitutions + }); + + if (Array.isArray(glyph)) { + currentEntry.entry.forward.range.push({ + entry, + range: glyph + }); + } else { + currentEntry.entry.forward.individual[glyph] = entry; + } + } + } + + return nextEntries; +} + +export function processBacktrackPosition( + glyphs: (number | [number, number])[], + currentEntries: EntryMeta[] +): EntryMeta[] { + const nextEntries: EntryMeta[] = []; + for (const currentEntry of currentEntries) { + for (const glyph of glyphs) { + const entry: LookupTreeEntry = {}; + if (!currentEntry.entry.reverse) { + currentEntry.entry.reverse = { + individual: {}, + range: [] + }; + } + nextEntries.push({ + entry, + substitutions: currentEntry.substitutions + }); + + if (Array.isArray(glyph)) { + currentEntry.entry.reverse.range.push({ + entry, + range: glyph + }); + } else { + currentEntry.entry.reverse.individual[glyph] = entry; + } + } + } + + return nextEntries; +} + +export function getInputTree(tree: LookupTree, substitutions: SubstitutionLookupRecord[], lookups: Lookup[], inputIndex: number, glyphId: number | [number, number]): { entry: LookupTreeEntry; substitution: number | null; }[] { + const result: { entry: LookupTreeEntry; substitution: number | null; }[] = []; + if (!Array.isArray(glyphId)) { + tree.individual[glyphId] = {}; + result.push({ + entry: tree.individual[glyphId], + substitution: getSubstitutionAtPosition(substitutions, lookups, inputIndex, glyphId) + }); + } else { + const subs = getSubstitutionAtPositionRange(substitutions, lookups, inputIndex, glyphId); + for (const [range, substitution] of subs) { + const entry: LookupTreeEntry = {}; + if (Array.isArray(range)) { + tree.range.push({ range, entry }); + } else { + tree.individual[range] = {}; + } + result.push({ entry, substitution }); + } + } + + return result; +} + +function getSubstitutionAtPositionRange(substitutions: SubstitutionLookupRecord[], lookups: Lookup[], index: number, range: [number, number]): Map { + for (const substitution of substitutions.filter(s => s.sequenceIndex === index)) { + for (const substitutionTable of (lookups[substitution.lookupListIndex] as Lookup.Type1).subtables) { + const sub = getRangeSubstitutionGlyphs( + substitutionTable, + range + ); + + if (!Array.from(sub.values()).every(val => val !== null)) { + return sub; + } + } + } + + return new Map([[range, null]]); +} + +function getSubstitutionAtPosition(substitutions: SubstitutionLookupRecord[], lookups: Lookup[], index: number, glyphId: number): number | null { + for (const substitution of substitutions.filter(s => s.sequenceIndex === index)) { + for (const substitutionTable of (lookups[substitution.lookupListIndex] as Lookup.Type1).subtables) { + const sub = getIndividualSubstitutionGlyph( + substitutionTable, + glyphId + ); + + if (sub !== null) { + return sub; + } + } + } + + return null; +} diff --git a/addons/addon-ligatures/src/fontLigatures/processors/substitution.ts b/addons/addon-ligatures/src/fontLigatures/processors/substitution.ts new file mode 100644 index 000000000..975d7a25e --- /dev/null +++ b/addons/addon-ligatures/src/fontLigatures/processors/substitution.ts @@ -0,0 +1,62 @@ +import { SubstitutionTable } from '../tables'; + +import getCoverageGlyphIndex from './coverage'; + +/** + * Get the substitution glyph for the givne glyph, or null if the glyph was not + * found in the table. + * + * @param table JSON representation of the substitution table + * @param glyphId The index of the glpyh to find substitutions for + */ +export function getRangeSubstitutionGlyphs(table: SubstitutionTable, glyphId: [number, number]): Map<[number, number] | number, number | null> { + let replacementStart: number = glyphId[0]; + let currentReplacement: number | null = getIndividualSubstitutionGlyph(table, replacementStart); + let search: number = glyphId[0] + 1; + + const result = new Map<[number, number] | number, number | null>(); + + while (search < glyphId[1]) { + const sub = getIndividualSubstitutionGlyph(table, search); + if (sub !== currentReplacement) { + if (search - replacementStart <= 1) { + result.set(replacementStart, currentReplacement); + } else { + result.set([replacementStart, search], currentReplacement); + } + } + + search++; + } + + if (search - replacementStart <= 1) { + result.set(replacementStart, currentReplacement); + } else { + result.set([replacementStart, search], currentReplacement); + } + + return result; +} + +export function getIndividualSubstitutionGlyph(table: SubstitutionTable, glyphId: number): number | null { + const coverageIndex = getCoverageGlyphIndex(table.coverage, glyphId); + + // istanbul ignore next - invalid font + if (coverageIndex === null) { + return null; + } + + switch (table.substFormat) { + // https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#11-single-substitution-format-1 + case 1: + // TODO: determine if there's a rhyme or reason to the 16-bit + // wraparound and if it can ever be a different number + return (glyphId + table.deltaGlyphId) % (2 ** 16); + // https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#12-single-substitution-format-2 + case 2: + // tslint:disable-next-line + return table.substitute[coverageIndex] != null + ? table.substitute[coverageIndex] + : null; + } +} diff --git a/addons/addon-ligatures/src/fontLigatures/tables.ts b/addons/addon-ligatures/src/fontLigatures/tables.ts new file mode 100644 index 000000000..d3eccd08d --- /dev/null +++ b/addons/addon-ligatures/src/fontLigatures/tables.ts @@ -0,0 +1,112 @@ +export type SubstitutionTable = SubstitutionTable.Format1 | SubstitutionTable.Format2; +export namespace SubstitutionTable { + export interface Format1 { + substFormat: 1; + coverage: CoverageTable; + deltaGlyphId: number; + } + + export interface Format2 { + substFormat: 2; + coverage: CoverageTable; + substitute: number[]; + } +} + +export type CoverageTable = CoverageTable.Format1 | CoverageTable.Format2; +export namespace CoverageTable { + export interface Format1 { + format: 1; + glyphs: number[]; + } + + export interface Format2 { + format: 2; + ranges: { + start: number; + end: number; + index: number; + }[]; + } +} + +export type ChainingContextualSubstitutionTable = ChainingContextualSubstitutionTable.Format1 | + ChainingContextualSubstitutionTable.Format2 | ChainingContextualSubstitutionTable.Format3; +export namespace ChainingContextualSubstitutionTable { + export interface Format1 { + substFormat: 1; + coverage: CoverageTable; + chainRuleSets: ChainSubRuleTable[][]; + } + + export interface Format2 { + substFormat: 2; + coverage: CoverageTable; + backtrackClassDef: ClassDefTable; + inputClassDef: ClassDefTable; + lookaheadClassDef: ClassDefTable; + chainClassSet: (null | ChainSubClassRuleTable[])[]; + } + + export interface Format3 { + substFormat: 3; + backtrackCoverage: CoverageTable[]; + inputCoverage: CoverageTable[]; + lookaheadCoverage: CoverageTable[]; + lookupRecords: SubstitutionLookupRecord[]; + } +} + +export interface ReverseChainingContextualSingleSubstitutionTable { + substFormat: 1; + coverage: CoverageTable; + backtrackCoverage: CoverageTable[]; + lookaheadCoverage: CoverageTable[]; + substitutes: number[]; +} + +export type ClassDefTable = ClassDefTable.Format2; +export namespace ClassDefTable { + export interface Format2 { + format: 2; + ranges: { + start: number; + end: number; + classId: number; + }[]; + } +} + +export interface SubstitutionLookupRecord { + sequenceIndex: number; + lookupListIndex: number; +} + +export type ChainSubRuleTable = ChainSubClassRuleTable; +export interface ChainSubClassRuleTable { + backtrack: number[]; + input: number[]; + lookahead: number[]; + lookupRecords: SubstitutionLookupRecord[]; +} + +export type Lookup = Lookup.Type1 | Lookup.Type6 | Lookup.Type8; +export namespace Lookup { + export interface Type1 { + lookupType: 1; + lookupFlag: number; + subtables: SubstitutionTable[]; + } + + export interface Type6 { + lookupType: 6; + lookupFlag: number; + subtables: ChainingContextualSubstitutionTable[]; + } + + export interface Type8 { + lookupType: 8; + lookupFlag: number; + subtables: ReverseChainingContextualSingleSubstitutionTable[]; + } +} diff --git a/addons/addon-ligatures/src/fontLigatures/types.ts b/addons/addon-ligatures/src/fontLigatures/types.ts new file mode 100644 index 000000000..9364c75c4 --- /dev/null +++ b/addons/addon-ligatures/src/fontLigatures/types.ts @@ -0,0 +1,86 @@ +export interface SubstitutionResult { + index: number; + contextRange: [number, number]; +} + +/** + * Information about ligatures found in a sequence of text + */ +export interface LigatureData { + /** + * The list of font glyphs in the input text. + */ + inputGlyphs: number[]; + + /** + * The list of font glyphs after performing replacements for font ligatures. + */ + outputGlyphs: number[]; + + /** + * Sorted array of ranges that must be rendered together to produce the + * ligatures in the output sequence. The ranges are inclusive on the left and + * exclusive on the right. + */ + contextRanges: [number, number][]; +} + +export interface Font { + /** + * Scans the provided text for font ligatures, returning an object with + * metadata about the text and any ligatures found. + * + * @param text String to search for ligatures + */ + findLigatures(text: string): LigatureData; + + /** + * Scans the provided text for font ligatures, returning an array of ranges + * where ligatures are located. + * + * @param text String to search for ligatures + */ + findLigatureRanges(text: string): [number, number][]; +} + +export interface Options { + /** + * Optional size of previous results to store, measured in total number of + * characters from input strings. Defaults to no cache (0) + */ + cacheSize?: number; +} + +export interface LookupTree { + individual: { + [glyphId: string]: LookupTreeEntry; + }; + range: { + range: [number, number]; + entry: LookupTreeEntry; + }[]; +} + +export interface LookupTreeEntry { + lookup?: LookupResult; + forward?: LookupTree; + reverse?: LookupTree; +} + +export interface LookupResult { + substitutions: (number | null)[]; + length: number; + index: number; + subIndex: number; + contextRange: [number, number]; +} + +export interface FlattenedLookupTree { + [glyphId: string]: FlattenedLookupTreeEntry; +} + +export interface FlattenedLookupTreeEntry { + lookup?: LookupResult; + forward?: FlattenedLookupTree; + reverse?: FlattenedLookupTree; +} diff --git a/addons/addon-ligatures/src/fontLigatures/walk.ts b/addons/addon-ligatures/src/fontLigatures/walk.ts new file mode 100644 index 000000000..56d6eac1f --- /dev/null +++ b/addons/addon-ligatures/src/fontLigatures/walk.ts @@ -0,0 +1,67 @@ +import { FlattenedLookupTree, LookupResult } from './types'; + +export default function walkTree(tree: FlattenedLookupTree, sequence: number[], startIndex: number, index: number): LookupResult | undefined { + const glyphId = sequence[index]; + let subtree = tree[glyphId]; + if (!subtree) { + return undefined; + } + + let lookup = subtree.lookup; + if (subtree.reverse) { + const reverseLookup = walkReverse(subtree.reverse, sequence, startIndex); + + if ( + (!lookup && reverseLookup) || + ( + reverseLookup && lookup && ( + lookup.index > reverseLookup.index || + (lookup.index === reverseLookup.index && lookup.subIndex > reverseLookup.subIndex) + ) + ) + ) { + lookup = reverseLookup; + } + } + + if (++index >= sequence.length || !subtree.forward) { + return lookup; + } + + const forwardLookup = walkTree(subtree.forward, sequence, startIndex, index); + + if ( + (!lookup && forwardLookup) || + ( + forwardLookup && lookup && ( + lookup.index > forwardLookup.index || + (lookup.index === forwardLookup.index && lookup.subIndex > forwardLookup.subIndex) + ) + ) + ) { + lookup = forwardLookup; + } + + return lookup; +} + +function walkReverse(tree: FlattenedLookupTree, sequence: number[], index: number): LookupResult | undefined { + let subtree = tree[sequence[--index]]; + let lookup: LookupResult | undefined = subtree && subtree.lookup; + while (subtree) { + if ( + (!lookup && subtree.lookup) || + (subtree.lookup && lookup && lookup.index > subtree.lookup.index) + ) { + lookup = subtree.lookup; + } + + if (--index < 0 || !subtree.reverse) { + break; + } + + subtree = subtree.reverse[sequence[index]]; + } + + return lookup; +} diff --git a/addons/addon-ligatures/src/index.ts b/addons/addon-ligatures/src/index.ts index bd8ff215f..0c67f510b 100644 --- a/addons/addon-ligatures/src/index.ts +++ b/addons/addon-ligatures/src/index.ts @@ -4,7 +4,7 @@ */ import type { Terminal } from '@xterm/xterm'; -import { Font } from 'font-ligatures'; +import { Font } from './fontLigatures/index'; import load from './font'; diff --git a/addons/addon-ligatures/test/index.test.ts b/addons/addon-ligatures/test/index.test.ts new file mode 100644 index 000000000..ecef7661a --- /dev/null +++ b/addons/addon-ligatures/test/index.test.ts @@ -0,0 +1,360 @@ +/** + * 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'; + +const fontLigatures = require('../out-esbuild/fontLigatures/index'); +const loadBuffer: (buffer: ArrayBuffer, options?: { cacheSize?: number }) => Font = fontLigatures.loadBuffer; + +interface Font { + findLigatures(text: string): { outputGlyphs: number[]; contextRanges: [number, number][] }; + findLigatureRanges(text: string): [number, number][]; +} + +interface TestCase { + font: string; + input: string; + glyphs: number[]; + ranges: [number, number][]; +} + +const fira = (input: string, glyphs: number[], ranges: [number, number][]): TestCase => + ({ font: 'Fira Code', input, glyphs, ranges }); + +const iosevka = (input: string, glyphs: number[], ranges: [number, number][]): TestCase => + ({ font: 'Iosevka', input, glyphs, ranges }); + +const monoid = (input: string, glyphs: number[], ranges: [number, number][]): TestCase => + ({ font: 'Monoid', input, glyphs, ranges }); + +const ubuntu = (input: string, glyphs: number[], ranges: [number, number][]): TestCase => + ({ font: 'Ubuntu Mono', input, glyphs, ranges }); + +const firaCases: TestCase[] = [ + 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, 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: TestCase[] = [ + 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('', [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: TestCase[] = [ + ubuntu('==>', [32, 32, 33], []) +]; + +const fontPaths: Record = { + '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 = new Map(); + +function loadFont(fontName: string): Font { + 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); + }); + }); +}); \ No newline at end of file diff --git a/addons/addon-ligatures/test/merge.test.ts b/addons/addon-ligatures/test/merge.test.ts new file mode 100644 index 000000000..0dc157f2a --- /dev/null +++ b/addons/addon-ligatures/test/merge.test.ts @@ -0,0 +1,226 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert } from 'chai'; + +const mergeTrees = require('../out-esbuild/fontLigatures/merge').default; + +interface LookupResult { + contextRange: [number, number]; + index: number; + subIndex: number; + length: number; + substitutions: number[]; +} + +function lookup(substitutionGlyph: number, index?: number, subIndex?: number): LookupResult { + 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] + }] + }); + }); + }); +}); \ No newline at end of file diff --git a/addons/addon-ligatures/test/mergeRange.test.ts b/addons/addon-ligatures/test/mergeRange.test.ts new file mode 100644 index 000000000..91642e8df --- /dev/null +++ b/addons/addon-ligatures/test/mergeRange.test.ts @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert } from 'chai'; + +const mergeRange = require('../out-esbuild/fontLigatures/mergeRange').default; + +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]]); + }); +}); diff --git a/addons/addon-ligatures/test/parse.test.ts b/addons/addon-ligatures/test/parse.test.ts index d6c56d2cd..de846f4a9 100644 --- a/addons/addon-ligatures/test/parse.test.ts +++ b/addons/addon-ligatures/test/parse.test.ts @@ -8,7 +8,7 @@ import { assert } from 'chai'; const parse = require('../out-esbuild/parse').default; // TODO: integrate tests from http://test.csswg.org/suites/css-fonts-4_dev/nightly-unstable/ -describe('parse', () => { +describe('addon-ligatures - parse', () => { it('parses individual families', () => { assert.deepEqual(parse('monospace'), ['monospace']); }); diff --git a/package-lock.json b/package-lock.json index 71dff295c..e48e512cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -87,11 +87,14 @@ "version": "0.10.0", "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" }, @@ -99,6 +102,24 @@ "node": ">8.0.0" } }, + "addons/addon-ligatures/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "addons/addon-ligatures/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "addons/addon-progress": { "name": "@xterm/addon-progress", "version": "0.2.0", @@ -1701,6 +1722,13 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mathjs": { "version": "6.0.12", "resolved": "https://registry.npmjs.org/@types/mathjs/-/mathjs-6.0.12.tgz", @@ -1737,6 +1765,13 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/opentype.js": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@types/opentype.js/-/opentype.js-0.7.2.tgz", + "integrity": "sha512-Riz6WyBUBEFs7YqSsJya3SbDHJZ6BmMkY7bzNoue6rtwj+RNilLc+mgOX/eJ0Y0asq16FSU6DatBeOg8ZMy2UQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/puppeteer": { "version": "5.4.7", "resolved": "https://registry.npmjs.org/@types/puppeteer/-/puppeteer-5.4.7.tgz", @@ -4228,6 +4263,8 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/font-finder/-/font-finder-1.1.0.tgz", "integrity": "sha512-wpCL2uIbi6GurJbU7ZlQ3nGd61Ho+dSU6U83/xJT5UPFfN35EeCW/rOtS+5k+IuEZu2SYmHzDIPL9eA5tSYRAw==", + "dev": true, + "license": "MIT", "dependencies": { "get-system-fonts": "^2.0.0", "promise-stream-reader": "^1.0.1" @@ -4236,35 +4273,6 @@ "node": ">8.0.0" } }, - "node_modules/font-ligatures": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/font-ligatures/-/font-ligatures-1.4.1.tgz", - "integrity": "sha512-7W6zlfyhvCqShZ5ReUWqmSd9vBaUudW0Hxis+tqUjtHhsPU+L3Grf8mcZAtCiXHTzorhwdRTId2WeH/88gdFkw==", - "dependencies": { - "font-finder": "^1.0.3", - "lru-cache": "^6.0.0", - "opentype.js": "^0.8.0" - }, - "engines": { - "node": ">8.0.0" - } - }, - "node_modules/font-ligatures/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/font-ligatures/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -4490,6 +4498,8 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-system-fonts/-/get-system-fonts-2.0.2.tgz", "integrity": "sha512-zzlgaYnHMIEgHRrfC7x0Qp0Ylhw/sHpM6MHXeVBTYIsvGf5GpbnClB+Q6rAPdn+0gd2oZZIo6Tj3EaWrt4VhDQ==", + "dev": true, + "license": "MIT", "engines": { "node": ">8.0.0" } @@ -6700,6 +6710,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-stream-reader/-/promise-stream-reader-1.0.1.tgz", "integrity": "sha512-Tnxit5trUjBAqqZCGWwjyxhmgMN4hGrtpW3Oc/tRI4bpm/O2+ej72BB08l6JBnGQgVDGCLvHFGjGgQS6vzhwXg==", + "dev": true, + "license": "MIT", "engines": { "node": ">8.0.0" } From 21b8f77afb5fac4142d1c1167c47611d7f1cf69f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 3 Jan 2026 09:16:58 -0800 Subject: [PATCH 15/38] Move unit tests to right folder, push font fixtures --- addons/addon-ligatures/.gitignore | 1 - .../addon-ligatures/fonts/FiraCode-Regular.otf | Bin 0 -> 159496 bytes .../addon-ligatures/fonts/Monoid-Regular.ttf | Bin 0 -> 79248 bytes .../fonts/UbuntuMono-Regular.ttf | Bin 0 -> 205748 bytes .../addon-ligatures/fonts/iosevka-regular.ttf | Bin 0 -> 399128 bytes .../{test => src/fontLigatures}/index.test.ts | 12 +++++------- .../{test => src/fontLigatures}/merge.test.ts | 3 +-- .../fontLigatures}/mergeRange.test.ts | 3 +-- .../{test => src}/parse.test.ts | 3 +-- addons/addon-ligatures/src/tsconfig.json | 4 +++- 10 files changed, 11 insertions(+), 15 deletions(-) create mode 100644 addons/addon-ligatures/fonts/FiraCode-Regular.otf create mode 100644 addons/addon-ligatures/fonts/Monoid-Regular.ttf create mode 100644 addons/addon-ligatures/fonts/UbuntuMono-Regular.ttf create mode 100644 addons/addon-ligatures/fonts/iosevka-regular.ttf rename addons/addon-ligatures/{test => src/fontLigatures}/index.test.ts (96%) rename addons/addon-ligatures/{test => src/fontLigatures}/merge.test.ts (98%) rename addons/addon-ligatures/{test => src/fontLigatures}/mergeRange.test.ts (94%) rename addons/addon-ligatures/{test => src}/parse.test.ts (97%) diff --git a/addons/addon-ligatures/.gitignore b/addons/addon-ligatures/.gitignore index f172c3a1d..7b48eed8a 100644 --- a/addons/addon-ligatures/.gitignore +++ b/addons/addon-ligatures/.gitignore @@ -3,7 +3,6 @@ node_modules/ coverage/ lib/ -fonts/ .env .vscode/ diff --git a/addons/addon-ligatures/fonts/FiraCode-Regular.otf b/addons/addon-ligatures/fonts/FiraCode-Regular.otf new file mode 100644 index 0000000000000000000000000000000000000000..e7a9fda69dad82c508e6374a053e6d3f1b82f6c0 GIT binary patch literal 159496 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≈zbcMU#SyAFWR(?<3sJ@KsyJSi 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

u>(C*Bpy*W=7J3vC#i*pFYXwG8IZ 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 A!vFvP literal 0 HcmV?d00001 diff --git a/addons/addon-ligatures/fonts/UbuntuMono-Regular.ttf b/addons/addon-ligatures/fonts/UbuntuMono-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..fdd309d716629f4e5339d5e5508225ed857a3ede GIT binary patch literal 205748 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ăESk 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 G(fmJpf?fdt literal 0 HcmV?d00001 diff --git a/addons/addon-ligatures/fonts/iosevka-regular.ttf b/addons/addon-ligatures/fonts/iosevka-regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..963cbe2a654a8e5ab284a622575c57464e8f1b35 GIT binary patch literal 399128 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

PqP!cZsvKW-pW-=)Z;ME?&cXe~f1oBjZq+bIFoz z9!A-E4P|DnKs*7vC&fm=U|1ClhE1}9Eu;4q3kJh(x1jPTHue3KCOSPeHc~ETmaNDrmld(nRiZ#i?X&vy3l;Vx@0=c|01X%tLj^P0TXl~#O9sdF?BuFyZa!B0Mf zwRFYWPoRmX`RJ$=F@Sx#YS|ybVot`j3@S|`w6CaPlM{YS+6nYc4x1a_ z(`-oRFf7u&w{k;s)ogoS&<)b^JGilVV0&%|Yc#DZ>}PAiwcFNy!1psa%_f@mD#iyR zgfWUftZ!wsq5VwX$u?nE!+q~t^Q_&x_RoB*z&Gf=8M@{DpZzQb>*xDdx)?0*xVPcD zYuAXs{&GY9`+bb#0X~jDIDH&poBbjzkefxkiS!~h8PB``_K3$|kEqCB`xx=fubnIU z@q zY16Jly`!y>)yd^bwOU!8+|q2(*gc_WY(e?q|10lPH2%JFtyVU@??Sxq-Pl)OjxX^E z`Bln=+?V)7??$%J``pXFzjq_Q|0D9R*oDg9NqK2Ar|4;5PBIyVyRaFUm=5Mq*jEjk zMuLI`?c{$oY#MrS^LoyrtL5-8*lwwmQ{#R2Rv3)U*hon_*CQgUCBrzU^jW)IXM4TH<=uxa zy6DiZO-nj8$p3wEb)?m5ZV|-5>7Zw4SFhE=l)!f3ekSBBa+8VhPm>8fs2VQ} zRSDb-tL-mF&pY)$$kM!)VP3=vs`@r)*DsNvqrev|>O-l#(lXpZTJ{XQ#D9=zL z*3MBB!RvveC)upb295#~JaH6w5@8mxl1Yim9iYH>A_NC7MPVCe5ylO2BGv}QpbSB| zKL$Kf1yfOtK!in#61P3|{yi4-As4_IOw{u>4@xN5l%~}e8!bdJ$72q!+H_BN-1&l! z;^em@Gd_Siynd(O@36Z)cAWRdyiUJ2cz?p}a7`l!2YJyplz!6tdl~whL!K3veA-KC z0GN@f4xI%MHSOw1mVk?6V8n+jj~mXayBZ9c*9}bl7K~h`$LBB$H}O z@ArSeVM_I9Ls~iwj26#biAdDzkefDd{ODn%Hitc4c{`lCTB^j)^D8Tngckw8L2vwx z5wFL??LTtwambW=P=$i$t?_wVfp}`T9I;p|%9Vs(Y?Ch_r!UWLmTh|h^q}XgiBxeR ziV#xKuqXmilKWLqMI#Un2k`&Gsgc7Y%3LTP!oQ!@-&p@>-5AeHF`jYcQ~i3MGliy= z3W;X2DL})9ea`EKkSnZ&6HuL*6#_z3gI)5)!O#po!FrDPcN#(dTi))$D5g!Lh>xn< zQ}_5{r5M$>;J?6MAK~<$g8fR@4^e&=#6bQGa!GL$AcRqoj=(K|01oX(F&ORV7((2d zFW3uxEYA2???1vB7aTaAV9?_o8*`@Q?ZIHQaLX-m*!jHCH+(DQqm&i2gVy0C7{{ix z%x815QZ}&V&TPmnI zVJ=T3?4=R(h5?(101>-O)z%Fr>ONvci6KO?q0{Fyf&r42VEbGF{*WQYJN>o*Y z4;;K3{UHA#Ut|zPVDdRg5I#sY;UBBuC_&4@ERD~o7f?d_Fd@s*ied{sCdj;)~M zH0TcP3Y&-nVN#V>`T|4LA?TvBM(ZnY(1jaI8wLz)ioldX7O3TF`Ut;*!%m>L+#j2l z@~T=0144>G$9X%T8XDY5|IK3riv|7*KYJe?m1QTmDCBpW%ja_iM?LPbsNDG$-sp7M zkfh~w1i@wozR(+mrr7im_K4||2o>mb3WCSQ%s$?WvdB&{s!SeTln zOPzMUSEDIk8IxrfybJN5%hI%INT5P95UdCc&7jkZ(%pgFjd=f~STi?ae$vtozGjx= zfE1&cEa;77zK7=!+vAmxA0^vCo;nEzX%ilhK0>vPbPrg`7%=6F%e!UZ($fDWM<;e-hw`f99dID4@bpPps2E^@T@NG6-jjA{5Y*8e1T zWUWzeG{Um%$fVO5hb)I1IH2KsHkHacK#P!{5l`Mg?D&&p6Jb9;-6n!M3!;{~O@`3{ z*BkYRtBCnN)jPs$z(+#3FnQT}N61owYm9s7nxD`$bnm)rK8QTv4)8f`(23}t8s76B z#E9B}4@}=T%11bec=LC`#$%UX=*J^2WAS}j`5Nv+ASCX4m}MboeHPze%fA!mnK*wB z|887g0SD)HB0T9N>_m)x{G@It5_LZLGk=fY!!aBA_qPo_!zR6F=zB$hK8TT|42t#7 zgi(k68QG_lWBqH$KBcga8}=!VsYO_;HmTHqF4c3gNoArX9lwp@XbHa!gmV;!YdpZW z&Xco5z=b0Mpv%0L4<2S$^?ocr+c|X&uEQ3|`;O;ns9M&96wRreKZ2ZR3_x%U*hhP! zn61ccbQafX39AFJnIso(=4vPT&0uw;$mKE)SAoibl}J`eXiWgGwrR9u;Wwg>mV=OL zY}Wbt&iuR;pKQV2x4!46iZ$f$WOAtt1TB0Z_H0r7yOulgq%#u#w@-fZ-(pc`GTvF< z)sdIB`%x+0<*~UG=6vM7`yQd+Hjm4S!`qk8c+5GUV?Iu`{hrdsXvD5D-8>m}aX)pH2j-Xd{ibNEh6)-(kYu&oX;UKS(!%b1)Cf|`0bVaZM zWILw+)9l{f!z|tVD7zQI-ksi4_>bSb4aR)gP`s1YfkDSmBikrPA?t4?PczXkuCa^2 zZwD3gLl`;!a!3OC*;9GMKwpD?(|Q!+0QKEG4(zvvzk@=C&ePjLLl{l=1jZFLw2}Kz zX&0Bq^0|zu)>>^hTBA1yfF@amFC;7$+PFBeoK|a1tDddWS|itpCX-S8>Y6_KJqf?6 z`V*ex{ZIab)=;cIw))=Hv3e|IJ-F`cd)d2?E1bxTymus%@RJqL^krRNX@L#KyC@;| zzYq$LI75;REQ?UM5JJgnJPEtb)9tZ=Aw|?mV{ep}(*s;k5i1CxrjKNL^5XE!Pj$G+Kn+4UX1Uk3d9ijX@%S_}5 zf;l(Q`=|MQ$YIAeNc0Mu2KLIoz}|7+U{2{fTjbw)%|wh>RnPacDck$Uzl`G*IgJPD z*?_b@2)BcdvrC4Ki+zo*C*Eql{+GD^+jzd%t8L05oFCL`Yyd~}+O~7DoYBB63_P4T zp9oqwlR&~WbPfqD7_)_y-Xxe8&Y>p>2%)ZDL`g-s24DvxO$O-D=GKv|MOZpfxdlM` zjy{*}FFhUPPWaJ4hS<=^O~};k0B3T@vTc6Y(!fD#l3`WXPCqtU28>91<2lx<5mYH@duD^>^ zb`j;)WW1V~ND|_K?sPT28i}~tGK#5{T66VsB%y^$4y0F(#Nw&s=*;eJcUw0c%oN7Q zwq+xl*B|sYlL-~I)=tV_zYxXNv}mLJZ=?K+ht8Jw<9^;rX3B*i+&uTt9xDML|yBb;Zo1D##a;_hd2u+J-8-++{{>Y4r zzd}k^v@hH*TO8JHNEDT?%U{*~@X3?kf=A2dw^tkihvkb8KY!iK%zeF2o+LUo_MBPi zfP{J#Daa}3q}_0Nk%lZl zt$CxaaCaY`7_T>%asctx0={@;lhPam~ zpL%bmPv9|$&fD1o9avr2`TQ3h5&h)+9C6t;;^(?tM3~Vw?r|B&0-~M{86os~I(3O= zP}I`_<&(}3B7RGI|GIiQuP%AIj(4M;PPfbJ>2zLET(;XMBfa^{SIgzu3xD(@)YCEc z9RBW`@n{rm#g9*e#ynD~ows5Yf(?Kd4A_7W{ydT=#-*)kWv;*Yzx=UtZmnI_eg7xf zw!#lT_>U*}N+Dx+J8)etb5TGMNAo*~#Vb^9t;(h3+HlNyzuJ?8)9N z=e4xA5y}cjc8U=!7;0j|;)vHx0tyU~Of>rAV7Po!OD3TARuZjbiwJI{Y^OoTp4PYE zi7m~>qdBz@-_Uh#I<%utudAZHSa&&phnLc(D9>#F(@9xbUqm}*v;UYOCETc0Z2m~J zIt{w|JjQ4f;hYun!gB%n3AQ!@fad*yGy?(k67+P`vS-uEG>9mb$dkq`f{b~XF*X3A zp!K4QUBF_1SLpkKK{Ys1gfX`ig%QqS_s2IS8-Zpln~l{+mWN6RO$9V9kkqPM>ZM>H zpKWeF*@2PRoAcnORrbIzIrOaDcc8 zrp3QXXIufJe)Uil9Gs&w| zM;%7-ifG)?v4&G4D~AuvH^)0(U$VC8@TOYI>rM5in&wyNpei&?=%A!n=t}@89i&`f zOw2l{p#P^HV7ELVC?DsE_s||fbs)jd$oK++z-r<}sxeGdp_&(fBNQm3^Xm%0@vrDU zFI#6S`qtgJ)*DrR(@VTz&PG6^PvHGj#U5A%ZrFor6-bttnqBZe0rnq_K}(x3F)_wv ztf@&{T}{R<)SekZm|$p8V?W+Nu+)UkXV}!Wr@S#YDx%Ns9506Bijq`gnCx&OTt>C@ zNJ3Fi2tDU;I&9ICUsV7U-P(8Kt*^;kbKT{e@;-<>+TMvY{6hZx>h-TjnRKu9t1QA~ zltDjnOCBL{`^C5ljKMm|i)Ezr^z-wZyUEZhaiqOR3R^#tgJ9_=_;zp&aG>2CheA3#JKr^bIjN4_@zIamF{Y}yOPvmO{q;9yRUG=rM~4p$+DDkwjW)1- zb$uGkfNTad+(bm9>y&A|PJ4g;`^vN+0T4Wt>2$1`$PpP>EC9yBZ_oj7NKby%>WiXQ zp;7n`LjWrXz)n%GKqrZ)&-ztr8`lfrxoA=qf+z+_m%VdN2zUz|d* zL}THQ-%FLd1_EzDCkS$4nY?Wvfw>5mZ@`#2?NF<29}KHewF!F2+VCga6~D%J&Ngv8 z9P~JYb}tgD{i?^|@@03i;OjD1z2TiXRAKx;xBCJ5{mwUBm3dw7XS=dKm))v*5uYDT zxP(1R$TL5M9{Ok8X55!+_}ViaC;ime>%gaN(x-H}kMvlxo-D32^ydmB1hd{te4pEu zWRsqaHO8-lZTMsC=g1Or!ruJpwN>3-PWvm>s^YeEw84uyEY~8=#YUJ2Mw=jO+SsN1 zJM~G}t{&3csJ~Nsynj8_W*xkhvCV1OZWq-IEg3`+EV=m`1_} z?6JkZca85GIK#9=)eE0v81-W4S9jr=z2PiL*I6x-h?FXhPBfAkc?YV=x?L`Jq%hn2 z1X(Ems;CA6YH{Y=cdU+9!cm$h8jr^@9u=jAPaF6F^f95apCc_ESRINyH3`rZXq9jg zS``bTsD*HlB~yn`A9A!8j5Hu$=96()p6A${-M*paxequ+eNK~eTNf{dO+)yT&swVN z>8DjcKsm__Z4@hWoUK^dU7fr2vR#8}Wi%EbVTAP6zAxFsJ}hC(qpr0sCT*Vb)~9 zBFql>%L_&70#=j3dgE8ZU$-|4#VE?<6${OSt)Qa$wCE4*DndGdB6LLCqww31W~fLGtKy)z9IIWgpbo-E zK5WX3$8hm4o->cO!sFZz(P0i6MQ9>lq^Rw@q9j!(TZIZUG{}gR>>Emh4fX6QV#n4!SeFuy(ighrx8VsV^_4% z={*TL;{FO+Q>Z|=;hNH~As=5wxszW*Xl3jDZ(LqO{8vSRJrrw`P^^Zo!R#2ipj)c9 z2^)i903ZS(O&5`v9FaJ~%@lpz22>WE0p2+Oi~2MV0QLBRlPBS3xQ3usP8w~)_@3s| zW83(;;Qiz0;p4;goQKeN;vt}=+}cg-X?*v?(g00@=v`9IC4H0xYb*ma5Xenf7o3a` zkP+Y+tkC!QDu6CJHyd1Gc?39+6gmfGEMU(iT@!3;y4(A4`g`x6y62lyKSZH#_zAyx z&pqFi_xJwlg??LvAC1}q6bk(KHrfLJVY}ra(-$M5TDB1j*--8+kVsX^$wV*|_9s&1 zg@qg-9#j>^ec-02<^9%hq}p6sY*r)5Xrv6(5H|{Ors9ophhkG>O<>qZ|nN$WT<;4zxW;@tRG;3}FG!5isA3vfRZwc>KDqOkAS( z^unG%$mNSF*6E~t=v1E7cf1#1d+F$@4;i%1*O*v`r>!yl8X>!2J)^CDy{*V=ZkJ$d z(a9R;mM-)!>phTWI@8qL=$GyMEO_7Z+WA-fG^Eo1C$`n=;nOct#TD)L6=JpG|EPgI` z&%s}#?ZlnYe0runE`ABVO$y<+AE3Nq;;ck{rja3B0P#o2nEkP#F%siLuP}`eDOumd zm_+d+l4Yp+HLB6#2q+r?tT-%~IJh^k4ImVANhXJQKU@pPb0rpZ7p=+IYr4HFx}mXRIg)2AA1N2dLLn^>^1=NO@r43fh`Z*W?R_V#)#lGT zZ@#95J1$qOy1H780TN2Yt`Oe{LdL^PVdf}4@k)##8VUvf5@b~9%p#o(WAvmzJg<)* zjUI7#)9~qh9Ti@?S8X0`#xt3Cv)ppIuDYr;Lw2#5(p6x0-$}$qrthC_#sPEE$)roy zmQxwL+0duB438KV6fZ~o8hM5gNQZD16i(Q9{~2jy(|T#HAsK#X_Yq-jCIM=A-vOtF z_)0Z+qhYJLvgihq`=)+TV#x6Ie9cr*^zP1G>*aPX=A$(4L=6>(N%kP#Mtf}*SynfQ zdNq0QFJ`kKSkG=3#S9X94RDzv%CO5)#ZTiJ7;{L@HTGiC-9&XPIPd6MRQ$~6nAE7{ zCi9aK)YSysfzx4^vMh^a`+hM55)&|o+;Be27Xn4t2%8@dtsy}Zhu~w`S_3G3c(L1E z3~ zo5F2k9I#M74BCrHC8|6Cj)^D(ii}0g3`{oEG_nDJV-VxOQlaX@>14c=C{Yd1U~tSq z%a~lQ+>yh5uV8!V7G{!fw}jS2rvK@4qe)Gz##5CwjIy;JpA9J}w*%=BvyvD;ZVl`LFVNT#q4(43Kv1Tn4sIiz(nF72eY=Xhi+ z>W@R6*RAR)ZdDgnN6g9O1d~>lCU#ElEElD5Hg4mZrZBrgr6%ZJF%jsF&?iBUSWaSz ztvAN%@elU2%>8xT7+Z-Jqpgb%CJ!C01mJnFgj1Q3g^@Zyp5o^%a+`bDKbbE~`Bj#uSgI6H*6)Lr@QuHfd8loeIYVuEY8evbl=T(C`Mb zd1}3E4$<6CVWikZgoesx^I_54X3wRa<3cpgfBXH)_&wjuy!X44y;+jX*&Dz0$J7s+ z<1g|#CYy{Qm#b%!%TvErE{}3lp*fPvt3{{^MLWsmd*j{sUXsiC2L=eV=kcfaAHDye z-)M`F$I0)DI8L-h$Q`OQL$=LUsThML*APEbX7O`Inn4_00s<(n%MHk9#SZELp`h4u zSOdk7913`7We#MwB10SnMD!dg#TgN&ftteLwP%rubyGP(`fMN*e;hLLX!||yp@29a zOPqp^ZW_0NOiT<6g@s?bOsuz^J;@(Q&(z0dy-W;__M6MZ{kPC488Wd7{V#2jiAkR0 zGBH-zkW8Egwm+`*nPp;4mx+C-m^GxhLm>Pr2ZYy9NU^f|pA|26QvJz31v zZm4C89lOIH+Y>`38eWMtCiEv2pH#GHoh*#jAna^d7EUL%kVUn?$a1R#Rqa}UGmqy7X_x|BT0TDluP_8iXhkuCD zOGw4ZYJZ3}ghRRND=T?5+yTt()@U$9bY{f#wKyNd)}XJ@!!i zg2l!2wt<|>#VhQ(G+~ggsOvOsBuzmw97>+TF_Lr3b83{t z+^9NNE+3hRjCvw!X;fep7wl@Kk>nP7ePo3WPec$``a`%0cbS+&;mejhE ztm{RVu54}CMuA`hCJL`#NW|nFPq2^fqn+>UFZ(zagZj?6+kxy~bJ|UxuE2vyekZ!CZ5Z*s2=qrm zeiGWDnh4=Og?kr`EyJ@O)PwMNiXLb&O-pADt(xbWJ`d^8VWM~t(hOO|VWS#}kfqal zEv3vQGks9-LK%K2B{UoP0xsuw=4&|=_(6-g z{VEH8FU~cJeAx7f@xJ~j0IAC#HPoz>OT{$O54+AaWHl9rb75yxQ2fd`vs+)l`Gz83 z9NP;Xa4Zq>z!u2KkFgwcC4VQ|9o2(&lBEm3cfLQ&-@E*-pu5rsMf{-I&j$r|2${_4 zgKE|{@IfUgZ^ke{^!T8l=)?aM^9SwmuTS6(n@Bnx!EG?N&W0WFU_ABD6G3k( ze8Ee^HD3|IP;vMTLq*(|_LcU3JvNYn@5!5b4c{*-{Z3KXn<^XmotREp{Z5rqAHNfb z5RlY5WmMHUh;l5Nl=@AvP|)x{z=Xau*Mm=JLc^&vGeKv*+M}iMlNW$B1?6YC{R1fMG4egm#%7`gBq+`{$$CM~*cio^ zFTH#9-K*Kr)zR!Dxsj3F%9q4f+tXiKS-JA}+fRu>@zyI>#LI8K|NhtF9V%Zdy2(89 zzsWpEsq30YVrq3N@u}>{NOtb+;!yijZ=ai6IP%03M;7LU=gJ#yz@gDipO20mg#7?( zMtG!f+yd5MhY9ZWG*P<17G@wfO8St&7kW$7#!esd{disZk=Vk34IZBgyg$n6^Zc1F zykTwb7V*AY=GInM@~in3@!@CBy+nLiEVjSgUK6L5()C>_J{fN7hPK`cZ-qZ zd+LL;Yj1w@vBi3Q=gIbBYR)ABzT$DQcD(4r-|Y|M?+#pKtm_`_Wy>MJ#bJSqG_!IZ z%n4YuiJo0W)cxTqzLx7t5#wf`@<%sr#+T;87afw3`*1AX^8Vco|?ygsk*3|$BfK^a=V@(&{3I*rOISd66b z0SLQ}IYre6x*YuQiaoyA?b%;lYwf?|4&~49@$)}LBm{7kbBn%^xnaXDI5&gZ)7(rW zXNu?6ZQWd5+qrtj9c0(+*g2p0JJ7)ixPo&e6R;iP0(yevEQl8SJ8($l9V+tzE{vF^)_M7LxFY_AS zpA5J$fu8s0E$`23`E(+ZGT#r~9AuMJQOgXBIqb{VhB1rxkNdBPG)`0s^C|4+?CU2M zYmF!M=#O8snh3@UyRMv_IeC$>mo%4W0gI3}xjbe%hMe%UI$dWd#4pP+uM!l!E>?~Y zA=eN6lMo+V{QFh-pcjEaC)*EV-Cq)0$iqrFBi}8?t^PZpmOpiYG z6x;QH2eZHf8PoO$y#Mk$*)%_5&Vl4cAKAV;+l?SvDIuX#KPNj@$>|YTp?8r&#IZmM zj5_zQ1%_nT>_0A8!3!Ukqt9=laY?2I;Ap;1`ou7lantYyFPCGmns*HMgtSGD#DLj3 zogkk`Dq^Jfm{AKM5&7nkovRD@v%2%hlcMzC%{PsXo_*;{Uh>kjqoX(7{9yaPcD=X3 zGjQoYz^N+88Vm(hgF(sl4IA_Y$R6l(e$|MZUFA+{&wqeO4&eD-7_-9f8-sUiciRPt>~_U-t$yUUE>K(Py!z<23hXzR%pPXLMW}&oxJy z8_b=;mDW$46izwwxh-4fRtKY=Lx()k!By)wpZm}qcQ|r~j~qFib2ut{i-8+&3>5cP z5KYn37K>QV8S#zrj^KNz;%(I3G7 z^#LF~=X}HWTaLp9@9#FV_kVwnv7IOY@Xa_cInu7<$btvMr^sDIM!*?Ng~)^x&>(9p z6hxCZQ=k=T^$Z5Yzd#Qo1864%27bTNdskIDeA4YfFv)*8b1mh;cS2$8w%>X= zN5k-W<@r$l$OY$Pl@&Icj|rEJ=kw6(ZjpKU(hcFT9X`UIhz@KL+P(u1mFUhGpaF2f zi0JBguBb>Yss zSMwi^HR_DTa$d2)`UI6-m5jy-agJb4)5Wy_&eJa zAGLg^y*Oj}j=j*aBCJC_-`NZF>w$?Hvy-zTpJg9R7d-D@_pJ2OSm0@UjNjXV*E=?b z7hwD-;N=v*usmbI8;YIonXBZ}gv(Eu&wL9ugQiY*_yX59c!uuzm3>h3%kbCAIa=TM zuJ!MueFwZv*x+0cT-kred-`5ppW0KK1y8vTo^7CC_nuxS&ksez^Bxf#NUpQs8p}yr z%{59tGfpPepv8TDpTT{84gC=uET{g+Ut)NR{Vs&~5XAjO!6d{A+zK2w^kQ_`c7S6I zw}^vb1OSBN;W*^i5NwDpl=|(mAwp0=@Q`6Ybbj6W<=l=PIn!!asMiZ~e|^ITKL{#k zd2bAs;9bi*kRM0I)(}PDrV>YP&1rW#uOlxV#k*`HPpjhuYkFHKYC92^rcBaTV_wvy zVPfpR=6tpdF(VTv_-|4kb zN3DOmedhtx9B{d`Q=S0y&>h;rD~|~6s^dpaU3u!#!#ggTpPQZ>9jTV-Vp_hoa*x;( zLZwWUL}t0j%slM6;NG->AkiAJ-IOOoPGq`-oSkkQrV7iFwBng#P(dfSE2;a3YqO#F zpG9%GjxV|z3ukl48AAM;KU=G2vf*SZzJEIE4dQ0u)p#hAtJX4pkJpzyikvoIFzyY6 zywTU;zbm6&cQIF){>Ygo?v0E#u3Xs9+w4H$HZ`)!!SS)DTA{G9a9%|MH3XNg|EHMWcyr0oihNY62$q*xP zQ)R7303~)66yV7ua}&OhpM&scX?XbiBjr4>J%|XdawHs&R))5uLva*Mx#Nyv0vE0N zytp>>jvH=xT}vE}M)K8(!CWR}cSef&!SQM-o)U|p_`!JSozMEck?34B;`5JDVP~S7 z6jL%or)_*p)#Wr2s&&2udM{_sF8Q*^vKu~-uBrxF5`gF+1rr$1e?0Xfp@IyQX%WWpO4 zh<5$c<%w~k7UL7kM~>9D#`F33*7^}Wn#)b^JZV3<18mN3=xYP)w^+ zR0L8!Wbs-n%21?+TcV7nQ5l3vDM>^m3RLkZRGHgsUIt$DpWV$jKihh`?*!i|+?;3Vt6a(n=p!PU8IbDuKl_yR;doLM6>3C1(}Sa0r=2CAwr`Z#;!y% z(M7Y2<7Le{`SpRxMk+222T&Vpe5OLNaNcBjcFUHre9Vh9>R4f9ID@Mag*Y6G*Tyd1 zH#rcA2E%2KHyj*@fY;P#-T7pr*7Rl*D83c*MY0+Q)cq|D5;c?m< zm4QSymnanmE16u(VRyO{h1|e!xq>3~x_01vgJTFh(B`);FHjUY2lIfcP`)SGskab; zah#&saJwA`ak&{TIrA2L1$E&(?o7s}D0_&Dl7xG%|NYzB#L2sF`%s@tfTeAt)#pAt z?4!&tBwsFYgwuuCk;C;pFhANhav5UVY?=|GU9c4-WsucpnhQnU*dt0Mm2uer=yXok z8)=Hf&ZGEdG8K%y^}8tUHRQG>l7qFOP-!X~2xQ8W!qsX&yk~MSN#znCW00)Fv8d$# z#CN~(y`e)M6fWP8_!F%LSqpV3Yi#bo8l+4bg){*hR|Hp^*>Ir)mSKqyghW9hT3eK< zh9d+Sru`xWPo6$~auw-8S1n(qLpX@GeB;%!NEy_jMyNbhNE;m6-ubj)nZ-MIO`viR=<~!b z`LV_I6%~Qbl*i;pY?37=3jC&0#$82_w8%ZkIgBwslu=!Q(WAtd36N>g98_fr1=;MaXN){cu7=Elc{2T}A0rGC-~P>SWfeAhw{TUiIW z>-NhHpv4faUV!LE!UksnC_zmef;1KNhC<$m5E1VmN!cp2p^!#D#6z>STGsD@(6$WO zNq9-fiVU@XR*#TC#j;duW9f{3Y5#J%k}FDa%*5>|vX*yhOUqjZd^*D9NchINbeyFC zg>4px9ngGilq%MZ?i|}$ETogj-W(AlROXmbeO%0fCs5jeG|m*|-Rt}X26T1%q%3}7 z!5(HW&!>@`LzhO_Y?W9FB^Jjkm2uQpOvNfT8?{fh#tKOv{v-=y<|m{e#*%wZT%6SP zM5Q@@VzKF1oGPZr@gWw7Fu$CINJ+87ISqAD zs3IrbUk!>ubwqYDRf74cA+N5E8KG|2N*O9)a=wBvOY@+bg(}4(JY%X1&)C>S6xYx7 zcgE-}GEG60R*`5Fz|8mB z?r9<(3Lt=&>f@*rV^~#^~Z;r7F-geHCFQk!_abw1jf zoR0xZj?a&IT@(RC*HK`fZMTrL!`zouC-jG-Bo>dysKWe=3<*i$c1~Fj#Wv7%z4gFq zdfWWA^y-0yr6rNP=}m7s^@VSI;|r(W^roBIKl%RmyZA1+B=!ebt?#FqP}U&Bf^VrM zRSm6Rz|lyQ8StFIO(F?VNi2zS`-ixc@rR<^61St|)-9wbN!waf8`E|#FS95sR*)E! z18(dGftL}VMuiSTHZrh7Xaki}v5=?g5Zag+Bav2O1~?h}O5{;eGA-R`l5k1MU#iv5 zwI&ABPWuX_WMm3slO>i|6UB)eA33jy(~m?~rs^<&#Ol-F*go8T<frGmTPgPTrkPtpFTx4+6SfvqLFxFC|S>3l1T?re$?fUmZnB? zsI+4{09CCyvG)b^apxgz*BaG&I^aDSyBe3K#KVX{0dvD8PLP%(!A3IoLi(8a1b;O} zk=HULJQDtjo$MM#Iv?~V1DR0dX(~U;*CWV$Yn*uq;(&?VOwRlw%9*ILbPOOYjgREh zUiTJ+%R~^Qjlk>(!rT|#-gJIs-09e{t(47{mRp%{IuLr%%XW?y5S|?>jO@DpOel~J zXIi#uG@Xu8-9OUjST?qOi1$~?y=CGl2*)9rmkt}p$FK^lm9=szWhYu{B&W&fMU$;S zsp6eNT1>JLc&dS8rUk-)VnybZ96lhG*^+ z7q|bc+e@GBA) zA|5GJMvos)Km-dUjvtTY+&&?Eo-87rhILysl&_9Ab7^!8=L;iK1I0*8Oa@{rv0(eI zP$*U`#Y5O)n|3MomDWS{!37QVFQt7D{>%-`gIJj|53UiN(I9-=;Iyl zXBymmjbs5j9SXhhm9DIa;n(dD`(My%$9E`PqV0z;_VO7eW#`{$&D0`Y^uW)A# zQF@hWv>;u$*qir{R>}#A5loaTxb79U6(0fKhx)6R&~C(|17o}2xofP3(#CF2tT6G; ziDJ~_A$s!L#<}v5O#-;yWw$bLne0}K${o9vF9QrhJxu}`{`+k)^z_F*_B6eVQI^h) zU|fA0)~d#)+ma|UjZ4K~Y!u!FhV~sBv11fJ1|v-5v>aIs^T_AU$+3@%ns|?G8r~8} zl(DGPaAYZJvwYP_X>BFB;S^b0$HzuT>d?B81~e;XUB38;aqrvQc2wIln2L)@8?w2{ zAWwx*m0*tfNS6EjLbDWsepf9u7Zw}EsNarg3{MDl%vgxjzEBJ)r6IRx<-Ta7SVtAi zNTEFZZ^KK-Hi?wtX{6qSU|#kV691McdSI+af$l@Cca4+_(L@JsMC))n8Z53xrWMI{ z$7FR=#`~VVprjo#S`8#luim@5dM|CoH@n*OC<|y2HO$5RSE?Xc^i&1$Uu)DmOydqawxCu8g zpUjmBir+c1r=AqKi7h+-pUd|Ab)op5_ltMCE+|DbDs*E&2{{9#`J-r=eAVf@ zakK3z|DWG;Lp*WW{;!@pC*BcHT)O|OmbLLa!l@--;#JJG5E{^Y>*lpNpp#HdfmZan|d9=0*C6c}1Wroto?Og@y z$Y#MvR#EmT&18+1zwweQLuK4poJ^PQxFd?7YlL1$@3^CsPKF}s@=&D%gi0JV573BL zi1^W2K&>9*EQMhir`%Fox6J}4L7l2XE{38iT6nnPlqZXA7Lu!*Mhin?x$Z+_sO6tP zxz=d0GIr^u(L5EV_j~eD$Qef^5a4#WFAUl(2#_kmwev{;V@WuQwAN2hABsHy5t# z%=F5xnL6#&^6K`S+b=I+=ZG&l|T9dME5KZi7+xYOl)lhx39$MWu+H7K_{@1+m zjcb4T*kgaW_Qp59rv39TA}-1}gZJuxtPR0CedmFP8i*S6rIOHn8b~tIS90bzJH8o3 zrOSX9m76iknn$ZvLOX{|^oEaGw+Vte`;>cpX zR?PE_s6%4N(>c>@1L(O8Rmm!dNt=T_$YcvfYZRa%T>I`wJ`=>fjDcWreBU--d~7i3 zw;y!4BBjP6a*#uT)ljM!D#pUmZQIh2*}YLwiv-izYB`yWW;5`#C4He($X|>mQ{il? z)NYR_0@0S=Q%WS$@WgpzQjP`vHQ;vBvJWM71TwP0X_4njKr90y5GEbAvk=$CDV!8> z6=_BqZkARs>U7~BaZ?xBNX~S1Ghj3`MKGdk(SwN3(7Ip*wChw%d5dP;(aA!Ws_6e#>{5-gxJDBKgL;KLQj zimlwTYC+a3x2VG9fE5>LKf|@>wNsFA4Y=hyKLK28#_v?uoN+2acVpJ7>9Nchi(I%F zQ;NDI*n;aBiQRJx3VVQJ(l^gGR5*N51n39##R%2KiY$`A99~@wr?YM(=;I2|QX*aP zk*?>fh+3sExOK(y|--XEhX z(50w;anzOT^tz+!)FZg#}(@Yu2V0pbuLhS8w{;jy^Zj|+n z2XIZZUkFb*fj0zws0DO5;7eqx0~u_dEnO*(`mvjf4VTm7um|v4JYA`#@ROe2 z6Z40>PPfPDI*U7A;l&a)+(4Tw-oDj`+lFHvWGE%#ek9aFhzw>ExmqKaE88amkgDA& z#K`25xRI1RIC8x0g*SVtDZ3=4J4NuXhwG(S$~IoyVfm^AkV9VYjiGAg@W#3#Rl+Q&MJX} zYh;j&Mdh-Hph0mpiZYRZ6zN+D6Uhcl#9KDeENvj3r~%8SblVd^G86GyCX$(GXP!Oq zL@PW(@E_vn0R(`D`~o#$9XyOBFA}myXO*KZm)%$`?xvbRi^rOa;`t30v}j#4 zFN;?Q9hO#farc5O3kEXB@`4sfI^q$W=?Ad3Efn{Y&tb>>2>73$Lu~|(`=MIhB!j4} zQ?(Iz>!v41h6WpzQYNKs5nDQ2XV|6*9UJm99Z#})5S;_p#07E2CzF5vYWHX!L4>}n z+v9Mgd>&MA9NfP3V06F3ogAHvy4|sf@$tE&*OOeB$+?{S;*CL3%S6(td@7s>l1V>+ z`icnd38zMzMXx&*b0^)gR??gECi6~DxlR!&(w;#4f!A9q0yD?KAn-W;&k-ib2BPZQ zT+0t)0vQ-gj; znS~^ZYR`?2PsH5r=;Ub9?bshZxOMv=s#kh^DTl-3&icH8V1888(#d++f?M?UWC^{~Cy$2Ob)N=rw0Pq@f}iU0QPnbz_W# zPzYfgJiI*9dCsAMmZQ>nzvsJNxwf`kzx=YD%M=T{cI>h1y-%yO2?zMlsZ&&+dpO5^;pqhO@QW zTqK1;rXp1sm`ConE9SyQ&Zy8|B7ajW>4&C-Oq)Q&pA>;aa3lz#ln5r10k-ey+HKmc z;??>C@Rk;|YY#k>!k$&hyvIIOY>*06Tk8a>UE_dKQPPmJ=x{o&>D$>3eIn5(nvHEn zY{rA3)vki-pR_jNtzXh%TZ72e#z;rt{N;Ci~6E3sYa zA6KTmZhvHAd1>m3Mse5BR6IeU|9Ce%46J)hXs4F_bgb~=VO$xrU&=jca5Y?HK<;qx z?yFEX9(6KE5zZ|`bXGsnslaHKpVyY*k$^dAB2kME*CM!&Oc@Cc({P99Avso&5hF2( zg~Z75Rb0P_YtKSnywT6ZxpO@crp7|MZGSU+Y^Nl=ZJb-(c-T5 ze+XC3XNUXHr9OId?dN#Cr?vY8>8MTZ)TdFZ3l~$#h0^`YQa}dMkS2iM@+!L%gf3+u z5`GZEehNertL^Qwwj9J0aI@ATiQ$L~HvNUUB=L2NIsosbRlPEzz7wcrN|g_GYS6@Y}Y?}=BDXc7t+2A94!Qif3muT}OhtgFX{cjo__ z4UcxIBVbB(#^BRdAw+)e$e89QgGpN?5c#nqXqplyaPb7m-B<(1d#{VXGnf<^*_kO@ z;=N5Rr7Ql<@aZsv0*mTkoU&2=$9xkW2irlNJPx^ZG%QuIa5SBZhmb&V(B_X8oAWcM zT*aoZazoTmAaW5^vK1 zA)FK>j!B0lzJQM)fJzEymKU$`gp3U?gRILJm&-gI#>vy#{k(S48(y}Ii@qHia#x@w z$Rk$(_mqWKKsXRf%odANtb(85>wGZ?vsi=guJ5z99M3@7kqSE0lN7E#bHgyVppF?> znbegys4%h6S7SO&3VBCYVkLdWL}8^c-LLC(@Jw9^>eJ)jFtOL7FA41i;PL06FBQoW zok{b(3ZNfkdxg@8K!07wx=7eC>^A~Y(-hyr)smeWV%ntM2mS(E6nuBG#8|f(Ml3eA zM$$WXu3md>CV@VYbH-uYqk6{4%GV z;8ia`>By3K$lX7zH*mA%r2$G?t`{Rx-YU0BT6);j_5591$F3t)jP_CN{Cf zTwxP?K1fsh0n@kDE}930lxbRr(zWj*fHRLGy6^z1R^Kc!$3I%d&3j@c1N~%wZkfJP z7#x2!Z9ZV^;90j&%p#To5ez}CzxpL;VCagyF|9H-hgAty!m9Xz>`^$EL)T zMW2BKN^OH-u_ei#KC%wVcxJ?={Bgt|M(k=2$6D=WYoZ3`Rh$ywRR+^iAH6x@Zr|0- z-soJDyg+*x)DS-d%Z}j^x{6EhNGBJ{5+`FUD;SZcGEU9)%^O!&R#suG{_%D7`W2t~ z%oX+ebwBQm`N!IThCJ{Twp<{BOhceB@ph7tjBp}p14sBuBsU^`XncJGFcsn`X#vs} z#8M=Ji6E6?zfTX<1AdpY77h#-3d4bL&FS(7>O;$0P(eAm1&+|93uz8Jb`9Z{;7qbG zv}*^xx{|F56|t;zXbF5&6>XPBmF@c8m&{ITrWRyP(Df5UV7D*LOihk=?n_1xz(SvE z?TAz)O^c_Iy#ekKx8Zw+GIk3if>m=qbrAQIZoQUS#TH=b^Nb8}uq{k`O2O-eeRnF4$<9oP0uB zn-QkyN_^p}czkC%xg~S)>h1U6^E(ezbEvI(?UncJBRsfA$q|6_n6{Uq?@~BnOKC`8 zmV1)PFQ_uFaG=PMHokXaZ!Uu*Avn=Ghrda&00cpkugNQtQ%(tN#fRCT3_*jhkdudp zDic8snQfF}F;YB2zJL&+V75HAbAM~86oNvLu3lU#1fiD1Vx`6`3Z+895)h0gLV=J6 zy}e$tG*eN^$)(y{)E7L`gfAHLC*rt~4>zC`Q`KlZ=*6jaMDM@b z;f*)uR@N>rU%YQiJ?j1NR4L{6`2xAIy{mJLxYu#_fAK}?JN8BG^F%KgZyvzehCjcQ z41m>|B!m6=d`|H5Ksuc!K?I0nxj&I)ScGd>E>EPeuhY{@OVg`EmCDen`RRWD!08)r zJT(vq#4g#fA{EW)-0x%|+(=eC%WB>FE^u(fVU&y);#7(pg^iL4Ts9FCF^`wx zQ-F$6UE@1>$%JYHZ*?KoVt>^tTXXgDpKZExlCZ@@}AFX!#{toZQ zFY9?fK&{ex=&oo(lH8Q{zbq6gLY?^@EZ5OyPz*+VV{9~m=8OE+%3?LG+eZ?^2%AmbRabnNLuUUd8wdwsY+69 z8n3Q)1bXw+FU3n{lxciMDyEV9zZr|d(N;)AWB5>j$1Tb}+9O!U2XJ;S!~+O1g6_wqsMeu+>n<^*$ByQ8~v-1G^FHumyk1Vy_c95v) zuFlS`&d|0TCm`rwbO0A2BexC4e|l(bd0=W1<+rtCS03EAyd4~Pdh+E{FCVBRCsL#E zI3+|PfRqRaM0_LXT~c5oYbi3CN~Wy{vx^wZHr-a8fYz1*y;j-h#?#di_T@E9TSQSX z|A!nPBj@CmM#5Ihg3o~4z&t8HjpQo>nV1KbhGb=GrktV#;7BYtIITx3dHVy~kj9lv z)<>6{CATveh)m!b(%lo)C?wKYqB(QPp_%4jG??)?13_On73RKoh5X6Xj&vdr3~X zf$#7*2UQ=f>s{a_v*&l}TQSZS?U44`WnZHXFBwWjF$PnO@vVzG6zs{^)2I3ZxW?tMZK!`-%Z_s*?{wjIiiG=_)59#7W1SqYkn z)Ig<(DPvts>@?Ai z&1Nnc@aTtZp=51*_pXV0A_>=fZcF>)xTh%;y=t)r%=CFaE4Y$=qTpHpslmTg?tNWPf@f%CcyWGs_sH(T z$Vk|Q^ezXxS1!UXn(%}bm*8Y@AjErgLk|XQ1!W^k*?^&}^*)AlpGqZNuZ|6ut7%=V zN}&C9p1gSE)5;f9xB`JPBPHNaa~Wmdk!`f~*zsdqhZ6Wah^EnV|2&ZL}|bM_?3HWDAdfdcl2wb%R=zy&%8V#?+6fqfiewMds67@*h~8DaK^*YGSbk&&^_ z6O4=t^B3=$KfG`_2ZO*!FET20>;a#IWrCbdxaSLKXhFo^5R~cE&~$682t$zwFed({ zKs~r+craCHfjVYu=YBlGxVRB`LDQD_yi-2#rJt7Nqa|>2qz!H}WyYSPoi?D@(}@7+ zNhTY~5mbliI>icbhKZ0U0E9L9jT&aUUSeF z4uo7V(Uj|Ri}g~}>o{Z!Cz{z9S!=wLh26fK zNTo~pWIUPOfxB(^f{{oZdEZHRUZS}X6sM1cCgvxCv7FOmv!$0po)mm#-k`^c>RVW> zjB6j$22p8a`|>uZ`^b+Z%WgJ9DHLAZnI|h3o~58@nTHI6*6l$hL@1sGAITu z1}y~&+Hh1`r@t0xzZX)_`IueEpsbGL#uC{gRgpp(5pB+ULN9^5l#MrQjly!MHf6;0 zn5v14-;r^F6z4KGTLU9@hT$nJZBMzJ0ry^SB3TM<_g?vCcDRs7jl4gxnEX2q`Gs;y zCFJ*RDU6wO|Hxf2%XFI)&-w@V7zTlEpio{{sFnqz})v&Tug;)i;pv<=}8pt7lDI7?n+D!S z3XNKy-F_8YEp;SGvFnzO0NI>ms0XhPL<>K{dP5ohpk~%C%W2E6WQ7uV61|{8u*ZOH zO~5IRQ^R$atFAnTg9npjIN8KeODsYEcx6iR2+*J8$JHIsrGwF-%ozm^wydDL7or8 zPMV`E805zuMM8;7k(W;w5l_J2h^w05VOJWY91;wk02F&d) z#BqCEWO~oJ5VY-f<>uTzuNPr|$xNw|LCwXWFPf(whL00F!l0-fT${Yq@C6A@cm2}J(wEah`{l4nfOlBN$Y`Wj>h*aEe+S7Mv?-jSh ze;?6Ga26YAg79s^MQq>+?O-yJ4BP4CJFzsBD4~T&xm?`7w}Ju}mp$ll`6Jc)L_C-& zY(ZwwxpM_t6XOi$I|2_?z9aLR3*|eCSGB)w;M{SZvPet1G}MrNku^HQ@dK6^A-?M{uq4hjW;hh$6-2zI`0~)Hj%`Z`jvpsTd*e z3ij|gXt|6TK;=3z3_{o$hQzZU*&>M3)^6d9(5nvMxPq*csB(fY_L$P9vWZ%9xJDss z7Q${t;O53b0JwCcCupsI5WxK(>Iz!hECPV|f&ead;?}&_@J`$sDP83I<@3EhZjFC1 zf@@f{>F#32t$~*%jkq<|U7&dy5Mp0(>G6&g?rq;C!iXX&-b>aj z)@=a040|Ntsi$a65)nY@nebB*3W+U7P&Z}ojt)2K#k z2q+Z#F68zh{$3VkB6e#?*}!<`DM)jW#;Gteh5)XoMjFkL#t1aw{Kx`x8Z2)CizBg` zg9S*>lxmcrp}PPI($C3|i;_8pe?f|;hGpVcd#ap-yx;Clk4?qhzR2R1QnnoM=c37c zeOt2;?f=V21xz>)mOQPvc&O<1q+=c*>LH70XrYu(B*VzcN+lcpfABjE1}L=O(;gJB zwC&dl+Qm{_Nh)VJ#9AN%t0)`-7zZSAige1iH=CuSyr2~-xr&TdNEsB170Lxpxgf(L z>leSL*TO!R3p!P0f9KOb52VGr=b&Q7M2o+*U&`NT|8BtkQ4HUZws(2=Kvl%w71CXd)IU4_=ZaI|_6m z)`VCaG^A(RD_Q~WVIsXy6q&N7itFP0V&SqG5p4Ia3-h}k{Ew_bq(@xIy8i=o*PN** zI)&Sr5}I45%MU4VE+kuAs>RR~OH{=iA|iTSM;6Pg4){E(Sl0t!STnj2 zF@zaqZMl@s2@REUnt94cAm!Kya@4XSYfUXARf3CdK**-+GynucEu_NS$e?5fe^ZA1;amfwj#%O4^OAg4^aTof?PG+D+w&S;;N869P88~CT~71&QG0JN3Z zL^5oW%uE4-2Ob)R2wgGy*>N@Eh8;)H@xlYXfU(wKM!?n#4WjnWKn{7{6b(ujv3Amz zr45}lE;`^u(ag{a&@_rsOMqB|fiMz1kEkYiZhYbK*XceF3I@yY@`T5yzivG2!%w9F zQKO=Rvz3WTEYNzfCozjU&oauqGMn(cxD|-ur&+qUmvD;sn}nsA*ABp0>}C`Lx<0R6 zYQzT7BK%jCUpC8Y|H)4}S?xdhNhhaWzMt!zK?)Ec6lR#J5sQl3#t`|#fv8Q$+#ed!yYRO z93X5n7?ZJ?Al);DY()`OR6~o(AR!db49~R66&|unfS&^$J9Ptj4`kgy=}$H429lk9 zZh;CExMmWkzx6FKi~ok9x033rWUmLbBvoYy2;B{RU_W+~3(oL?;0ziFiyg@-iF$+y zi&zEyO}&7XA09qDk@mc6VC0Mc`y}+G~^QP|y!1h6saoOz1 zn4)9HRsir!luD-^h8dt<9jap@G3JV}QJa(KXd6ja5+r0qIMp4ubYlK>r`vOiXO?%^ z^R-+FF&?-UPb6chfh)bk^D#G)xqOj8^oPD`fhX*CPr>Z&jmEQ?x-VFHiN_IgI^C#o z4fvff^owE2C(Ix@m<^8lN|AMpo}!C@Cl=p5>px%$@6iNdYZuoELZ$X?Y?&_WzfDi<^i_bM;J`5Xu1OlhssJhDmf}NsCq2 zFz^xITOit$N|GdLh$u)ZsAS8@p`m0sTj_lAdjj5!{hjuVH{iKQ{)J0B(FRf^ zbu3DT+GwnpIaYH4*LAg@zJ1TB59(XqJ$)NJfq5;Qe?&izx7K)G8DRB3;FBhf(A-Yo zgLWtq(NX%4CS+@Z;HTbk>VsdnU0=AZ^+&w6V;JLo)-gZ@_u)acGz=jQMWSj9(Odv) zXg{#$_Ah);UzqwM%){3Hu)gQ~Sz8IWhlHWvU(a)SQ3FQgN;yY%eAu{WXT}paidzLL zL?g@?Nx)P|6vvWcad6uNqyZeuqx3g~GcXpLX2bOkLff)1JvlZ?cTAKDk!{g!LBH3d zxrN)0P#T8vG+TrdaojpGJ5VhM7qm2KHA3X+Kou1R=3ySfe@R?iSP+no=2EE23-gh) z{f_~k)6+r`_HEfv6nY{A@`^t&R>~!Up+vqU_St;FF%b^ulcAt~L#Uy<+`;5p8C9?z z3`cNxYuF#Rv->L!Z_Js3*NBca_UA9ezn?!1xNOt*GfcLRjpnit(`RQ~ z5E~%>$+B(`-2n%?fM5#xNEpS*Vw&SHK5o;tHiyQl5f^SGFtBQR77UtEzuOhAve96Sw2~MSdqA{Y`l#^wlkotepo4)pu51^hy2~34#ZgAV z>raS(F9#Ynw-51awF2z&crxk_KvMVngXvltfkmF6Kj4SXV)Nj+iqA)D*V}&qoH%2< zK`U$1+Qq#3Eg_IIu@aBzUA{7OK5DwmbnS4pLds=jx;hQTvMkC}+MTgiY2QQvu%iS3 z(qBv|g$#d9sK2DNI2i`d^MnxupC|-TIwcZw=d+CjtD@$GdUcodlGqb*@$bV#j90sqZQgc$RLl_0(P4lOV32yHRsepbM2sS$&4SacUrrx z{Sn~xwZQ9nP@ALLjlBA^SLAaxm%AR&ZLTJ=;UV(dw^35;3K2)QOSsSagbS5tT(;i= zzB_S0#96PPc#xy`3z16*es^ex_qP@ogm&pAt)u&o?%ckxZ*kwmV6B?PwX)j0m{*71 zR#m!c1I7lx$N-m-u8iU#e7+&?8`;XzvCO#v=#9uZsnr@D<|eUp24O87j<_5!!35n7 zkH;NJR)T)}E?hZ;Na9L7Vn>lpp(o3UP%s=x*YW|>@w9uB>A1(?@@Lb2x7!zUrhQ~> z_KANFdJ549Zdio@871I@!>B&**Ts+sAg9g~002S)_ec-qy}_Y?(~IIH9)}xN>ihie zG-`I@-Xr{rvjjSVvvk__O56vzmtnn4)m4N-i=#tfHMnokj<7K7&QUfkL$b4>e6a={ zpk6__FWA+`#W@ux5(#fPH)iFf!I ze5wh3PXIUWq~4aU`iMyQf?&FSzc2E(h!^@tWOV3A8fU_f^?X8aF>Od|)4ZOhHBZ^( zJS62xt}2t24K9ZAr({bs6P6PL^Ee|Qa_vuyZx7oiouQFTFh4Gm!FVX5w?yNQNAga) z3wFGEb}|`hr1O41!t@BPk7FJOL8Cm{49|n&>&R1w+UKOi9l>9k9ipOLvlDZNFibi) z740@CF?(~smmi-VJwB?phQ2j)4*rk!BgXn1+9;1(!OLK%fTYcgnXU6ezXU^)ybz(% z#5^EYhQS^gzm=wqk!X$ttCN^xTwCI?rb5W0w~;2$x!D8K$Ps`|(@&5gTGMH=sKN#P zt25ci3@VJnw?HRFRVQqJVibSIacjq5agZ+gz-!<8-uBbm7Pn`!ciolEZeQHinL`Qj zJhME9Asg7ZG)LmqwzIwOM)G70j&}>75=J(cwEcoWlhTO|^W@|4A`0&W=&FE#-uhM! z2P=}QzW()@AW~3Lw|2oGqVT0t9}&wnmQF#(a_L3|L)1eyph~=gjiOEF7VS?gT#9o$ z&i|)1KjZUF4!yel^gsFh6GN}TE5W}yI69y$^Qz~lt{mN1fNisD4)TcN60)QC%TB6= zT|bmX^@wUYGmssi{Q7vY;_l3WwL_L+DWYE@?Es>bz_WnAsFBzH#HlRZR)wPis_n~; zBV0IDnhJS|RC_~HG*O=?np{doJ$5}78z0EkQu!P2_Xe`rfcHhTPY*NR<+X~!l>!cs zSYZTy6X6TTBLGpX@*$Q}(JGZ@B(C<$Mh6O_qB%EwBtF}j{gWc+3m>{3G!m4~;lBR* z1eBRr3~|wpxYwt*;#t3c$mtG8+>Rl)-5J{*M`aWpfrsP*677$Jo}FU;aeyJaTq(n* z$q@$-iV!Qv^;48xljeJr1X9v@1QE5dkwy(sJycQzOreXYkwicfC!L%H2IWAskT_}1 zL`T-_el?NxI)?1_KsN7lIDMrGqJ^MAhJz@P(*LJWD6Q|wdVLY6E$DNAbPMuN#9(D` zLeuc$y54}#9yFSx;j|}+HGcSG^&ST0g>?JFHr|&7gY5)d7;|L5D=S3ec%fy!N#Zz4o=BdbbbZ zUph0S2asG)Mb`Z#{D$2TsN&5u!p2GZp_^7zMvS=mS&!v!tkL z#EnBhiv@xpP(t_Na~@J|;Ih}{QmNdq*HeM_KbZYftox&^rQEx)_o7}^S(H0F2ogkWM^?Jyni28v1~nycoc55_lV&0dw|^)S$oPG+IO$`on*>s+00c!TE4XbIJmMr|>#!?kap-g{M#utnZs}JJ&A+IO;--_WN?t0@-p`e@fYV0x2=}B820)`LKqLo9Rcg-S$vmO+%*SnFXWMdNW9iDBY?CRt|GE#56p4gH0;NKmK4T_ zro_VmouH$nv2W#r5vbNA<4USLw zt{EAw+8oY(!sA^Oc8`r>?)VBaL|IbW7b-U}4fA>&-m{tqWqpn!|I2>LjqAxg_QQ#| z4-Th`cWu9DVSaKPl^aU&ZHaA&6pKNxvuhsVq1(x5KW)_p*_D!UC$P@=Br{dzk&)Rl zM8vudzH~<=85QWql=GY6csz{%P8)yQachg)?eavjLlvh#lS1wA5l1jRR!xDGkGliW z5#OZWSE&Z!NwO`(DlzllpLtOhAcoXkPblPe`TV#JCK^ZwQ0ypjZ`SAb&cbK|rQE9o2Gt zEHP#UQ%6x(!082(Ito^=Nf&ct3r;U|(uN5N2As|(b(oF6d!Ql>JDuK&YDxxZoN^#0 z>>8f{FHkr&q8(+392~-eId;>ldn8k0eH3 zI7}q3=slCY>-Si+B|4xzYwc2<=&)j0xP$sZefa$EA|BbL{~F6IA~OwlxY&yF>PW

96HyU=I39bRAAf;+D-=Daz&bBFFZZy>Ie^5z~k?pL>2?^j2@ z+v9YfHg0&PF(1}J$@yIkw9~urEZUz`_pal9P}wf~ciMM$_5Z%vU$x8rs(tqSFnsM_ z!}x!|bNdTpZn|Y|vX72FAK^JrAD)}Ju4dP|x}SXxu%dOrN>1w%U+-BLp0%uN7VDC* zmHpSROZM-~e`i;J)xLyv5qz5ZH)yul3p@vS(pqTzPCMSiaE=)LzuVJ4V)R$-vOnLG zYyVTM>s!3`kBXlotDSQg#e+r;Bi*C-gzYKx`L^nVPwt1Y@84GWlD*>qwig_S;NLsW z!SiyS4Q-I&$+PA4M#K}yk!lmd`≥aG*2tpSjQe4g2t& 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.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 Font = fontLigatures.loadBuffer; +import { loadBuffer } from './index'; interface Font { findLigatures(text: string): { outputGlyphs: number[]; contextRanges: [number, number][] }; @@ -263,10 +261,10 @@ const ubuntuCases: TestCase[] = [ ]; const fontPaths: Record = { - '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') + '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 = new Map(); diff --git a/addons/addon-ligatures/test/merge.test.ts b/addons/addon-ligatures/src/fontLigatures/merge.test.ts similarity index 98% rename from addons/addon-ligatures/test/merge.test.ts rename to addons/addon-ligatures/src/fontLigatures/merge.test.ts index 0dc157f2a..e56c8dae4 100644 --- a/addons/addon-ligatures/test/merge.test.ts +++ b/addons/addon-ligatures/src/fontLigatures/merge.test.ts @@ -4,8 +4,7 @@ */ import { assert } from 'chai'; - -const mergeTrees = require('../out-esbuild/fontLigatures/merge').default; +import mergeTrees from './merge'; interface LookupResult { contextRange: [number, number]; diff --git a/addons/addon-ligatures/test/mergeRange.test.ts b/addons/addon-ligatures/src/fontLigatures/mergeRange.test.ts similarity index 94% rename from addons/addon-ligatures/test/mergeRange.test.ts rename to addons/addon-ligatures/src/fontLigatures/mergeRange.test.ts index 91642e8df..790ea666b 100644 --- a/addons/addon-ligatures/test/mergeRange.test.ts +++ b/addons/addon-ligatures/src/fontLigatures/mergeRange.test.ts @@ -4,8 +4,7 @@ */ import { assert } from 'chai'; - -const mergeRange = require('../out-esbuild/fontLigatures/mergeRange').default; +import mergeRange from './mergeRange'; describe('addon-ligatures - mergeRange', () => { it('inserts a new range before the existing ones', () => { diff --git a/addons/addon-ligatures/test/parse.test.ts b/addons/addon-ligatures/src/parse.test.ts similarity index 97% rename from addons/addon-ligatures/test/parse.test.ts rename to addons/addon-ligatures/src/parse.test.ts index de846f4a9..d2150e9dc 100644 --- a/addons/addon-ligatures/test/parse.test.ts +++ b/addons/addon-ligatures/src/parse.test.ts @@ -4,8 +4,7 @@ */ import { assert } from 'chai'; - -const parse = require('../out-esbuild/parse').default; +import parse from './parse'; // TODO: integrate tests from http://test.csswg.org/suites/css-fonts-4_dev/nightly-unstable/ describe('addon-ligatures - parse', () => { diff --git a/addons/addon-ligatures/src/tsconfig.json b/addons/addon-ligatures/src/tsconfig.json index cc9a9befa..f54111a33 100644 --- a/addons/addon-ligatures/src/tsconfig.json +++ b/addons/addon-ligatures/src/tsconfig.json @@ -9,7 +9,9 @@ "noUnusedLocals": true, "preserveWatchOutput": true, "types": [ - "../../../node_modules/@types/mocha" + "../../../node_modules/@types/mocha", + // HACK: src shouldn't use node types but it's needed for index.test.ts + "../../../node_modules/@types/node" ], "paths": { "@xterm/addon-ligatures" : [ From ec03109c39cde7ef8f33cee273913eac72726bed Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 3 Jan 2026 09:25:39 -0800 Subject: [PATCH 16/38] 2 space indent --- .../src/fontLigatures/flatten.ts | 42 +- .../src/fontLigatures/index.ts | 420 +++++++------- .../src/fontLigatures/merge.ts | 520 +++++++++--------- .../src/fontLigatures/mergeRange.ts | 100 ++-- .../src/fontLigatures/processors/6-1.ts | 122 ++-- .../src/fontLigatures/processors/6-2.ts | 138 ++--- .../src/fontLigatures/processors/6-3.ts | 104 ++-- .../src/fontLigatures/processors/8-1.ts | 100 ++-- .../src/fontLigatures/processors/classDef.ts | 112 ++-- .../src/fontLigatures/processors/coverage.ts | 58 +- .../src/fontLigatures/processors/helper.ts | 238 ++++---- .../fontLigatures/processors/substitution.ts | 76 +-- .../src/fontLigatures/tables.ts | 154 +++--- .../src/fontLigatures/types.ts | 108 ++-- .../addon-ligatures/src/fontLigatures/walk.ts | 100 ++-- 15 files changed, 1196 insertions(+), 1196 deletions(-) diff --git a/addons/addon-ligatures/src/fontLigatures/flatten.ts b/addons/addon-ligatures/src/fontLigatures/flatten.ts index 38e99c3ad..f911b986a 100644 --- a/addons/addon-ligatures/src/fontLigatures/flatten.ts +++ b/addons/addon-ligatures/src/fontLigatures/flatten.ts @@ -1,35 +1,35 @@ import { LookupTree, FlattenedLookupTree, LookupTreeEntry, FlattenedLookupTreeEntry } from './types'; export default function flatten(tree: LookupTree): FlattenedLookupTree { - const result: FlattenedLookupTree = {}; - for (const [glyphId, entry] of Object.entries(tree.individual)) { - result[glyphId] = flattenEntry(entry); - } + const result: FlattenedLookupTree = {}; + 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; - } + 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; + return result; } function flattenEntry(entry: LookupTreeEntry): FlattenedLookupTreeEntry { - const result: FlattenedLookupTreeEntry = {}; + const result: FlattenedLookupTreeEntry = {}; - if (entry.forward) { - result.forward = flatten(entry.forward); - } + if (entry.forward) { + result.forward = flatten(entry.forward); + } - if (entry.reverse) { - result.reverse = flatten(entry.reverse); - } + if (entry.reverse) { + result.reverse = flatten(entry.reverse); + } - if (entry.lookup) { - result.lookup = entry.lookup; - } + if (entry.lookup) { + result.lookup = entry.lookup; + } - return result; + return result; } diff --git a/addons/addon-ligatures/src/fontLigatures/index.ts b/addons/addon-ligatures/src/fontLigatures/index.ts index e91eea92b..f15bd32a6 100644 --- a/addons/addon-ligatures/src/fontLigatures/index.ts +++ b/addons/addon-ligatures/src/fontLigatures/index.ts @@ -13,236 +13,236 @@ import buildTreeGsubType8Format1 from './processors/8-1'; import flatten from './flatten'; class FontImpl implements Font { - private _font: opentype.Font; - private _lookupTrees: { tree: FlattenedLookupTree; processForward: boolean; }[] = []; - private _glyphLookups: { [glyphId: string]: number[] } = {}; - private _cache?: LRUCache; + private _font: opentype.Font; + private _lookupTrees: { tree: FlattenedLookupTree; processForward: boolean; }[] = []; + private _glyphLookups: { [glyphId: string]: number[] } = {}; + private _cache?: LRUCache; - constructor(font: opentype.Font, options: Required) { - this._font = font; + constructor(font: opentype.Font, options: Required) { + this._font = font; - if (options.cacheSize > 0) { - this._cache = new LRUCache({ - max: options.cacheSize, - length: ((val: LigatureData | [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: LookupTree[] = []; - 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); - } - } + if (options.cacheSize > 0) { + this._cache = new LRUCache({ + max: options.cacheSize, + length: ((val: LigatureData | [number, number][], key: string) => key.length) as any + }); } - findLigatures(text: string): LigatureData { - const cached = this._cache && this._cache.get(text); - if (cached && !Array.isArray(cached)) { - return cached; + 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: LookupTree[] = []; + 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] = []; } - const glyphIds: number[] = []; - for (const char of text) { - glyphIds.push(this._font.charToGlyphIndex(char)); - } + this._glyphLookups[glyphId].push(index); + } + } + } - // 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: LigatureData = { - inputGlyphs: glyphIds, - outputGlyphs: result.sequence, - contextRanges: result.ranges - }; - if (this._cache) { - this._cache.set(text, finalResult); - } - - return finalResult; + findLigatures(text: string): LigatureData { + const cached = this._cache && this._cache.get(text); + if (cached && !Array.isArray(cached)) { + return cached; } - 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; + const glyphIds: number[] = []; + for (const char of text) { + glyphIds.push(this._font.charToGlyphIndex(char)); } - private _findInternal(sequence: number[]): { sequence: number[]; ranges: [number, number][]; } { - const ranges: [number, number][] = []; + // 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: [] + }; + } - 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; - } - } + const result = this._findInternal(glyphIds.slice()); + const finalResult: LigatureData = { + inputGlyphs: glyphIds, + outputGlyphs: result.sequence, + contextRanges: result.ranges + }; + if (this._cache) { + this._cache.set(text, finalResult); + } - mergeRange( - ranges, - result.contextRange[0] + i, - result.contextRange[1] + i - ); + return finalResult; + } - // Substitutions can end up extending the search range - if (i + result.length >= lastGlyphIndex) { - lastGlyphIndex = i + result.length + 1; - } + findLigatureRanges(text: string): [number, number][] { + // Short circuit the process if there are no possible ligatures in the + // font + if (this._lookupTrees.length === 0) { + return []; + } - 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; - } - } + const cached = this._cache && this._cache.get(text); + if (cached) { + return Array.isArray(cached) ? cached : cached.contextRanges; + } - mergeRange( - ranges, - result.contextRange[0] + i, - result.contextRange[1] + i - ); + const glyphIds: number[] = []; + for (const char of text) { + glyphIds.push(this._font.charToGlyphIndex(char)); + } - i -= result.length - 1; - } - } + 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; + } } - nextLookup = this._getNextLookup(sequence, nextLookup.index + 1); - } + mergeRange( + ranges, + result.contextRange[0] + i, + result.contextRange[1] + i + ); - 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; + // Substitutions can end up extending the search range + if (i + result.length >= lastGlyphIndex) { + lastGlyphIndex = i + result.length + 1; } - 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; - } - } + 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; + } + } - return result; + 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; + } } /** @@ -252,11 +252,11 @@ class FontImpl implements Font { * @param buffer ArrayBuffer of the font to load */ export function loadBuffer(buffer: ArrayBuffer, options?: Options): Font { - const font = opentype.parse(buffer); - return new FontImpl(font, { - cacheSize: 0, - ...options - }); + const font = opentype.parse(buffer); + return new FontImpl(font, { + cacheSize: 0, + ...options + }); } export { Font, LigatureData, Options }; diff --git a/addons/addon-ligatures/src/fontLigatures/merge.ts b/addons/addon-ligatures/src/fontLigatures/merge.ts index d0b1f23e9..13c877335 100644 --- a/addons/addon-ligatures/src/fontLigatures/merge.ts +++ b/addons/addon-ligatures/src/fontLigatures/merge.ts @@ -9,16 +9,16 @@ import { LookupTree, LookupTreeEntry } from './types'; * over those in later trees when there is a choice. */ export default function mergeTrees(trees: LookupTree[]): LookupTree { - const result: LookupTree = { - individual: {}, - range: [] - }; + const result: LookupTree = { + individual: {}, + range: [] + }; - for (const tree of trees) { - mergeSubtree(result, tree); - } + for (const tree of trees) { + mergeSubtree(result, tree); + } - return result; + return result; } /** @@ -28,162 +28,162 @@ export default function mergeTrees(trees: LookupTree[]): LookupTree { * @param mergeTree The tree to be merged into the mainTree */ function mergeSubtree(mainTree: LookupTree, mergeTree: LookupTree): 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); + // 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; - } + // 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 (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; - } + 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--; + mainTree.range.splice(index, 1); + index--; - const entryToMerge: LookupTreeEntry = cloneEntry(resultEntry); - if (Array.isArray(overlap.both)) { - mainTree.range.push({ - range: overlap.both, - entry: entryToMerge - }); - } else { - mainTree.individual[overlap.both] = entryToMerge; - } + const entryToMerge: LookupTreeEntry = 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)); + 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) - }); + for (const second of overlap.second) { + if (Array.isArray(second)) { + mainTree.range.push({ + range: second, + entry: cloneEntry(resultEntry) + }); } else { - mainTree.individual[remainingRange] = cloneEntry(entry); + 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); + } + } + } } /** @@ -193,37 +193,37 @@ function mergeSubtree(mainTree: LookupTree, mergeTree: LookupTree): void { * @param mergeTree The entry to merge into the mainTree */ function mergeTreeEntry(mainTree: LookupTreeEntry, mergeTree: LookupTreeEntry): 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.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.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); - } + if (mergeTree.reverse) { + if (!mainTree.reverse) { + mainTree.reverse = mergeTree.reverse; + } else { + mergeSubtree(mainTree.reverse, mergeTree.reverse); } + } } interface Overlap { - first: (number | [number, number])[]; - second: (number | [number, number])[]; - both: number | [number, number] | null; + first: (number | [number, number])[]; + second: (number | [number, number])[]; + both: number | [number, number] | null; } /** @@ -234,42 +234,42 @@ interface Overlap { * @param second Second range */ function getRangeOverlap(first: [number, number], second: [number, number]): Overlap { - const result: Overlap = { - first: [], - second: [], - both: null - }; + const result: Overlap = { + 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); - } + // 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)); - } + // 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)); - } + // 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; + return result; } /** @@ -281,30 +281,30 @@ function getRangeOverlap(first: [number, number], second: [number, number]): Ove * @param second Range */ function getIndividualOverlap(first: number, second: [number, number]): Overlap { - // Disjoint - if (first < second[0] || first > second[1]) { - return { - first: [first], - second: [second], - both: null - }; - } - - const result: Overlap = { - first: [], - second: [], - both: first + // Disjoint + if (first < second[0] || first > second[1]) { + return { + first: [first], + second: [second], + both: null }; + } - if (second[0] < first) { - result.second.push(rangeOrIndividual(second[0], first)); - } + const result: Overlap = { + first: [], + second: [], + both: first + }; - if (second[1] > first) { - result.second.push(rangeOrIndividual(first + 1, second[1])); - } + if (second[0] < first) { + result.second.push(rangeOrIndividual(second[0], first)); + } - return result; + if (second[1] > first) { + result.second.push(rangeOrIndividual(first + 1, second[1])); + } + + return result; } /** @@ -315,11 +315,11 @@ function getIndividualOverlap(first: number, second: [number, number]): Overlap * @param end End of the range (exclusive) */ function rangeOrIndividual(start: number, end: number): number | [number, number] { - if (end - start === 1) { - return start; - } else { - return [start, end]; - } + if (end - start === 1) { + return start; + } else { + return [start, end]; + } } /** @@ -328,27 +328,27 @@ function rangeOrIndividual(start: number, end: number): number | [number, number * @param entry Lookup tree entry to clone */ function cloneEntry(entry: LookupTreeEntry): LookupTreeEntry { - const result: LookupTreeEntry = {}; + const result: LookupTreeEntry = {}; - if (entry.forward) { - result.forward = cloneTree(entry.forward); - } + if (entry.forward) { + result.forward = cloneTree(entry.forward); + } - if (entry.reverse) { - result.reverse = cloneTree(entry.reverse); - } + 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() - }; - } + 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; + return result; } /** @@ -357,16 +357,16 @@ function cloneEntry(entry: LookupTreeEntry): LookupTreeEntry { * @param tree Lookup tree to clone */ function cloneTree(tree: LookupTree): LookupTree { - const individual: { [glyphId: string]: LookupTreeEntry; } = {}; - for (const [glyphId, entry] of Object.entries(tree.individual)) { - individual[glyphId] = cloneEntry(entry); - } + const individual: { [glyphId: string]: LookupTreeEntry; } = {}; + 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) - })) - }; + return { + individual, + range: tree.range.map(({ range, entry }) => ({ + range: range.slice() as [number, number], + entry: cloneEntry(entry) + })) + }; } diff --git a/addons/addon-ligatures/src/fontLigatures/mergeRange.ts b/addons/addon-ligatures/src/fontLigatures/mergeRange.ts index 7c872a8a3..2b9ac6b7b 100644 --- a/addons/addon-ligatures/src/fontLigatures/mergeRange.ts +++ b/addons/addon-ligatures/src/fontLigatures/mergeRange.ts @@ -8,57 +8,57 @@ * @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; - } else 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; - } else 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; - } else 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; - } else { - // 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; + 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; + } else 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; + } else 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 { - // Case 9: New range starts after the last existing range - ranges.push([newRangeStart, newRangeEnd]); + 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; + } else 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; + } else { + // Case 7: New range extends from previous range past the + // end of the current range + ranges.splice(i, 1); + i--; + } } + } - return ranges; + 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; } diff --git a/addons/addon-ligatures/src/fontLigatures/processors/6-1.ts b/addons/addon-ligatures/src/fontLigatures/processors/6-1.ts index 2f0dad180..dc2e8917d 100644 --- a/addons/addon-ligatures/src/fontLigatures/processors/6-1.ts +++ b/addons/addon-ligatures/src/fontLigatures/processors/6-1.ts @@ -13,70 +13,70 @@ import { processInputPosition, processLookaheadPosition, processBacktrackPositio * @param tableIndex Index of this table in the overall lookup */ export default function buildTree(table: ChainingContextualSubstitutionTable.Format1, lookups: Lookup[], tableIndex: number): LookupTree { - const result: LookupTree = { - individual: {}, - range: [] - }; + const result: LookupTree = { + individual: {}, + range: [] + }; - const firstGlyphs = listGlyphsByIndex(table.coverage); + const firstGlyphs = listGlyphsByIndex(table.coverage); - for (const { glyphId, index } of firstGlyphs) { - const chainRuleSet = table.chainRuleSets[index]; + 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: EntryMeta[] = 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 - ] - }; - } - } + // If the chain rule set is null there's nothing to do with this table. + if (!chainRuleSet) { + continue; } - return result; + for (const [subIndex, subTable] of chainRuleSet.entries()) { + let currentEntries: EntryMeta[] = 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; } diff --git a/addons/addon-ligatures/src/fontLigatures/processors/6-2.ts b/addons/addon-ligatures/src/fontLigatures/processors/6-2.ts index 9cd8eaf33..f0f9245ce 100644 --- a/addons/addon-ligatures/src/fontLigatures/processors/6-2.ts +++ b/addons/addon-ligatures/src/fontLigatures/processors/6-2.ts @@ -15,82 +15,82 @@ import { processInputPosition, processLookaheadPosition, processBacktrackPositio * @param tableIndex Index of this table in the overall lookup */ export default function buildTree(table: ChainingContextualSubstitutionTable.Format2, lookups: Lookup[], tableIndex: number): LookupTree { - const results: LookupTree[] = []; + const results: LookupTree[] = []; - const firstGlyphs = listGlyphsByIndex(table.coverage); + 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; - } + 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]; + const classSet = table.chainClassSet[inputClass]; - // If the class set is null there's nothing to do with this table. - if (!classSet) { - continue; - } + // 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: LookupTree = { - individual: {}, - range: [] - }; + for (const [subIndex, subTable] of classSet.entries()) { + const result: LookupTree = { + individual: {}, + range: [] + }; - let currentEntries: EntryMeta[] = getInputTree( - result, - subTable.lookupRecords, - lookups, - 0, - glyphId - ).map(({ entry, substitution }) => ({ entry, substitutions: [substitution] })); + let currentEntries: EntryMeta[] = 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); - } + for (const [index, classNum] of subTable.input.entries()) { + currentEntries = processInputPosition( + listClassGlyphs(table.inputClassDef, classNum), + index + 1, + currentEntries, + subTable.lookupRecords, + lookups + ); } - } - return mergeTrees(results); + 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); } diff --git a/addons/addon-ligatures/src/fontLigatures/processors/6-3.ts b/addons/addon-ligatures/src/fontLigatures/processors/6-3.ts index fc671e7b7..07e4681fe 100644 --- a/addons/addon-ligatures/src/fontLigatures/processors/6-3.ts +++ b/addons/addon-ligatures/src/fontLigatures/processors/6-3.ts @@ -13,61 +13,61 @@ import { processInputPosition, processLookaheadPosition, processBacktrackPositio * @param tableIndex Index of this table in the overall lookup */ export default function buildTree(table: ChainingContextualSubstitutionTable.Format3, lookups: Lookup[], tableIndex: number): LookupTree { - const result: LookupTree = { - individual: {}, - range: [] - }; + const result: LookupTree = { + individual: {}, + range: [] + }; - const firstGlyphs = listGlyphsByIndex(table.inputCoverage[0]); + const firstGlyphs = listGlyphsByIndex(table.inputCoverage[0]); - for (const { glyphId } of firstGlyphs) { - let currentEntries: EntryMeta[] = getInputTree( - result, - table.lookupRecords, - lookups, - 0, - glyphId - ).map(({ entry, substitution }) => ({ entry, substitutions: [substitution] })); + for (const { glyphId } of firstGlyphs) { + let currentEntries: EntryMeta[] = 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 - ] - }; - } + for (const [index, coverage] of table.inputCoverage.slice(1).entries()) { + currentEntries = processInputPosition( + listGlyphsByIndex(coverage).map(glyph => glyph.glyphId), + index + 1, + currentEntries, + table.lookupRecords, + lookups + ); } - return result; + 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; } diff --git a/addons/addon-ligatures/src/fontLigatures/processors/8-1.ts b/addons/addon-ligatures/src/fontLigatures/processors/8-1.ts index 620d2e20a..8f127f6b9 100644 --- a/addons/addon-ligatures/src/fontLigatures/processors/8-1.ts +++ b/addons/addon-ligatures/src/fontLigatures/processors/8-1.ts @@ -12,58 +12,58 @@ import { processLookaheadPosition, processBacktrackPosition, EntryMeta } from '. * @param tableIndex Index of this table in the overall lookup */ export default function buildTree(table: ReverseChainingContextualSingleSubstitutionTable, tableIndex: number): LookupTree { - const result: LookupTree = { - individual: {}, - range: [] - }; + const result: LookupTree = { + individual: {}, + range: [] + }; - const glyphs = listGlyphsByIndex(table.coverage); + const glyphs = listGlyphsByIndex(table.coverage); - for (const { glyphId, index } of glyphs) { - const initialEntry: LookupTreeEntry = {}; - if (Array.isArray(glyphId)) { - result.range.push({ - entry: initialEntry, - range: glyphId - }); - } else { - result.individual[glyphId] = initialEntry; - } - - let currentEntries: EntryMeta[] = [{ - 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 - ] - }; - } + for (const { glyphId, index } of glyphs) { + const initialEntry: LookupTreeEntry = {}; + if (Array.isArray(glyphId)) { + result.range.push({ + entry: initialEntry, + range: glyphId + }); + } else { + result.individual[glyphId] = initialEntry; } - return result; + let currentEntries: EntryMeta[] = [{ + 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; } diff --git a/addons/addon-ligatures/src/fontLigatures/processors/classDef.ts b/addons/addon-ligatures/src/fontLigatures/processors/classDef.ts index 3f2dc21ec..bcf98ea69 100644 --- a/addons/addon-ligatures/src/fontLigatures/processors/classDef.ts +++ b/addons/addon-ligatures/src/fontLigatures/processors/classDef.ts @@ -8,78 +8,78 @@ import { ClassDefTable } from '../tables'; * @param glyphId Index of the glyph to look for */ export default function getGlyphClass(table: ClassDefTable, glyphId: number | [number, number]): Map { - 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); - } else { - 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]]); - } + 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); + } else { + 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.Format2, glyphId: [number, number]): Map { - let classStart: number = glyphId[0]; - let currentClass: number | null = getIndividualGlyphClass(table, classStart); - let search: number = glyphId[0] + 1; + let classStart: number = glyphId[0]; + let currentClass: number | null = getIndividualGlyphClass(table, classStart); + let search: number = glyphId[0] + 1; - const result = new Map<[number, number] | number, number | null>(); + 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) { + while (search < glyphId[1]) { + const clazz = getIndividualGlyphClass(table, search); + if (clazz !== currentClass) { + if (search - classStart <= 1) { result.set(classStart, currentClass); - } else { + } else { result.set([classStart, search], currentClass); + } } + search++; + } - return result; + if (search - classStart <= 1) { + result.set(classStart, currentClass); + } else { + result.set([classStart, search], currentClass); + } + + return result; } function getIndividualGlyphClass(table: ClassDefTable.Format2, glyphId: number): number | null { - for (const range of table.ranges) { - if (range.start <= glyphId && range.end >= glyphId) { - return range.classId; - } + for (const range of table.ranges) { + if (range.start <= glyphId && range.end >= glyphId) { + return range.classId; } + } - return null; + 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; - } + 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 []; - } + if (range.end === range.start) { + results.push(range.start); + } else { + results.push([range.start, range.end + 1]); + } + } + return results; + default: + return []; + } } diff --git a/addons/addon-ligatures/src/fontLigatures/processors/coverage.ts b/addons/addon-ligatures/src/fontLigatures/processors/coverage.ts index 0ee3f42d6..02d518d08 100644 --- a/addons/addon-ligatures/src/fontLigatures/processors/coverage.ts +++ b/addons/addon-ligatures/src/fontLigatures/processors/coverage.ts @@ -8,36 +8,36 @@ import { CoverageTable } from '../tables'; * @param glyphId Index of the glyph to look for */ export default function getCoverageGlyphIndex(table: CoverageTable, glyphId: number): number | null { - switch (table.format) { - // https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#coverage-format-1 - case 1: - const index = table.glyphs.indexOf(glyphId); - return index !== -1 - ? index - : null; - // https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#coverage-format-2 - case 2: - const range = table.ranges - .find(range => range.start <= glyphId && range.end >= glyphId); - return range - ? range.index - : null; - } + switch (table.format) { + // https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#coverage-format-1 + case 1: + const index = table.glyphs.indexOf(glyphId); + return index !== -1 + ? index + : null; + // https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#coverage-format-2 + case 2: + const range = table.ranges + .find(range => range.start <= glyphId && range.end >= glyphId); + return range + ? range.index + : null; + } } export function listGlyphsByIndex(table: CoverageTable): { glyphId: number | [number, number]; index: number; }[] { - switch (table.format) { - case 1: - return table.glyphs.map((glyphId, index) => ({ glyphId, index })); - case 2: - let results: { glyphId: number | [number, number]; index: number; }[] = []; - for (const [index, range] of table.ranges.entries()) { - if (range.end === range.start) { - results.push({ glyphId: range.start, index }); - } else { - results.push({ glyphId: [range.start, range.end + 1], index }); - } - } - return results; - } + switch (table.format) { + case 1: + return table.glyphs.map((glyphId, index) => ({ glyphId, index })); + case 2: + let results: { glyphId: number | [number, number]; index: number; }[] = []; + for (const [index, range] of table.ranges.entries()) { + if (range.end === range.start) { + results.push({ glyphId: range.start, index }); + } else { + results.push({ glyphId: [range.start, range.end + 1], index }); + } + } + return results; + } } diff --git a/addons/addon-ligatures/src/fontLigatures/processors/helper.ts b/addons/addon-ligatures/src/fontLigatures/processors/helper.ts index 7186a22de..c15c0c920 100644 --- a/addons/addon-ligatures/src/fontLigatures/processors/helper.ts +++ b/addons/addon-ligatures/src/fontLigatures/processors/helper.ts @@ -4,160 +4,160 @@ import { SubstitutionLookupRecord, Lookup } from '../tables'; import { getIndividualSubstitutionGlyph, getRangeSubstitutionGlyphs } from './substitution'; export interface EntryMeta { - entry: LookupTreeEntry; - substitutions: (number | null)[]; + entry: LookupTreeEntry; + substitutions: (number | null)[]; } export function processInputPosition( - glyphs: (number | [number, number])[], - position: number, - currentEntries: EntryMeta[], - lookupRecords: SubstitutionLookupRecord[], - lookups: Lookup[] + glyphs: (number | [number, number])[], + position: number, + currentEntries: EntryMeta[], + lookupRecords: SubstitutionLookupRecord[], + lookups: Lookup[] ): EntryMeta[] { - const nextEntries: EntryMeta[] = []; - for (const currentEntry of currentEntries) { - currentEntry.entry.forward = { - individual: {}, - range: [] - }; - for (const glyph of glyphs) { - nextEntries.push(...getInputTree( - currentEntry.entry.forward, - lookupRecords, - lookups, - position, - glyph - ).map(({ entry, substitution }) => ({ - entry, - substitutions: [...currentEntry.substitutions, substitution] - }))); - } + const nextEntries: EntryMeta[] = []; + for (const currentEntry of currentEntries) { + currentEntry.entry.forward = { + individual: {}, + range: [] + }; + for (const glyph of glyphs) { + nextEntries.push(...getInputTree( + currentEntry.entry.forward, + lookupRecords, + lookups, + position, + glyph + ).map(({ entry, substitution }) => ({ + entry, + substitutions: [...currentEntry.substitutions, substitution] + }))); } + } - return nextEntries; + return nextEntries; } export function processLookaheadPosition( - glyphs: (number | [number, number])[], - currentEntries: EntryMeta[] + glyphs: (number | [number, number])[], + currentEntries: EntryMeta[] ): EntryMeta[] { - const nextEntries: EntryMeta[] = []; - for (const currentEntry of currentEntries) { - for (const glyph of glyphs) { - const entry: LookupTreeEntry = {}; - if (!currentEntry.entry.forward) { - currentEntry.entry.forward = { - individual: {}, - range: [] - }; - } - nextEntries.push({ - entry, - substitutions: currentEntry.substitutions - }); + const nextEntries: EntryMeta[] = []; + for (const currentEntry of currentEntries) { + for (const glyph of glyphs) { + const entry: LookupTreeEntry = {}; + if (!currentEntry.entry.forward) { + currentEntry.entry.forward = { + individual: {}, + range: [] + }; + } + nextEntries.push({ + entry, + substitutions: currentEntry.substitutions + }); - if (Array.isArray(glyph)) { - currentEntry.entry.forward.range.push({ - entry, - range: glyph - }); - } else { - currentEntry.entry.forward.individual[glyph] = entry; - } - } + if (Array.isArray(glyph)) { + currentEntry.entry.forward.range.push({ + entry, + range: glyph + }); + } else { + currentEntry.entry.forward.individual[glyph] = entry; + } } + } - return nextEntries; + return nextEntries; } export function processBacktrackPosition( - glyphs: (number | [number, number])[], - currentEntries: EntryMeta[] + glyphs: (number | [number, number])[], + currentEntries: EntryMeta[] ): EntryMeta[] { - const nextEntries: EntryMeta[] = []; - for (const currentEntry of currentEntries) { - for (const glyph of glyphs) { - const entry: LookupTreeEntry = {}; - if (!currentEntry.entry.reverse) { - currentEntry.entry.reverse = { - individual: {}, - range: [] - }; - } - nextEntries.push({ - entry, - substitutions: currentEntry.substitutions - }); + const nextEntries: EntryMeta[] = []; + for (const currentEntry of currentEntries) { + for (const glyph of glyphs) { + const entry: LookupTreeEntry = {}; + if (!currentEntry.entry.reverse) { + currentEntry.entry.reverse = { + individual: {}, + range: [] + }; + } + nextEntries.push({ + entry, + substitutions: currentEntry.substitutions + }); - if (Array.isArray(glyph)) { - currentEntry.entry.reverse.range.push({ - entry, - range: glyph - }); - } else { - currentEntry.entry.reverse.individual[glyph] = entry; - } - } + if (Array.isArray(glyph)) { + currentEntry.entry.reverse.range.push({ + entry, + range: glyph + }); + } else { + currentEntry.entry.reverse.individual[glyph] = entry; + } } + } - return nextEntries; + return nextEntries; } export function getInputTree(tree: LookupTree, substitutions: SubstitutionLookupRecord[], lookups: Lookup[], inputIndex: number, glyphId: number | [number, number]): { entry: LookupTreeEntry; substitution: number | null; }[] { - const result: { entry: LookupTreeEntry; substitution: number | null; }[] = []; - if (!Array.isArray(glyphId)) { - tree.individual[glyphId] = {}; - result.push({ - entry: tree.individual[glyphId], - substitution: getSubstitutionAtPosition(substitutions, lookups, inputIndex, glyphId) - }); - } else { - const subs = getSubstitutionAtPositionRange(substitutions, lookups, inputIndex, glyphId); - for (const [range, substitution] of subs) { - const entry: LookupTreeEntry = {}; - if (Array.isArray(range)) { - tree.range.push({ range, entry }); - } else { - tree.individual[range] = {}; - } - result.push({ entry, substitution }); - } + const result: { entry: LookupTreeEntry; substitution: number | null; }[] = []; + if (!Array.isArray(glyphId)) { + tree.individual[glyphId] = {}; + result.push({ + entry: tree.individual[glyphId], + substitution: getSubstitutionAtPosition(substitutions, lookups, inputIndex, glyphId) + }); + } else { + const subs = getSubstitutionAtPositionRange(substitutions, lookups, inputIndex, glyphId); + for (const [range, substitution] of subs) { + const entry: LookupTreeEntry = {}; + if (Array.isArray(range)) { + tree.range.push({ range, entry }); + } else { + tree.individual[range] = {}; + } + result.push({ entry, substitution }); } + } - return result; + return result; } function getSubstitutionAtPositionRange(substitutions: SubstitutionLookupRecord[], lookups: Lookup[], index: number, range: [number, number]): Map { - for (const substitution of substitutions.filter(s => s.sequenceIndex === index)) { - for (const substitutionTable of (lookups[substitution.lookupListIndex] as Lookup.Type1).subtables) { - const sub = getRangeSubstitutionGlyphs( - substitutionTable, - range - ); + for (const substitution of substitutions.filter(s => s.sequenceIndex === index)) { + for (const substitutionTable of (lookups[substitution.lookupListIndex] as Lookup.Type1).subtables) { + const sub = getRangeSubstitutionGlyphs( + substitutionTable, + range + ); - if (!Array.from(sub.values()).every(val => val !== null)) { - return sub; - } - } + if (!Array.from(sub.values()).every(val => val !== null)) { + return sub; + } } + } - return new Map([[range, null]]); + return new Map([[range, null]]); } function getSubstitutionAtPosition(substitutions: SubstitutionLookupRecord[], lookups: Lookup[], index: number, glyphId: number): number | null { - for (const substitution of substitutions.filter(s => s.sequenceIndex === index)) { - for (const substitutionTable of (lookups[substitution.lookupListIndex] as Lookup.Type1).subtables) { - const sub = getIndividualSubstitutionGlyph( - substitutionTable, - glyphId - ); + for (const substitution of substitutions.filter(s => s.sequenceIndex === index)) { + for (const substitutionTable of (lookups[substitution.lookupListIndex] as Lookup.Type1).subtables) { + const sub = getIndividualSubstitutionGlyph( + substitutionTable, + glyphId + ); - if (sub !== null) { - return sub; - } - } + if (sub !== null) { + return sub; + } } + } - return null; + return null; } diff --git a/addons/addon-ligatures/src/fontLigatures/processors/substitution.ts b/addons/addon-ligatures/src/fontLigatures/processors/substitution.ts index 975d7a25e..e277bd491 100644 --- a/addons/addon-ligatures/src/fontLigatures/processors/substitution.ts +++ b/addons/addon-ligatures/src/fontLigatures/processors/substitution.ts @@ -10,53 +10,53 @@ import getCoverageGlyphIndex from './coverage'; * @param glyphId The index of the glpyh to find substitutions for */ export function getRangeSubstitutionGlyphs(table: SubstitutionTable, glyphId: [number, number]): Map<[number, number] | number, number | null> { - let replacementStart: number = glyphId[0]; - let currentReplacement: number | null = getIndividualSubstitutionGlyph(table, replacementStart); - let search: number = glyphId[0] + 1; + let replacementStart: number = glyphId[0]; + let currentReplacement: number | null = getIndividualSubstitutionGlyph(table, replacementStart); + let search: number = glyphId[0] + 1; - const result = new Map<[number, number] | number, number | null>(); + const result = new Map<[number, number] | number, number | null>(); - while (search < glyphId[1]) { - const sub = getIndividualSubstitutionGlyph(table, search); - if (sub !== currentReplacement) { - if (search - replacementStart <= 1) { - result.set(replacementStart, currentReplacement); - } else { - result.set([replacementStart, search], currentReplacement); - } - } - - search++; - } - - if (search - replacementStart <= 1) { + while (search < glyphId[1]) { + const sub = getIndividualSubstitutionGlyph(table, search); + if (sub !== currentReplacement) { + if (search - replacementStart <= 1) { result.set(replacementStart, currentReplacement); - } else { + } else { result.set([replacementStart, search], currentReplacement); + } } - return result; + search++; + } + + if (search - replacementStart <= 1) { + result.set(replacementStart, currentReplacement); + } else { + result.set([replacementStart, search], currentReplacement); + } + + return result; } export function getIndividualSubstitutionGlyph(table: SubstitutionTable, glyphId: number): number | null { - const coverageIndex = getCoverageGlyphIndex(table.coverage, glyphId); + const coverageIndex = getCoverageGlyphIndex(table.coverage, glyphId); - // istanbul ignore next - invalid font - if (coverageIndex === null) { - return null; - } + // istanbul ignore next - invalid font + if (coverageIndex === null) { + return null; + } - switch (table.substFormat) { - // https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#11-single-substitution-format-1 - case 1: - // TODO: determine if there's a rhyme or reason to the 16-bit - // wraparound and if it can ever be a different number - return (glyphId + table.deltaGlyphId) % (2 ** 16); - // https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#12-single-substitution-format-2 - case 2: - // tslint:disable-next-line - return table.substitute[coverageIndex] != null - ? table.substitute[coverageIndex] - : null; - } + switch (table.substFormat) { + // https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#11-single-substitution-format-1 + case 1: + // TODO: determine if there's a rhyme or reason to the 16-bit + // wraparound and if it can ever be a different number + return (glyphId + table.deltaGlyphId) % (2 ** 16); + // https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#12-single-substitution-format-2 + case 2: + // tslint:disable-next-line + return table.substitute[coverageIndex] != null + ? table.substitute[coverageIndex] + : null; + } } diff --git a/addons/addon-ligatures/src/fontLigatures/tables.ts b/addons/addon-ligatures/src/fontLigatures/tables.ts index d3eccd08d..ec6f36046 100644 --- a/addons/addon-ligatures/src/fontLigatures/tables.ts +++ b/addons/addon-ligatures/src/fontLigatures/tables.ts @@ -1,112 +1,112 @@ export type SubstitutionTable = SubstitutionTable.Format1 | SubstitutionTable.Format2; export namespace SubstitutionTable { - export interface Format1 { - substFormat: 1; - coverage: CoverageTable; - deltaGlyphId: number; - } + export interface Format1 { + substFormat: 1; + coverage: CoverageTable; + deltaGlyphId: number; + } - export interface Format2 { - substFormat: 2; - coverage: CoverageTable; - substitute: number[]; - } + export interface Format2 { + substFormat: 2; + coverage: CoverageTable; + substitute: number[]; + } } export type CoverageTable = CoverageTable.Format1 | CoverageTable.Format2; export namespace CoverageTable { - export interface Format1 { - format: 1; - glyphs: number[]; - } + export interface Format1 { + format: 1; + glyphs: number[]; + } - export interface Format2 { - format: 2; - ranges: { - start: number; - end: number; - index: number; - }[]; - } + export interface Format2 { + format: 2; + ranges: { + start: number; + end: number; + index: number; + }[]; + } } export type ChainingContextualSubstitutionTable = ChainingContextualSubstitutionTable.Format1 | - ChainingContextualSubstitutionTable.Format2 | ChainingContextualSubstitutionTable.Format3; + ChainingContextualSubstitutionTable.Format2 | ChainingContextualSubstitutionTable.Format3; export namespace ChainingContextualSubstitutionTable { - export interface Format1 { - substFormat: 1; - coverage: CoverageTable; - chainRuleSets: ChainSubRuleTable[][]; - } + export interface Format1 { + substFormat: 1; + coverage: CoverageTable; + chainRuleSets: ChainSubRuleTable[][]; + } - export interface Format2 { - substFormat: 2; - coverage: CoverageTable; - backtrackClassDef: ClassDefTable; - inputClassDef: ClassDefTable; - lookaheadClassDef: ClassDefTable; - chainClassSet: (null | ChainSubClassRuleTable[])[]; - } + export interface Format2 { + substFormat: 2; + coverage: CoverageTable; + backtrackClassDef: ClassDefTable; + inputClassDef: ClassDefTable; + lookaheadClassDef: ClassDefTable; + chainClassSet: (null | ChainSubClassRuleTable[])[]; + } - export interface Format3 { - substFormat: 3; - backtrackCoverage: CoverageTable[]; - inputCoverage: CoverageTable[]; - lookaheadCoverage: CoverageTable[]; - lookupRecords: SubstitutionLookupRecord[]; - } + export interface Format3 { + substFormat: 3; + backtrackCoverage: CoverageTable[]; + inputCoverage: CoverageTable[]; + lookaheadCoverage: CoverageTable[]; + lookupRecords: SubstitutionLookupRecord[]; + } } export interface ReverseChainingContextualSingleSubstitutionTable { - substFormat: 1; - coverage: CoverageTable; - backtrackCoverage: CoverageTable[]; - lookaheadCoverage: CoverageTable[]; - substitutes: number[]; + substFormat: 1; + coverage: CoverageTable; + backtrackCoverage: CoverageTable[]; + lookaheadCoverage: CoverageTable[]; + substitutes: number[]; } export type ClassDefTable = ClassDefTable.Format2; export namespace ClassDefTable { - export interface Format2 { - format: 2; - ranges: { - start: number; - end: number; - classId: number; - }[]; - } + export interface Format2 { + format: 2; + ranges: { + start: number; + end: number; + classId: number; + }[]; + } } export interface SubstitutionLookupRecord { - sequenceIndex: number; - lookupListIndex: number; + sequenceIndex: number; + lookupListIndex: number; } export type ChainSubRuleTable = ChainSubClassRuleTable; export interface ChainSubClassRuleTable { - backtrack: number[]; - input: number[]; - lookahead: number[]; - lookupRecords: SubstitutionLookupRecord[]; + backtrack: number[]; + input: number[]; + lookahead: number[]; + lookupRecords: SubstitutionLookupRecord[]; } export type Lookup = Lookup.Type1 | Lookup.Type6 | Lookup.Type8; export namespace Lookup { - export interface Type1 { - lookupType: 1; - lookupFlag: number; - subtables: SubstitutionTable[]; - } + export interface Type1 { + lookupType: 1; + lookupFlag: number; + subtables: SubstitutionTable[]; + } - export interface Type6 { - lookupType: 6; - lookupFlag: number; - subtables: ChainingContextualSubstitutionTable[]; - } + export interface Type6 { + lookupType: 6; + lookupFlag: number; + subtables: ChainingContextualSubstitutionTable[]; + } - export interface Type8 { - lookupType: 8; - lookupFlag: number; - subtables: ReverseChainingContextualSingleSubstitutionTable[]; - } + export interface Type8 { + lookupType: 8; + lookupFlag: number; + subtables: ReverseChainingContextualSingleSubstitutionTable[]; + } } diff --git a/addons/addon-ligatures/src/fontLigatures/types.ts b/addons/addon-ligatures/src/fontLigatures/types.ts index 9364c75c4..789fed380 100644 --- a/addons/addon-ligatures/src/fontLigatures/types.ts +++ b/addons/addon-ligatures/src/fontLigatures/types.ts @@ -1,86 +1,86 @@ export interface SubstitutionResult { - index: number; - contextRange: [number, number]; + index: number; + contextRange: [number, number]; } /** * Information about ligatures found in a sequence of text */ export interface LigatureData { - /** - * The list of font glyphs in the input text. - */ - inputGlyphs: number[]; + /** + * The list of font glyphs in the input text. + */ + inputGlyphs: number[]; - /** - * The list of font glyphs after performing replacements for font ligatures. - */ - outputGlyphs: number[]; + /** + * The list of font glyphs after performing replacements for font ligatures. + */ + outputGlyphs: number[]; - /** - * Sorted array of ranges that must be rendered together to produce the - * ligatures in the output sequence. The ranges are inclusive on the left and - * exclusive on the right. - */ - contextRanges: [number, number][]; + /** + * Sorted array of ranges that must be rendered together to produce the + * ligatures in the output sequence. The ranges are inclusive on the left and + * exclusive on the right. + */ + contextRanges: [number, number][]; } export interface Font { - /** - * Scans the provided text for font ligatures, returning an object with - * metadata about the text and any ligatures found. - * - * @param text String to search for ligatures - */ - findLigatures(text: string): LigatureData; + /** + * Scans the provided text for font ligatures, returning an object with + * metadata about the text and any ligatures found. + * + * @param text String to search for ligatures + */ + findLigatures(text: string): LigatureData; - /** - * Scans the provided text for font ligatures, returning an array of ranges - * where ligatures are located. - * - * @param text String to search for ligatures - */ - findLigatureRanges(text: string): [number, number][]; + /** + * Scans the provided text for font ligatures, returning an array of ranges + * where ligatures are located. + * + * @param text String to search for ligatures + */ + findLigatureRanges(text: string): [number, number][]; } export interface Options { - /** - * Optional size of previous results to store, measured in total number of - * characters from input strings. Defaults to no cache (0) - */ - cacheSize?: number; + /** + * Optional size of previous results to store, measured in total number of + * characters from input strings. Defaults to no cache (0) + */ + cacheSize?: number; } export interface LookupTree { - individual: { - [glyphId: string]: LookupTreeEntry; - }; - range: { - range: [number, number]; - entry: LookupTreeEntry; - }[]; + individual: { + [glyphId: string]: LookupTreeEntry; + }; + range: { + range: [number, number]; + entry: LookupTreeEntry; + }[]; } export interface LookupTreeEntry { - lookup?: LookupResult; - forward?: LookupTree; - reverse?: LookupTree; + lookup?: LookupResult; + forward?: LookupTree; + reverse?: LookupTree; } export interface LookupResult { - substitutions: (number | null)[]; - length: number; - index: number; - subIndex: number; - contextRange: [number, number]; + substitutions: (number | null)[]; + length: number; + index: number; + subIndex: number; + contextRange: [number, number]; } export interface FlattenedLookupTree { - [glyphId: string]: FlattenedLookupTreeEntry; + [glyphId: string]: FlattenedLookupTreeEntry; } export interface FlattenedLookupTreeEntry { - lookup?: LookupResult; - forward?: FlattenedLookupTree; - reverse?: FlattenedLookupTree; + lookup?: LookupResult; + forward?: FlattenedLookupTree; + reverse?: FlattenedLookupTree; } diff --git a/addons/addon-ligatures/src/fontLigatures/walk.ts b/addons/addon-ligatures/src/fontLigatures/walk.ts index 56d6eac1f..b122897e6 100644 --- a/addons/addon-ligatures/src/fontLigatures/walk.ts +++ b/addons/addon-ligatures/src/fontLigatures/walk.ts @@ -1,67 +1,67 @@ import { FlattenedLookupTree, LookupResult } from './types'; export default function walkTree(tree: FlattenedLookupTree, sequence: number[], startIndex: number, index: number): LookupResult | undefined { - const glyphId = sequence[index]; - let subtree = tree[glyphId]; - if (!subtree) { - return undefined; - } + const glyphId = sequence[index]; + let subtree = tree[glyphId]; + if (!subtree) { + return undefined; + } - let lookup = subtree.lookup; - if (subtree.reverse) { - const reverseLookup = walkReverse(subtree.reverse, sequence, startIndex); - - if ( - (!lookup && reverseLookup) || - ( - reverseLookup && lookup && ( - lookup.index > reverseLookup.index || - (lookup.index === reverseLookup.index && lookup.subIndex > reverseLookup.subIndex) - ) - ) - ) { - lookup = reverseLookup; - } - } - - if (++index >= sequence.length || !subtree.forward) { - return lookup; - } - - const forwardLookup = walkTree(subtree.forward, sequence, startIndex, index); + let lookup = subtree.lookup; + if (subtree.reverse) { + const reverseLookup = walkReverse(subtree.reverse, sequence, startIndex); if ( - (!lookup && forwardLookup) || - ( - forwardLookup && lookup && ( - lookup.index > forwardLookup.index || - (lookup.index === forwardLookup.index && lookup.subIndex > forwardLookup.subIndex) - ) + (!lookup && reverseLookup) || + ( + reverseLookup && lookup && ( + lookup.index > reverseLookup.index || + (lookup.index === reverseLookup.index && lookup.subIndex > reverseLookup.subIndex) ) + ) ) { - lookup = forwardLookup; + lookup = reverseLookup; } + } + if (++index >= sequence.length || !subtree.forward) { return lookup; + } + + const forwardLookup = walkTree(subtree.forward, sequence, startIndex, index); + + if ( + (!lookup && forwardLookup) || + ( + forwardLookup && lookup && ( + lookup.index > forwardLookup.index || + (lookup.index === forwardLookup.index && lookup.subIndex > forwardLookup.subIndex) + ) + ) + ) { + lookup = forwardLookup; + } + + return lookup; } function walkReverse(tree: FlattenedLookupTree, sequence: number[], index: number): LookupResult | undefined { - let subtree = tree[sequence[--index]]; - let lookup: LookupResult | undefined = subtree && subtree.lookup; - while (subtree) { - if ( - (!lookup && subtree.lookup) || - (subtree.lookup && lookup && lookup.index > subtree.lookup.index) - ) { - lookup = subtree.lookup; - } - - if (--index < 0 || !subtree.reverse) { - break; - } - - subtree = subtree.reverse[sequence[index]]; + let subtree = tree[sequence[--index]]; + let lookup: LookupResult | undefined = subtree && subtree.lookup; + while (subtree) { + if ( + (!lookup && subtree.lookup) || + (subtree.lookup && lookup && lookup.index > subtree.lookup.index) + ) { + lookup = subtree.lookup; } - return lookup; + if (--index < 0 || !subtree.reverse) { + break; + } + + subtree = subtree.reverse[sequence[index]]; + } + + return lookup; } From f84384c83ac67f29637274adb4eef7c39c9e0808 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 3 Jan 2026 09:31:45 -0800 Subject: [PATCH 17/38] Fix lint --- .../src/fontLigatures/flatten.ts | 10 ++-- .../src/fontLigatures/index.test.ts | 26 +++++----- .../src/fontLigatures/index.ts | 30 +++++------ .../src/fontLigatures/merge.test.ts | 4 +- .../src/fontLigatures/merge.ts | 34 ++++++------- .../src/fontLigatures/mergeRange.ts | 18 ++++--- .../src/fontLigatures/processors/6-1.ts | 10 ++-- .../src/fontLigatures/processors/6-2.ts | 12 ++--- .../src/fontLigatures/processors/6-3.ts | 10 ++-- .../src/fontLigatures/processors/8-1.ts | 14 +++--- .../src/fontLigatures/processors/classDef.ts | 17 +++---- .../src/fontLigatures/processors/coverage.ts | 4 +- .../src/fontLigatures/processors/helper.ts | 46 ++++++++--------- .../fontLigatures/processors/substitution.ts | 6 +-- .../src/fontLigatures/tables.ts | 50 +++++++++---------- .../src/fontLigatures/types.ts | 38 +++++++------- .../addon-ligatures/src/fontLigatures/walk.ts | 10 ++-- 17 files changed, 170 insertions(+), 169 deletions(-) diff --git a/addons/addon-ligatures/src/fontLigatures/flatten.ts b/addons/addon-ligatures/src/fontLigatures/flatten.ts index f911b986a..843f9acf4 100644 --- a/addons/addon-ligatures/src/fontLigatures/flatten.ts +++ b/addons/addon-ligatures/src/fontLigatures/flatten.ts @@ -1,7 +1,7 @@ -import { LookupTree, FlattenedLookupTree, LookupTreeEntry, FlattenedLookupTreeEntry } from './types'; +import { ILookupTree, IFlattenedLookupTree, ILookupTreeEntry, IFlattenedLookupTreeEntry } from './types'; -export default function flatten(tree: LookupTree): FlattenedLookupTree { - const result: FlattenedLookupTree = {}; +export default function flatten(tree: ILookupTree): IFlattenedLookupTree { + const result: IFlattenedLookupTree = {}; for (const [glyphId, entry] of Object.entries(tree.individual)) { result[glyphId] = flattenEntry(entry); } @@ -16,8 +16,8 @@ export default function flatten(tree: LookupTree): FlattenedLookupTree { return result; } -function flattenEntry(entry: LookupTreeEntry): FlattenedLookupTreeEntry { - const result: FlattenedLookupTreeEntry = {}; +function flattenEntry(entry: ILookupTreeEntry): IFlattenedLookupTreeEntry { + const result: IFlattenedLookupTreeEntry = {}; if (entry.forward) { result.forward = flatten(entry.forward); diff --git a/addons/addon-ligatures/src/fontLigatures/index.test.ts b/addons/addon-ligatures/src/fontLigatures/index.test.ts index d8af5dfac..7f4949b12 100644 --- a/addons/addon-ligatures/src/fontLigatures/index.test.ts +++ b/addons/addon-ligatures/src/fontLigatures/index.test.ts @@ -8,31 +8,31 @@ import * as fs from 'fs'; import { assert } from 'chai'; import { loadBuffer } from './index'; -interface Font { - findLigatures(text: string): { outputGlyphs: number[]; contextRanges: [number, number][] }; +interface IFont { + findLigatures(text: string): { outputGlyphs: number[], contextRanges: [number, number][] }; findLigatureRanges(text: string): [number, number][]; } -interface TestCase { +interface ITestCase { font: string; input: string; glyphs: number[]; ranges: [number, number][]; } -const fira = (input: string, glyphs: number[], ranges: [number, number][]): TestCase => +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][]): TestCase => +const iosevka = (input: string, glyphs: number[], ranges: [number, number][]): ITestCase => ({ font: 'Iosevka', input, glyphs, ranges }); -const monoid = (input: string, glyphs: number[], ranges: [number, number][]): TestCase => +const monoid = (input: string, glyphs: number[], ranges: [number, number][]): ITestCase => ({ font: 'Monoid', input, glyphs, ranges }); -const ubuntu = (input: string, glyphs: number[], ranges: [number, number][]): TestCase => +const ubuntu = (input: string, glyphs: number[], ranges: [number, number][]): ITestCase => ({ font: 'Ubuntu Mono', input, glyphs, ranges }); -const firaCases: TestCase[] = [ +const firaCases: ITestCase[] = [ fira('abc', [133, 145, 146], []), fira('.=', [1614, 1081], [[0, 2]]), fira('..=', [1614, 1614, 1083], [[0, 3]]), @@ -168,7 +168,7 @@ const firaCases: TestCase[] = [ fira('===>', [1614, 1614, 1486, 1148], [[0, 4]]) ]; -const iosevkaCases: TestCase[] = [ +const iosevkaCases: ITestCase[] = [ iosevka('<-', [31, 3127], [[0, 2]]), iosevka('<--', [31, 3129, 3139], [[0, 3]]), iosevka('<---', [31, 3129, 3150, 3139], [[0, 4]]), @@ -218,7 +218,7 @@ const iosevkaCases: TestCase[] = [ iosevka('!===', [3100, 3085, 3085, 3084], [[0, 4]]) ]; -const monoidCases: TestCase[] = [ +const monoidCases: ITestCase[] = [ monoid('', [779, 779, 628], [[0, 3]]), monoid('<--', [776, 776, 627], [[0, 3]]), @@ -256,7 +256,7 @@ const monoidCases: TestCase[] = [ monoid('__', [763, 764], [[0, 2]]) ]; -const ubuntuCases: TestCase[] = [ +const ubuntuCases: ITestCase[] = [ ubuntu('==>', [32, 32, 33], []) ]; @@ -267,9 +267,9 @@ const fontPaths: Record = { 'Ubuntu Mono': path.join(__dirname, '../../fonts/UbuntuMono-Regular.ttf') }; -const fontCache: Map = new Map(); +const fontCache: Map = new Map(); -function loadFont(fontName: string): Font { +function loadFont(fontName: string): IFont { let font = fontCache.get(fontName); if (!font) { const fontPath = fontPaths[fontName]; diff --git a/addons/addon-ligatures/src/fontLigatures/index.ts b/addons/addon-ligatures/src/fontLigatures/index.ts index f15bd32a6..d65e73bc4 100644 --- a/addons/addon-ligatures/src/fontLigatures/index.ts +++ b/addons/addon-ligatures/src/fontLigatures/index.ts @@ -1,7 +1,7 @@ import * as opentype from 'opentype.js'; import LRUCache = require('lru-cache'); -import { Font, LigatureData, FlattenedLookupTree, LookupTree, Options } from './types'; +import { IFont, ILigatureData, IFlattenedLookupTree, ILookupTree, IOptions } from './types'; import mergeTrees from './merge'; import walkTree from './walk'; import mergeRange from './mergeRange'; @@ -12,19 +12,19 @@ import buildTreeGsubType6Format3 from './processors/6-3'; import buildTreeGsubType8Format1 from './processors/8-1'; import flatten from './flatten'; -class FontImpl implements Font { +class FontImpl implements IFont { private _font: opentype.Font; - private _lookupTrees: { tree: FlattenedLookupTree; processForward: boolean; }[] = []; + private _lookupTrees: { tree: IFlattenedLookupTree, processForward: boolean }[] = []; private _glyphLookups: { [glyphId: string]: number[] } = {}; - private _cache?: LRUCache; + private _cache?: LRUCache; - constructor(font: opentype.Font, options: Required) { + constructor(font: opentype.Font, options: Required) { this._font = font; if (options.cacheSize > 0) { this._cache = new LRUCache({ max: options.cacheSize, - length: ((val: LigatureData | [number, number][], key: string) => key.length) as any + length: ((val: ILigatureData | [number, number][], key: string) => key.length) as any }); } @@ -36,7 +36,7 @@ class FontImpl implements Font { const lookupGroups = allLookups.filter((l: unknown, i: number) => lookupIndices.some(idx => idx === i)); for (const [index, lookup] of lookupGroups.entries()) { - const trees: LookupTree[] = []; + const trees: ILookupTree[] = []; switch (lookup.lookupType) { case 6: for (const [index, table] of lookup.subtables.entries()) { @@ -77,7 +77,7 @@ class FontImpl implements Font { } } - findLigatures(text: string): LigatureData { + public findLigatures(text: string): ILigatureData { const cached = this._cache && this._cache.get(text); if (cached && !Array.isArray(cached)) { return cached; @@ -100,7 +100,7 @@ class FontImpl implements Font { } const result = this._findInternal(glyphIds.slice()); - const finalResult: LigatureData = { + const finalResult: ILigatureData = { inputGlyphs: glyphIds, outputGlyphs: result.sequence, contextRanges: result.ranges @@ -112,7 +112,7 @@ class FontImpl implements Font { return finalResult; } - findLigatureRanges(text: string): [number, number][] { + public findLigatureRanges(text: string): [number, number][] { // Short circuit the process if there are no possible ligatures in the // font if (this._lookupTrees.length === 0) { @@ -137,7 +137,7 @@ class FontImpl implements Font { return result.ranges; } - private _findInternal(sequence: number[]): { sequence: number[]; ranges: [number, number][]; } { + private _findInternal(sequence: number[]): { sequence: number[], ranges: [number, number][] } { const ranges: [number, number][] = []; let nextLookup = this._getNextLookup(sequence, 0); @@ -207,8 +207,8 @@ class FontImpl implements Font { * @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; } = { + 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 @@ -251,7 +251,7 @@ class FontImpl implements Font { * * @param buffer ArrayBuffer of the font to load */ -export function loadBuffer(buffer: ArrayBuffer, options?: Options): Font { +export function loadBuffer(buffer: ArrayBuffer, options?: IOptions): IFont { const font = opentype.parse(buffer); return new FontImpl(font, { cacheSize: 0, @@ -259,4 +259,4 @@ export function loadBuffer(buffer: ArrayBuffer, options?: Options): Font { }); } -export { Font, LigatureData, Options }; +export { IFont as Font, ILigatureData as LigatureData, IOptions as Options }; diff --git a/addons/addon-ligatures/src/fontLigatures/merge.test.ts b/addons/addon-ligatures/src/fontLigatures/merge.test.ts index e56c8dae4..d4e138a8d 100644 --- a/addons/addon-ligatures/src/fontLigatures/merge.test.ts +++ b/addons/addon-ligatures/src/fontLigatures/merge.test.ts @@ -6,7 +6,7 @@ import { assert } from 'chai'; import mergeTrees from './merge'; -interface LookupResult { +interface ILookupResult { contextRange: [number, number]; index: number; subIndex: number; @@ -14,7 +14,7 @@ interface LookupResult { substitutions: number[]; } -function lookup(substitutionGlyph: number, index?: number, subIndex?: number): LookupResult { +function lookup(substitutionGlyph: number, index?: number, subIndex?: number): ILookupResult { return { contextRange: [0, 1], index: index || 0, diff --git a/addons/addon-ligatures/src/fontLigatures/merge.ts b/addons/addon-ligatures/src/fontLigatures/merge.ts index 13c877335..3e1270480 100644 --- a/addons/addon-ligatures/src/fontLigatures/merge.ts +++ b/addons/addon-ligatures/src/fontLigatures/merge.ts @@ -1,4 +1,4 @@ -import { LookupTree, LookupTreeEntry } from './types'; +import { ILookupTree, ILookupTreeEntry } from './types'; /** * Merges the provided trees into a single lookup tree. When conflicting lookups @@ -8,8 +8,8 @@ import { LookupTree, LookupTreeEntry } from './types'; * @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: LookupTree[]): LookupTree { - const result: LookupTree = { +export default function mergeTrees(trees: ILookupTree[]): ILookupTree { + const result: ILookupTree = { individual: {}, range: [] }; @@ -27,7 +27,7 @@ export default function mergeTrees(trees: LookupTree[]): LookupTree { * @param mainTree The tree where the values should be merged * @param mergeTree The tree to be merged into the mainTree */ -function mergeSubtree(mainTree: LookupTree, mergeTree: LookupTree): void { +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 @@ -91,7 +91,7 @@ function mergeSubtree(mainTree: LookupTree, mergeTree: LookupTree): void { mainTree.range.splice(index, 1); index--; - const entryToMerge: LookupTreeEntry = cloneEntry(resultEntry); + const entryToMerge: ILookupTreeEntry = cloneEntry(resultEntry); if (Array.isArray(overlap.both)) { mainTree.range.push({ range: overlap.both, @@ -192,7 +192,7 @@ function mergeSubtree(mainTree: LookupTree, mergeTree: LookupTree): void { * @param mainTree The entry where the values should be merged * @param mergeTree The entry to merge into the mainTree */ -function mergeTreeEntry(mainTree: LookupTreeEntry, mergeTree: LookupTreeEntry): void { +function mergeTreeEntry(mainTree: ILookupTreeEntry, mergeTree: ILookupTreeEntry): void { if ( mergeTree.lookup && ( !mainTree.lookup || @@ -220,7 +220,7 @@ function mergeTreeEntry(mainTree: LookupTreeEntry, mergeTree: LookupTreeEntry): } } -interface Overlap { +interface IOverlap { first: (number | [number, number])[]; second: (number | [number, number])[]; both: number | [number, number] | null; @@ -233,8 +233,8 @@ interface Overlap { * @param first First range * @param second Second range */ -function getRangeOverlap(first: [number, number], second: [number, number]): Overlap { - const result: Overlap = { +function getRangeOverlap(first: [number, number], second: [number, number]): IOverlap { + const result: IOverlap = { first: [], second: [], both: null @@ -280,7 +280,7 @@ function getRangeOverlap(first: [number, number], second: [number, number]): Ove * @param first Individual glyph * @param second Range */ -function getIndividualOverlap(first: number, second: [number, number]): Overlap { +function getIndividualOverlap(first: number, second: [number, number]): IOverlap { // Disjoint if (first < second[0] || first > second[1]) { return { @@ -290,7 +290,7 @@ function getIndividualOverlap(first: number, second: [number, number]): Overlap }; } - const result: Overlap = { + const result: IOverlap = { first: [], second: [], both: first @@ -317,9 +317,9 @@ function getIndividualOverlap(first: number, second: [number, number]): Overlap function rangeOrIndividual(start: number, end: number): number | [number, number] { if (end - start === 1) { return start; - } else { - return [start, end]; } + return [start, end]; + } /** @@ -327,8 +327,8 @@ function rangeOrIndividual(start: number, end: number): number | [number, number * * @param entry Lookup tree entry to clone */ -function cloneEntry(entry: LookupTreeEntry): LookupTreeEntry { - const result: LookupTreeEntry = {}; +function cloneEntry(entry: ILookupTreeEntry): ILookupTreeEntry { + const result: ILookupTreeEntry = {}; if (entry.forward) { result.forward = cloneTree(entry.forward); @@ -356,8 +356,8 @@ function cloneEntry(entry: LookupTreeEntry): LookupTreeEntry { * * @param tree Lookup tree to clone */ -function cloneTree(tree: LookupTree): LookupTree { - const individual: { [glyphId: string]: LookupTreeEntry; } = {}; +function cloneTree(tree: ILookupTree): ILookupTree { + const individual: { [glyphId: string]: ILookupTreeEntry } = {}; for (const [glyphId, entry] of Object.entries(tree.individual)) { individual[glyphId] = cloneEntry(entry); } diff --git a/addons/addon-ligatures/src/fontLigatures/mergeRange.ts b/addons/addon-ligatures/src/fontLigatures/mergeRange.ts index 2b9ac6b7b..ec5305083 100644 --- a/addons/addon-ligatures/src/fontLigatures/mergeRange.ts +++ b/addons/addon-ligatures/src/fontLigatures/mergeRange.ts @@ -16,12 +16,14 @@ export default function mergeRange(ranges: [number, number][], newRangeStart: nu // Case 1: New range is before the search range ranges.splice(i, 0, [newRangeStart, newRangeEnd]); return ranges; - } else if (newRangeEnd <= range[1]) { + } + 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; - } else if (newRangeStart < range[1]) { + } + 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]); @@ -36,19 +38,19 @@ export default function mergeRange(ranges: [number, number][], newRangeStart: nu // reach the current one ranges[i - 1][1] = newRangeEnd; return ranges; - } else if (newRangeEnd <= range[1]) { + } + 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; - } else { - // Case 7: New range extends from previous range past the - // end of the current range - ranges.splice(i, 1); - i--; } + // Case 7: New range extends from previous range past the + // end of the current range + ranges.splice(i, 1); + i--; } } diff --git a/addons/addon-ligatures/src/fontLigatures/processors/6-1.ts b/addons/addon-ligatures/src/fontLigatures/processors/6-1.ts index dc2e8917d..c7f03b53b 100644 --- a/addons/addon-ligatures/src/fontLigatures/processors/6-1.ts +++ b/addons/addon-ligatures/src/fontLigatures/processors/6-1.ts @@ -1,8 +1,8 @@ import { ChainingContextualSubstitutionTable, Lookup } from '../tables'; -import { LookupTree } from '../types'; +import { ILookupTree } from '../types'; import { listGlyphsByIndex } from './coverage'; -import { processInputPosition, processLookaheadPosition, processBacktrackPosition, getInputTree, EntryMeta } from './helper'; +import { processInputPosition, processLookaheadPosition, processBacktrackPosition, getInputTree, IEntryMeta } from './helper'; /** * Build lookup tree for GSUB lookup table 6, format 1. @@ -12,8 +12,8 @@ import { processInputPosition, processLookaheadPosition, processBacktrackPositio * @param lookups List of lookup tables * @param tableIndex Index of this table in the overall lookup */ -export default function buildTree(table: ChainingContextualSubstitutionTable.Format1, lookups: Lookup[], tableIndex: number): LookupTree { - const result: LookupTree = { +export default function buildTree(table: ChainingContextualSubstitutionTable.IFormat1, lookups: Lookup[], tableIndex: number): ILookupTree { + const result: ILookupTree = { individual: {}, range: [] }; @@ -29,7 +29,7 @@ export default function buildTree(table: ChainingContextualSubstitutionTable.For } for (const [subIndex, subTable] of chainRuleSet.entries()) { - let currentEntries: EntryMeta[] = getInputTree( + let currentEntries: IEntryMeta[] = getInputTree( result, subTable.lookupRecords, lookups, diff --git a/addons/addon-ligatures/src/fontLigatures/processors/6-2.ts b/addons/addon-ligatures/src/fontLigatures/processors/6-2.ts index f0f9245ce..f39682423 100644 --- a/addons/addon-ligatures/src/fontLigatures/processors/6-2.ts +++ b/addons/addon-ligatures/src/fontLigatures/processors/6-2.ts @@ -1,10 +1,10 @@ import { ChainingContextualSubstitutionTable, Lookup } from '../tables'; -import { LookupTree } from '../types'; +import { ILookupTree } from '../types'; import mergeTrees from '../merge'; import { listGlyphsByIndex } from './coverage'; import getGlyphClass, { listClassGlyphs } from './classDef'; -import { processInputPosition, processLookaheadPosition, processBacktrackPosition, getInputTree, EntryMeta } from './helper'; +import { processInputPosition, processLookaheadPosition, processBacktrackPosition, getInputTree, IEntryMeta } from './helper'; /** * Build lookup tree for GSUB lookup table 6, format 2. @@ -14,8 +14,8 @@ import { processInputPosition, processLookaheadPosition, processBacktrackPositio * @param lookups List of lookup tables * @param tableIndex Index of this table in the overall lookup */ -export default function buildTree(table: ChainingContextualSubstitutionTable.Format2, lookups: Lookup[], tableIndex: number): LookupTree { - const results: LookupTree[] = []; +export default function buildTree(table: ChainingContextualSubstitutionTable.IFormat2, lookups: Lookup[], tableIndex: number): ILookupTree { + const results: ILookupTree[] = []; const firstGlyphs = listGlyphsByIndex(table.coverage); @@ -35,12 +35,12 @@ export default function buildTree(table: ChainingContextualSubstitutionTable.For } for (const [subIndex, subTable] of classSet.entries()) { - const result: LookupTree = { + const result: ILookupTree = { individual: {}, range: [] }; - let currentEntries: EntryMeta[] = getInputTree( + let currentEntries: IEntryMeta[] = getInputTree( result, subTable.lookupRecords, lookups, diff --git a/addons/addon-ligatures/src/fontLigatures/processors/6-3.ts b/addons/addon-ligatures/src/fontLigatures/processors/6-3.ts index 07e4681fe..334e6a566 100644 --- a/addons/addon-ligatures/src/fontLigatures/processors/6-3.ts +++ b/addons/addon-ligatures/src/fontLigatures/processors/6-3.ts @@ -1,8 +1,8 @@ import { ChainingContextualSubstitutionTable, Lookup } from '../tables'; -import { LookupTree } from '../types'; +import { ILookupTree } from '../types'; import { listGlyphsByIndex } from './coverage'; -import { processInputPosition, processLookaheadPosition, processBacktrackPosition, getInputTree, EntryMeta } from './helper'; +import { processInputPosition, processLookaheadPosition, processBacktrackPosition, getInputTree, IEntryMeta } from './helper'; /** * Build lookup tree for GSUB lookup table 6, format 3. @@ -12,8 +12,8 @@ import { processInputPosition, processLookaheadPosition, processBacktrackPositio * @param lookups List of lookup tables * @param tableIndex Index of this table in the overall lookup */ -export default function buildTree(table: ChainingContextualSubstitutionTable.Format3, lookups: Lookup[], tableIndex: number): LookupTree { - const result: LookupTree = { +export default function buildTree(table: ChainingContextualSubstitutionTable.IFormat3, lookups: Lookup[], tableIndex: number): ILookupTree { + const result: ILookupTree = { individual: {}, range: [] }; @@ -21,7 +21,7 @@ export default function buildTree(table: ChainingContextualSubstitutionTable.For const firstGlyphs = listGlyphsByIndex(table.inputCoverage[0]); for (const { glyphId } of firstGlyphs) { - let currentEntries: EntryMeta[] = getInputTree( + let currentEntries: IEntryMeta[] = getInputTree( result, table.lookupRecords, lookups, diff --git a/addons/addon-ligatures/src/fontLigatures/processors/8-1.ts b/addons/addon-ligatures/src/fontLigatures/processors/8-1.ts index 8f127f6b9..536b38c74 100644 --- a/addons/addon-ligatures/src/fontLigatures/processors/8-1.ts +++ b/addons/addon-ligatures/src/fontLigatures/processors/8-1.ts @@ -1,8 +1,8 @@ -import { ReverseChainingContextualSingleSubstitutionTable } from '../tables'; -import { LookupTree, LookupTreeEntry } from '../types'; +import { IReverseChainingContextualSingleSubstitutionTable } from '../tables'; +import { ILookupTree, ILookupTreeEntry } from '../types'; import { listGlyphsByIndex } from './coverage'; -import { processLookaheadPosition, processBacktrackPosition, EntryMeta } from './helper'; +import { processLookaheadPosition, processBacktrackPosition, IEntryMeta } from './helper'; /** * Build lookup tree for GSUB lookup table 8, format 1. @@ -11,8 +11,8 @@ import { processLookaheadPosition, processBacktrackPosition, EntryMeta } from '. * @param table JSON representation of the table * @param tableIndex Index of this table in the overall lookup */ -export default function buildTree(table: ReverseChainingContextualSingleSubstitutionTable, tableIndex: number): LookupTree { - const result: LookupTree = { +export default function buildTree(table: IReverseChainingContextualSingleSubstitutionTable, tableIndex: number): ILookupTree { + const result: ILookupTree = { individual: {}, range: [] }; @@ -20,7 +20,7 @@ export default function buildTree(table: ReverseChainingContextualSingleSubstitu const glyphs = listGlyphsByIndex(table.coverage); for (const { glyphId, index } of glyphs) { - const initialEntry: LookupTreeEntry = {}; + const initialEntry: ILookupTreeEntry = {}; if (Array.isArray(glyphId)) { result.range.push({ entry: initialEntry, @@ -30,7 +30,7 @@ export default function buildTree(table: ReverseChainingContextualSingleSubstitu result.individual[glyphId] = initialEntry; } - let currentEntries: EntryMeta[] = [{ + let currentEntries: IEntryMeta[] = [{ entry: initialEntry, substitutions: [table.substitutes[index]] }]; diff --git a/addons/addon-ligatures/src/fontLigatures/processors/classDef.ts b/addons/addon-ligatures/src/fontLigatures/processors/classDef.ts index bcf98ea69..5dac85a15 100644 --- a/addons/addon-ligatures/src/fontLigatures/processors/classDef.ts +++ b/addons/addon-ligatures/src/fontLigatures/processors/classDef.ts @@ -13,21 +13,20 @@ export default function getGlyphClass(table: ClassDefTable, glyphId: number | [n case 2: if (Array.isArray(glyphId)) { return getRangeGlyphClass(table, glyphId); - } else { - return new Map([[ - glyphId, - getIndividualGlyphClass(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.Format2, glyphId: [number, number]): Map { - let classStart: number = glyphId[0]; - let currentClass: number | null = getIndividualGlyphClass(table, classStart); +function getRangeGlyphClass(table: ClassDefTable.IFormat2, glyphId: [number, number]): Map { + 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>(); @@ -53,7 +52,7 @@ function getRangeGlyphClass(table: ClassDefTable.Format2, glyphId: [number, numb return result; } -function getIndividualGlyphClass(table: ClassDefTable.Format2, glyphId: number): number | null { +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; diff --git a/addons/addon-ligatures/src/fontLigatures/processors/coverage.ts b/addons/addon-ligatures/src/fontLigatures/processors/coverage.ts index 02d518d08..b287a2f72 100644 --- a/addons/addon-ligatures/src/fontLigatures/processors/coverage.ts +++ b/addons/addon-ligatures/src/fontLigatures/processors/coverage.ts @@ -25,12 +25,12 @@ export default function getCoverageGlyphIndex(table: CoverageTable, glyphId: num } } -export function listGlyphsByIndex(table: CoverageTable): { glyphId: number | [number, number]; index: number; }[] { +export function listGlyphsByIndex(table: CoverageTable): { glyphId: number | [number, number], index: number }[] { switch (table.format) { case 1: return table.glyphs.map((glyphId, index) => ({ glyphId, index })); case 2: - let results: { glyphId: number | [number, number]; index: number; }[] = []; + const results: { glyphId: number | [number, number], index: number }[] = []; for (const [index, range] of table.ranges.entries()) { if (range.end === range.start) { results.push({ glyphId: range.start, index }); diff --git a/addons/addon-ligatures/src/fontLigatures/processors/helper.ts b/addons/addon-ligatures/src/fontLigatures/processors/helper.ts index c15c0c920..b05e1e2e0 100644 --- a/addons/addon-ligatures/src/fontLigatures/processors/helper.ts +++ b/addons/addon-ligatures/src/fontLigatures/processors/helper.ts @@ -1,21 +1,21 @@ -import { LookupTreeEntry, LookupTree } from '../types'; -import { SubstitutionLookupRecord, Lookup } from '../tables'; +import { ILookupTreeEntry, ILookupTree } from '../types'; +import { ISubstitutionLookupRecord, Lookup } from '../tables'; import { getIndividualSubstitutionGlyph, getRangeSubstitutionGlyphs } from './substitution'; -export interface EntryMeta { - entry: LookupTreeEntry; +export interface IEntryMeta { + entry: ILookupTreeEntry; substitutions: (number | null)[]; } export function processInputPosition( glyphs: (number | [number, number])[], position: number, - currentEntries: EntryMeta[], - lookupRecords: SubstitutionLookupRecord[], + currentEntries: IEntryMeta[], + lookupRecords: ISubstitutionLookupRecord[], lookups: Lookup[] -): EntryMeta[] { - const nextEntries: EntryMeta[] = []; +): IEntryMeta[] { + const nextEntries: IEntryMeta[] = []; for (const currentEntry of currentEntries) { currentEntry.entry.forward = { individual: {}, @@ -40,12 +40,12 @@ export function processInputPosition( export function processLookaheadPosition( glyphs: (number | [number, number])[], - currentEntries: EntryMeta[] -): EntryMeta[] { - const nextEntries: EntryMeta[] = []; + currentEntries: IEntryMeta[] +): IEntryMeta[] { + const nextEntries: IEntryMeta[] = []; for (const currentEntry of currentEntries) { for (const glyph of glyphs) { - const entry: LookupTreeEntry = {}; + const entry: ILookupTreeEntry = {}; if (!currentEntry.entry.forward) { currentEntry.entry.forward = { individual: {}, @@ -73,12 +73,12 @@ export function processLookaheadPosition( export function processBacktrackPosition( glyphs: (number | [number, number])[], - currentEntries: EntryMeta[] -): EntryMeta[] { - const nextEntries: EntryMeta[] = []; + currentEntries: IEntryMeta[] +): IEntryMeta[] { + const nextEntries: IEntryMeta[] = []; for (const currentEntry of currentEntries) { for (const glyph of glyphs) { - const entry: LookupTreeEntry = {}; + const entry: ILookupTreeEntry = {}; if (!currentEntry.entry.reverse) { currentEntry.entry.reverse = { individual: {}, @@ -104,8 +104,8 @@ export function processBacktrackPosition( return nextEntries; } -export function getInputTree(tree: LookupTree, substitutions: SubstitutionLookupRecord[], lookups: Lookup[], inputIndex: number, glyphId: number | [number, number]): { entry: LookupTreeEntry; substitution: number | null; }[] { - const result: { entry: LookupTreeEntry; substitution: number | null; }[] = []; +export function getInputTree(tree: ILookupTree, substitutions: ISubstitutionLookupRecord[], lookups: Lookup[], inputIndex: number, glyphId: number | [number, number]): { entry: ILookupTreeEntry, substitution: number | null }[] { + const result: { entry: ILookupTreeEntry, substitution: number | null }[] = []; if (!Array.isArray(glyphId)) { tree.individual[glyphId] = {}; result.push({ @@ -115,7 +115,7 @@ export function getInputTree(tree: LookupTree, substitutions: SubstitutionLookup } else { const subs = getSubstitutionAtPositionRange(substitutions, lookups, inputIndex, glyphId); for (const [range, substitution] of subs) { - const entry: LookupTreeEntry = {}; + const entry: ILookupTreeEntry = {}; if (Array.isArray(range)) { tree.range.push({ range, entry }); } else { @@ -128,9 +128,9 @@ export function getInputTree(tree: LookupTree, substitutions: SubstitutionLookup return result; } -function getSubstitutionAtPositionRange(substitutions: SubstitutionLookupRecord[], lookups: Lookup[], index: number, range: [number, number]): Map { +function getSubstitutionAtPositionRange(substitutions: ISubstitutionLookupRecord[], lookups: Lookup[], index: number, range: [number, number]): Map { for (const substitution of substitutions.filter(s => s.sequenceIndex === index)) { - for (const substitutionTable of (lookups[substitution.lookupListIndex] as Lookup.Type1).subtables) { + for (const substitutionTable of (lookups[substitution.lookupListIndex] as Lookup.IType1).subtables) { const sub = getRangeSubstitutionGlyphs( substitutionTable, range @@ -145,9 +145,9 @@ function getSubstitutionAtPositionRange(substitutions: SubstitutionLookupRecord[ return new Map([[range, null]]); } -function getSubstitutionAtPosition(substitutions: SubstitutionLookupRecord[], lookups: Lookup[], index: number, glyphId: number): number | null { +function getSubstitutionAtPosition(substitutions: ISubstitutionLookupRecord[], lookups: Lookup[], index: number, glyphId: number): number | null { for (const substitution of substitutions.filter(s => s.sequenceIndex === index)) { - for (const substitutionTable of (lookups[substitution.lookupListIndex] as Lookup.Type1).subtables) { + for (const substitutionTable of (lookups[substitution.lookupListIndex] as Lookup.IType1).subtables) { const sub = getIndividualSubstitutionGlyph( substitutionTable, glyphId diff --git a/addons/addon-ligatures/src/fontLigatures/processors/substitution.ts b/addons/addon-ligatures/src/fontLigatures/processors/substitution.ts index e277bd491..5eae24761 100644 --- a/addons/addon-ligatures/src/fontLigatures/processors/substitution.ts +++ b/addons/addon-ligatures/src/fontLigatures/processors/substitution.ts @@ -10,8 +10,8 @@ import getCoverageGlyphIndex from './coverage'; * @param glyphId The index of the glpyh to find substitutions for */ export function getRangeSubstitutionGlyphs(table: SubstitutionTable, glyphId: [number, number]): Map<[number, number] | number, number | null> { - let replacementStart: number = glyphId[0]; - let currentReplacement: number | null = getIndividualSubstitutionGlyph(table, replacementStart); + const replacementStart: number = glyphId[0]; + const currentReplacement: number | null = getIndividualSubstitutionGlyph(table, replacementStart); let search: number = glyphId[0] + 1; const result = new Map<[number, number] | number, number | null>(); @@ -54,7 +54,7 @@ export function getIndividualSubstitutionGlyph(table: SubstitutionTable, glyphId return (glyphId + table.deltaGlyphId) % (2 ** 16); // https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#12-single-substitution-format-2 case 2: - // tslint:disable-next-line + // eslint-disable-next-line eqeqeq return table.substitute[coverageIndex] != null ? table.substitute[coverageIndex] : null; diff --git a/addons/addon-ligatures/src/fontLigatures/tables.ts b/addons/addon-ligatures/src/fontLigatures/tables.ts index ec6f36046..a2433dbfa 100644 --- a/addons/addon-ligatures/src/fontLigatures/tables.ts +++ b/addons/addon-ligatures/src/fontLigatures/tables.ts @@ -1,26 +1,26 @@ -export type SubstitutionTable = SubstitutionTable.Format1 | SubstitutionTable.Format2; +export type SubstitutionTable = SubstitutionTable.IFormat1 | SubstitutionTable.IFormat2; export namespace SubstitutionTable { - export interface Format1 { + export interface IFormat1 { substFormat: 1; coverage: CoverageTable; deltaGlyphId: number; } - export interface Format2 { + export interface IFormat2 { substFormat: 2; coverage: CoverageTable; substitute: number[]; } } -export type CoverageTable = CoverageTable.Format1 | CoverageTable.Format2; +export type CoverageTable = CoverageTable.IFormat1 | CoverageTable.IFormat2; export namespace CoverageTable { - export interface Format1 { + export interface IFormat1 { format: 1; glyphs: number[]; } - export interface Format2 { + export interface IFormat2 { format: 2; ranges: { start: number; @@ -30,34 +30,34 @@ export namespace CoverageTable { } } -export type ChainingContextualSubstitutionTable = ChainingContextualSubstitutionTable.Format1 | - ChainingContextualSubstitutionTable.Format2 | ChainingContextualSubstitutionTable.Format3; +export type ChainingContextualSubstitutionTable = ChainingContextualSubstitutionTable.IFormat1 | + ChainingContextualSubstitutionTable.IFormat2 | ChainingContextualSubstitutionTable.IFormat3; export namespace ChainingContextualSubstitutionTable { - export interface Format1 { + export interface IFormat1 { substFormat: 1; coverage: CoverageTable; chainRuleSets: ChainSubRuleTable[][]; } - export interface Format2 { + export interface IFormat2 { substFormat: 2; coverage: CoverageTable; backtrackClassDef: ClassDefTable; inputClassDef: ClassDefTable; lookaheadClassDef: ClassDefTable; - chainClassSet: (null | ChainSubClassRuleTable[])[]; + chainClassSet: (null | IChainSubClassRuleTable[])[]; } - export interface Format3 { + export interface IFormat3 { substFormat: 3; backtrackCoverage: CoverageTable[]; inputCoverage: CoverageTable[]; lookaheadCoverage: CoverageTable[]; - lookupRecords: SubstitutionLookupRecord[]; + lookupRecords: ISubstitutionLookupRecord[]; } } -export interface ReverseChainingContextualSingleSubstitutionTable { +export interface IReverseChainingContextualSingleSubstitutionTable { substFormat: 1; coverage: CoverageTable; backtrackCoverage: CoverageTable[]; @@ -65,9 +65,9 @@ export interface ReverseChainingContextualSingleSubstitutionTable { substitutes: number[]; } -export type ClassDefTable = ClassDefTable.Format2; +export type ClassDefTable = ClassDefTable.IFormat2; export namespace ClassDefTable { - export interface Format2 { + export interface IFormat2 { format: 2; ranges: { start: number; @@ -77,36 +77,36 @@ export namespace ClassDefTable { } } -export interface SubstitutionLookupRecord { +export interface ISubstitutionLookupRecord { sequenceIndex: number; lookupListIndex: number; } -export type ChainSubRuleTable = ChainSubClassRuleTable; -export interface ChainSubClassRuleTable { +export type ChainSubRuleTable = IChainSubClassRuleTable; +export interface IChainSubClassRuleTable { backtrack: number[]; input: number[]; lookahead: number[]; - lookupRecords: SubstitutionLookupRecord[]; + lookupRecords: ISubstitutionLookupRecord[]; } -export type Lookup = Lookup.Type1 | Lookup.Type6 | Lookup.Type8; +export type Lookup = Lookup.IType1 | Lookup.IType6 | Lookup.IType8; export namespace Lookup { - export interface Type1 { + export interface IType1 { lookupType: 1; lookupFlag: number; subtables: SubstitutionTable[]; } - export interface Type6 { + export interface IType6 { lookupType: 6; lookupFlag: number; subtables: ChainingContextualSubstitutionTable[]; } - export interface Type8 { + export interface IType8 { lookupType: 8; lookupFlag: number; - subtables: ReverseChainingContextualSingleSubstitutionTable[]; + subtables: IReverseChainingContextualSingleSubstitutionTable[]; } } diff --git a/addons/addon-ligatures/src/fontLigatures/types.ts b/addons/addon-ligatures/src/fontLigatures/types.ts index 789fed380..4d6f44a0f 100644 --- a/addons/addon-ligatures/src/fontLigatures/types.ts +++ b/addons/addon-ligatures/src/fontLigatures/types.ts @@ -1,4 +1,4 @@ -export interface SubstitutionResult { +export interface ISubstitutionResult { index: number; contextRange: [number, number]; } @@ -6,7 +6,7 @@ export interface SubstitutionResult { /** * Information about ligatures found in a sequence of text */ -export interface LigatureData { +export interface ILigatureData { /** * The list of font glyphs in the input text. */ @@ -25,14 +25,14 @@ export interface LigatureData { contextRanges: [number, number][]; } -export interface Font { +export interface IFont { /** * Scans the provided text for font ligatures, returning an object with * metadata about the text and any ligatures found. * * @param text String to search for ligatures */ - findLigatures(text: string): LigatureData; + findLigatures(text: string): ILigatureData; /** * Scans the provided text for font ligatures, returning an array of ranges @@ -43,7 +43,7 @@ export interface Font { findLigatureRanges(text: string): [number, number][]; } -export interface Options { +export interface IOptions { /** * Optional size of previous results to store, measured in total number of * characters from input strings. Defaults to no cache (0) @@ -51,23 +51,23 @@ export interface Options { cacheSize?: number; } -export interface LookupTree { +export interface ILookupTree { individual: { - [glyphId: string]: LookupTreeEntry; + [glyphId: string]: ILookupTreeEntry; }; range: { range: [number, number]; - entry: LookupTreeEntry; + entry: ILookupTreeEntry; }[]; } -export interface LookupTreeEntry { - lookup?: LookupResult; - forward?: LookupTree; - reverse?: LookupTree; +export interface ILookupTreeEntry { + lookup?: ILookupResult; + forward?: ILookupTree; + reverse?: ILookupTree; } -export interface LookupResult { +export interface ILookupResult { substitutions: (number | null)[]; length: number; index: number; @@ -75,12 +75,12 @@ export interface LookupResult { contextRange: [number, number]; } -export interface FlattenedLookupTree { - [glyphId: string]: FlattenedLookupTreeEntry; +export interface IFlattenedLookupTree { + [glyphId: string]: IFlattenedLookupTreeEntry; } -export interface FlattenedLookupTreeEntry { - lookup?: LookupResult; - forward?: FlattenedLookupTree; - reverse?: FlattenedLookupTree; +export interface IFlattenedLookupTreeEntry { + lookup?: ILookupResult; + forward?: IFlattenedLookupTree; + reverse?: IFlattenedLookupTree; } diff --git a/addons/addon-ligatures/src/fontLigatures/walk.ts b/addons/addon-ligatures/src/fontLigatures/walk.ts index b122897e6..cfcf12526 100644 --- a/addons/addon-ligatures/src/fontLigatures/walk.ts +++ b/addons/addon-ligatures/src/fontLigatures/walk.ts @@ -1,8 +1,8 @@ -import { FlattenedLookupTree, LookupResult } from './types'; +import { IFlattenedLookupTree, ILookupResult } from './types'; -export default function walkTree(tree: FlattenedLookupTree, sequence: number[], startIndex: number, index: number): LookupResult | undefined { +export default function walkTree(tree: IFlattenedLookupTree, sequence: number[], startIndex: number, index: number): ILookupResult | undefined { const glyphId = sequence[index]; - let subtree = tree[glyphId]; + const subtree = tree[glyphId]; if (!subtree) { return undefined; } @@ -45,9 +45,9 @@ export default function walkTree(tree: FlattenedLookupTree, sequence: number[], return lookup; } -function walkReverse(tree: FlattenedLookupTree, sequence: number[], index: number): LookupResult | undefined { +function walkReverse(tree: IFlattenedLookupTree, sequence: number[], index: number): ILookupResult | undefined { let subtree = tree[sequence[--index]]; - let lookup: LookupResult | undefined = subtree && subtree.lookup; + let lookup: ILookupResult | undefined = subtree && subtree.lookup; while (subtree) { if ( (!lookup && subtree.lookup) || From 917a8ded92a59a490c41dd32f10ab0d2507c4bbb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 3 Jan 2026 17:44:20 +0000 Subject: [PATCH 18/38] Bump qs from 6.14.0 to 6.14.1 Bumps [qs](https://github.com/ljharb/qs) from 6.14.0 to 6.14.1. - [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md) - [Commits](https://github.com/ljharb/qs/compare/v6.14.0...v6.14.1) --- updated-dependencies: - dependency-name: qs dependency-version: 6.14.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e48e512cc..2e55f3de8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -239,6 +239,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -552,6 +553,7 @@ "url": "https://opencollective.com/csstools" } ], + "peer": true, "engines": { "node": ">=18" }, @@ -593,6 +595,7 @@ "url": "https://opencollective.com/csstools" } ], + "peer": true, "engines": { "node": ">=18" } @@ -1894,6 +1897,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.1.tgz", "integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.50.1", "@typescript-eslint/types": "8.50.1", @@ -2338,6 +2342,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2727,6 +2732,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3728,6 +3734,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6745,10 +6752,11 @@ } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" }, @@ -7082,6 +7090,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -7949,6 +7958,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8132,6 +8142,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz", "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", "dev": true, + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -8180,6 +8191,7 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz", "integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==", "dev": true, + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^1.2.0", From 08c49301b7d5a3cca9fbf2dc468e456e725f6455 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 3 Jan 2026 11:19:23 -0800 Subject: [PATCH 19/38] Fix isNode detection when process module is used in webpack Fixes #5341 --- src/common/Platform.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/common/Platform.ts b/src/common/Platform.ts index 4102f20c0..7ea0e9c6d 100644 --- a/src/common/Platform.ts +++ b/src/common/Platform.ts @@ -14,7 +14,9 @@ interface INavigator { declare const navigator: INavigator; declare const process: unknown; -export const isNode = (typeof process !== 'undefined' && 'title' in (process as any)) ? true : false; +// navigator is also checked here because bundling with the process module can cause issues +// otherwise +export const isNode = (typeof process !== 'undefined' && 'title' in (process as any) && typeof navigator === 'undefined') ? true : false; const userAgent = (isNode) ? 'node' : navigator.userAgent; const platform = (isNode) ? 'node' : navigator.platform; From 8712f8067d76d7525373d0431ff030e2e12a676d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 3 Jan 2026 14:01:17 -0800 Subject: [PATCH 20/38] Fix infinite recursion OOM crash in addon-ligatures 1. processLookahead/BacktrackPosition created separate entry objects for each glyph at each position (O(N^M) with N glyphs, M positions). Now all glyphs at a position share one entry object. 2. cloneEntry, cloneTree, flattenEntry, and mergeTreeEntry could infinitely recurse or duplicate work on shared/cyclic references. Added visited/merged tracking maps to cache results. Note that parsing CommitMono locks up the renderer for some time still so there is still a performance issue here, this fixes the OOM though. This was mostly defensive generated code since the codebase is unfamiliar to me. Fixes #5570 --- .../src/fontLigatures/flatten.ts | 17 ++-- .../src/fontLigatures/merge.ts | 55 ++++++++---- .../src/fontLigatures/processors/helper.ts | 84 ++++++++++++------- 3 files changed, 103 insertions(+), 53 deletions(-) diff --git a/addons/addon-ligatures/src/fontLigatures/flatten.ts b/addons/addon-ligatures/src/fontLigatures/flatten.ts index 843f9acf4..967940f67 100644 --- a/addons/addon-ligatures/src/fontLigatures/flatten.ts +++ b/addons/addon-ligatures/src/fontLigatures/flatten.ts @@ -1,13 +1,13 @@ import { ILookupTree, IFlattenedLookupTree, ILookupTreeEntry, IFlattenedLookupTreeEntry } from './types'; -export default function flatten(tree: ILookupTree): IFlattenedLookupTree { +export default function flatten(tree: ILookupTree, visited: Map = new Map()): IFlattenedLookupTree { const result: IFlattenedLookupTree = {}; for (const [glyphId, entry] of Object.entries(tree.individual)) { - result[glyphId] = flattenEntry(entry); + result[glyphId] = flattenEntry(entry, visited); } for (const { range, entry } of tree.range) { - const flattened = flattenEntry(entry); + const flattened = flattenEntry(entry, visited); for (let glyphId = range[0]; glyphId < range[1]; glyphId++) { result[glyphId] = flattened; } @@ -16,15 +16,20 @@ export default function flatten(tree: ILookupTree): IFlattenedLookupTree { return result; } -function flattenEntry(entry: ILookupTreeEntry): IFlattenedLookupTreeEntry { +function flattenEntry(entry: ILookupTreeEntry, visited: Map): IFlattenedLookupTreeEntry { + if (visited.has(entry)) { + return visited.get(entry)!; + } + const result: IFlattenedLookupTreeEntry = {}; + visited.set(entry, result); if (entry.forward) { - result.forward = flatten(entry.forward); + result.forward = flatten(entry.forward, visited); } if (entry.reverse) { - result.reverse = flatten(entry.reverse); + result.reverse = flatten(entry.reverse, visited); } if (entry.lookup) { diff --git a/addons/addon-ligatures/src/fontLigatures/merge.ts b/addons/addon-ligatures/src/fontLigatures/merge.ts index 3e1270480..262ea85ae 100644 --- a/addons/addon-ligatures/src/fontLigatures/merge.ts +++ b/addons/addon-ligatures/src/fontLigatures/merge.ts @@ -14,8 +14,9 @@ export default function mergeTrees(trees: ILookupTree[]): ILookupTree { range: [] }; + const mergedEntries = new WeakMap>(); for (const tree of trees) { - mergeSubtree(result, tree); + mergeSubtree(result, tree, mergedEntries); } return result; @@ -26,15 +27,16 @@ export default function mergeTrees(trees: ILookupTree[]): ILookupTree { * * @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): void { +function mergeSubtree(mainTree: ILookupTree, mergeTree: ILookupTree, mergedEntries: WeakMap>): 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); + mergeTreeEntry(mainTree.individual[glyphId], value, mergedEntries); } else { let matched = false; for (const [index, { range, entry }] of mainTree.range.entries()) { @@ -50,7 +52,7 @@ function mergeSubtree(mainTree: ILookupTree, mergeTree: ILookupTree): void { // If they overlap, we have to split the range and then // merge the overlap mainTree.individual[glyphId] = value; - mergeTreeEntry(mainTree.individual[glyphId], cloneEntry(entry)); + 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 @@ -101,7 +103,7 @@ function mergeSubtree(mainTree: ILookupTree, mergeTree: ILookupTree): void { mainTree.individual[overlap.both] = entryToMerge; } - mergeTreeEntry(entryToMerge, cloneEntry(entry)); + mergeTreeEntry(entryToMerge, cloneEntry(entry), mergedEntries); for (const second of overlap.second) { if (Array.isArray(second)) { @@ -124,7 +126,7 @@ function mergeSubtree(mainTree: ILookupTree, mergeTree: ILookupTree): void { // 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)); + 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 @@ -158,14 +160,14 @@ function mergeSubtree(mainTree: ILookupTree, mergeTree: ILookupTree): void { } // If they overlap, we have to merge the overlap - mergeTreeEntry(mainTree.individual[glyphId], cloneEntry(entry)); + 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)); + mergeTreeEntry(mainTree.individual[glyphId], cloneEntry(entry), mergedEntries); break; } } @@ -191,8 +193,20 @@ function mergeSubtree(mainTree: ILookupTree, mergeTree: ILookupTree): void { * * @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): void { +function mergeTreeEntry(mainTree: ILookupTreeEntry, mergeTree: ILookupTreeEntry, mergedEntries: WeakMap>): 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 || @@ -207,7 +221,7 @@ function mergeTreeEntry(mainTree: ILookupTreeEntry, mergeTree: ILookupTreeEntry) if (!mainTree.forward) { mainTree.forward = mergeTree.forward; } else { - mergeSubtree(mainTree.forward, mergeTree.forward); + mergeSubtree(mainTree.forward, mergeTree.forward, mergedEntries); } } @@ -215,7 +229,7 @@ function mergeTreeEntry(mainTree: ILookupTreeEntry, mergeTree: ILookupTreeEntry) if (!mainTree.reverse) { mainTree.reverse = mergeTree.reverse; } else { - mergeSubtree(mainTree.reverse, mergeTree.reverse); + mergeSubtree(mainTree.reverse, mergeTree.reverse, mergedEntries); } } } @@ -326,16 +340,22 @@ function rangeOrIndividual(start: number, end: number): number | [number, number * 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): ILookupTreeEntry { +function cloneEntry(entry: ILookupTreeEntry, visited: Map = 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); + result.forward = cloneTree(entry.forward, visited); } if (entry.reverse) { - result.reverse = cloneTree(entry.reverse); + result.reverse = cloneTree(entry.reverse, visited); } if (entry.lookup) { @@ -355,18 +375,19 @@ function cloneEntry(entry: ILookupTreeEntry): ILookupTreeEntry { * 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): ILookupTree { +function cloneTree(tree: ILookupTree, visited: Map = new Map()): ILookupTree { const individual: { [glyphId: string]: ILookupTreeEntry } = {}; for (const [glyphId, entry] of Object.entries(tree.individual)) { - individual[glyphId] = cloneEntry(entry); + individual[glyphId] = cloneEntry(entry, visited); } return { individual, range: tree.range.map(({ range, entry }) => ({ range: range.slice() as [number, number], - entry: cloneEntry(entry) + entry: cloneEntry(entry, visited) })) }; } diff --git a/addons/addon-ligatures/src/fontLigatures/processors/helper.ts b/addons/addon-ligatures/src/fontLigatures/processors/helper.ts index b05e1e2e0..3f06d672f 100644 --- a/addons/addon-ligatures/src/fontLigatures/processors/helper.ts +++ b/addons/addon-ligatures/src/fontLigatures/processors/helper.ts @@ -43,29 +43,41 @@ export function processLookaheadPosition( currentEntries: IEntryMeta[] ): IEntryMeta[] { const nextEntries: IEntryMeta[] = []; - for (const currentEntry of currentEntries) { - for (const glyph of glyphs) { - const entry: ILookupTreeEntry = {}; - if (!currentEntry.entry.forward) { - currentEntry.entry.forward = { - individual: {}, - range: [] - }; - } - nextEntries.push({ - entry, - substitutions: currentEntry.substitutions - }); + const processedEntries = new Set(); + for (const currentEntry of currentEntries) { + // Skip if we've already processed this entry object + if (processedEntries.has(currentEntry.entry)) { + continue; + } + processedEntries.add(currentEntry.entry); + + if (!currentEntry.entry.forward) { + currentEntry.entry.forward = { + individual: {}, + range: [] + }; + } + + // All glyphs at this position share ONE entry - lookahead just needs to match, + // all paths lead to the same result + const sharedEntry: ILookupTreeEntry = {}; + + for (const glyph of glyphs) { if (Array.isArray(glyph)) { currentEntry.entry.forward.range.push({ - entry, + entry: sharedEntry, range: glyph }); } else { - currentEntry.entry.forward.individual[glyph] = entry; + currentEntry.entry.forward.individual[glyph] = sharedEntry; } } + + nextEntries.push({ + entry: sharedEntry, + substitutions: currentEntry.substitutions + }); } return nextEntries; @@ -76,29 +88,41 @@ export function processBacktrackPosition( currentEntries: IEntryMeta[] ): IEntryMeta[] { const nextEntries: IEntryMeta[] = []; - for (const currentEntry of currentEntries) { - for (const glyph of glyphs) { - const entry: ILookupTreeEntry = {}; - if (!currentEntry.entry.reverse) { - currentEntry.entry.reverse = { - individual: {}, - range: [] - }; - } - nextEntries.push({ - entry, - substitutions: currentEntry.substitutions - }); + const processedEntries = new Set(); + for (const currentEntry of currentEntries) { + // Skip if we've already processed this entry object + if (processedEntries.has(currentEntry.entry)) { + continue; + } + processedEntries.add(currentEntry.entry); + + if (!currentEntry.entry.reverse) { + currentEntry.entry.reverse = { + individual: {}, + range: [] + }; + } + + // All glyphs at this position share ONE entry - backtrack just needs to match, + // all paths lead to the same result + const sharedEntry: ILookupTreeEntry = {}; + + for (const glyph of glyphs) { if (Array.isArray(glyph)) { currentEntry.entry.reverse.range.push({ - entry, + entry: sharedEntry, range: glyph }); } else { - currentEntry.entry.reverse.individual[glyph] = entry; + currentEntry.entry.reverse.individual[glyph] = sharedEntry; } } + + nextEntries.push({ + entry: sharedEntry, + substitutions: currentEntry.substitutions + }); } return nextEntries; From e2f45b5f4ee64bc6e50061d0169329f0f12a79ad Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 3 Jan 2026 14:10:44 -0800 Subject: [PATCH 21/38] Fix for modern node that includes a navigator.userAgent --- src/common/Platform.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/common/Platform.ts b/src/common/Platform.ts index 7ea0e9c6d..7cfb9f874 100644 --- a/src/common/Platform.ts +++ b/src/common/Platform.ts @@ -14,9 +14,9 @@ interface INavigator { declare const navigator: INavigator; declare const process: unknown; -// navigator is also checked here because bundling with the process module can cause issues -// otherwise -export const isNode = (typeof process !== 'undefined' && 'title' in (process as any) && typeof navigator === 'undefined') ? true : false; +// navigator.userAgent is also checked here because bundling with the process module can cause +// issues otherwise. Note that navigator exists in Node.js 21+ but the userAgent is "Node.js/". +export const isNode = (typeof process !== 'undefined' && 'title' in (process as any) && (typeof navigator === 'undefined' || navigator.userAgent.startsWith('Node.js/'))) ? true : false; const userAgent = (isNode) ? 'node' : navigator.userAgent; const platform = (isNode) ? 'node' : navigator.platform; From d7850af9a2db27adda9f1f4bd2973292417739bd Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 3 Jan 2026 14:19:36 -0800 Subject: [PATCH 22/38] Fix lint --- src/common/Platform.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/common/Platform.ts b/src/common/Platform.ts index 7cfb9f874..ec34acde2 100644 --- a/src/common/Platform.ts +++ b/src/common/Platform.ts @@ -15,7 +15,8 @@ declare const navigator: INavigator; declare const process: unknown; // navigator.userAgent is also checked here because bundling with the process module can cause -// issues otherwise. Note that navigator exists in Node.js 21+ but the userAgent is "Node.js/". +// issues otherwise. Note that navigator exists in Node.js 21+ but the userAgent is +// "Node.js/". export const isNode = (typeof process !== 'undefined' && 'title' in (process as any) && (typeof navigator === 'undefined' || navigator.userAgent.startsWith('Node.js/'))) ? true : false; const userAgent = (isNode) ? 'node' : navigator.userAgent; const platform = (isNode) ? 'node' : navigator.platform; From a63980819638d7b6ac80114a0521a262e85c89e7 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 3 Jan 2026 14:32:12 -0800 Subject: [PATCH 23/38] Recommend eslint extension in vscode Fixes #5575 --- .vscode/extensions.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .vscode/extensions.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..897af65d7 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "dbaeumer.vscode-eslint" + ] +} \ No newline at end of file From 6211bf078cdd187a95f4900599299d82318fb456 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 3 Jan 2026 14:35:39 -0800 Subject: [PATCH 24/38] Lint demo code Fixes #5576 --- demo/client/components/window/addonSerializeWindow.ts | 2 -- demo/client/components/window/cellInspectorWindow.ts | 10 +--------- demo/client/components/window/gpuWindow.ts | 6 ++++-- demo/client/components/window/optionsWindow.ts | 8 ++++---- demo/client/components/window/testWindow.ts | 8 +++----- demo/client/components/window/vtWindow.ts | 2 +- demo/client/components/window/webglWindow.ts | 6 ++++-- package.json | 4 ++-- 8 files changed, 19 insertions(+), 27 deletions(-) diff --git a/demo/client/components/window/addonSerializeWindow.ts b/demo/client/components/window/addonSerializeWindow.ts index 688708ab7..ebc3df364 100644 --- a/demo/client/components/window/addonSerializeWindow.ts +++ b/demo/client/components/window/addonSerializeWindow.ts @@ -3,10 +3,8 @@ * @license MIT */ -import type { Terminal } from '@xterm/xterm'; import { BaseWindow } from './baseWindow'; import type { IControlWindow } from '../controlBar'; -import type { AddonCollection } from '../../types'; export class AddonSerializeWindow extends BaseWindow implements IControlWindow { public readonly id = 'addon-serialize'; diff --git a/demo/client/components/window/cellInspectorWindow.ts b/demo/client/components/window/cellInspectorWindow.ts index 6a6f67d7a..9098fc5e3 100644 --- a/demo/client/components/window/cellInspectorWindow.ts +++ b/demo/client/components/window/cellInspectorWindow.ts @@ -5,8 +5,7 @@ import { BaseWindow } from './baseWindow'; import type { IControlWindow } from '../controlBar'; -import type { Terminal, IBufferCell } from '@xterm/xterm'; -import type { AddonCollection } from '../../types'; +import type { IBufferCell } from '@xterm/xterm'; // Underline style values from common/buffer/Constants.ts const enum UnderlineStyle { @@ -40,13 +39,6 @@ export class CellInspectorWindow extends BaseWindow implements IControlWindow { private _bgEl: HTMLElement; private _attrsEl: HTMLElement; - constructor( - terminal: Terminal, - addons: AddonCollection, - ) { - super(terminal, addons); - } - public build(container: HTMLElement): void { this._container = container; diff --git a/demo/client/components/window/gpuWindow.ts b/demo/client/components/window/gpuWindow.ts index 7035321f5..84bccf11d 100644 --- a/demo/client/components/window/gpuWindow.ts +++ b/demo/client/components/window/gpuWindow.ts @@ -43,7 +43,9 @@ export class GpuWindow extends BaseWindow implements IControlWindow { } private _styleAtlasPage(canvas: HTMLCanvasElement): void { - canvas.style.width = `${canvas.width / window.devicePixelRatio}px`; - canvas.style.height = `${canvas.height / window.devicePixelRatio}px`; + // eslint-disable-next-line no-restricted-syntax + const dpr = window.devicePixelRatio; + canvas.style.width = `${canvas.width / dpr}px`; + canvas.style.height = `${canvas.height / dpr}px`; } } diff --git a/demo/client/components/window/optionsWindow.ts b/demo/client/components/window/optionsWindow.ts index c2f810b8e..0096d2cea 100644 --- a/demo/client/components/window/optionsWindow.ts +++ b/demo/client/components/window/optionsWindow.ts @@ -88,11 +88,11 @@ export class OptionsWindow extends BaseWindow implements IControlWindow { terminal: Terminal, addons: AddonCollection, private readonly _handlers: { - updateTerminalSize: () => void, - updateTerminalContainerBackground: () => void + updateTerminalSize: () => void; + updateTerminalContainerBackground: () => void; }, ) { - super(terminal, addons) + super(terminal, addons); } public build(container: HTMLElement): void { @@ -238,7 +238,7 @@ export class OptionsWindow extends BaseWindow implements IControlWindow { this._autoResize = value; } - private _getTheme() { + private _getTheme(): ITheme { const input = document.querySelector('#opt-theme'); let theme: ITheme; switch (input.value) { diff --git a/demo/client/components/window/testWindow.ts b/demo/client/components/window/testWindow.ts index 1b2d5721e..e811ce786 100644 --- a/demo/client/components/window/testWindow.ts +++ b/demo/client/components/window/testWindow.ts @@ -3,8 +3,6 @@ * @license MIT */ -/// - import { writeUnicodeTable } from '../../unicodeTable'; import type { IControlWindow } from '../controlBar'; import { BaseWindow } from './baseWindow'; @@ -21,8 +19,8 @@ export class TestWindow extends BaseWindow implements IControlWindow { terminal: Terminal, addons: AddonCollection, private readonly _handlers: { - disposeRecreateButtonHandler: () => void, - createNewWindowButtonHandler: () => void, + disposeRecreateButtonHandler: () => void; + createNewWindowButtonHandler: () => void; }, ) { super(terminal, addons); @@ -501,7 +499,7 @@ function customGlyphAlignmentHandler(term: Terminal): void { term.write(' \u{F5F9}\u{F5F9} \n\r'); term.write('\x1b[0m'); term.write('\n\r'); - + term.write('\x1b[0mProgress bar alignment tests:\x1b[33m\n\r'); term.write('\u{EE00}\u{EE01}\u{EE02} \u{EE03}\u{EE04}\u{EE05}'); diff --git a/demo/client/components/window/vtWindow.ts b/demo/client/components/window/vtWindow.ts index 7e6d2c741..b5d01367e 100644 --- a/demo/client/components/window/vtWindow.ts +++ b/demo/client/components/window/vtWindow.ts @@ -22,7 +22,7 @@ export class VtWindow extends BaseWindow implements IControlWindow { container.appendChild(vtContainer); const vtFragment = document.createDocumentFragment(); - const buttonSpecs: { [key: string]: { label: string; description: string; paramCount?: number } } = { + const buttonSpecs: { [key: string]: { label: string, description: string, paramCount?: number } } = { 'A': { label: 'CUU ↑', description: 'Cursor Up Ps Times' }, 'B': { label: 'CUD ↓', description: 'Cursor Down Ps Times' }, 'C': { label: 'CUF →', description: 'Cursor Forward Ps Times' }, diff --git a/demo/client/components/window/webglWindow.ts b/demo/client/components/window/webglWindow.ts index b63728eb6..f4fabf63f 100644 --- a/demo/client/components/window/webglWindow.ts +++ b/demo/client/components/window/webglWindow.ts @@ -43,7 +43,9 @@ export class WebglWindow extends BaseWindow implements IControlWindow { } private _styleAtlasPage(canvas: HTMLCanvasElement): void { - canvas.style.width = `${canvas.width / window.devicePixelRatio}px`; - canvas.style.height = `${canvas.height / window.devicePixelRatio}px`; + // eslint-disable-next-line no-restricted-syntax + const dpr = window.devicePixelRatio; + canvas.style.width = `${canvas.width / dpr}px`; + canvas.style.height = `${canvas.height / dpr}px`; } } diff --git a/package.json b/package.json index e99b53509..ecb60ef47 100644 --- a/package.json +++ b/package.json @@ -47,8 +47,8 @@ "esbuild-demo-server-watch": "node bin/esbuild.mjs --demo-server --watch", "test": "npm run test-unit", "posttest": "npm run lint", - "lint": "eslint --max-warnings 0 src/ addons/", - "lint-fix": "eslint --fix src/ addons/", + "lint": "eslint --max-warnings 0 src/ addons/ demo/", + "lint-fix": "eslint --fix src/ addons/ demo/", "lint-api": "eslint --config eslint.config.typings.mjs --max-warnings 0 typings/", "test-unit": "node ./bin/test_unit.js", "test-unit-coverage": "node ./bin/test_unit.js --coverage", From 8193ceae12721f08187dbd5bd33fec4949c23b17 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 3 Jan 2026 14:40:07 -0800 Subject: [PATCH 25/38] Make everything except unicode APIs stable --- src/browser/public/Terminal.ts | 4 ---- src/headless/public/Terminal.ts | 4 ---- typings/xterm-headless.d.ts | 11 ++++------- typings/xterm.d.ts | 20 ++++++++++---------- 4 files changed, 14 insertions(+), 25 deletions(-) diff --git a/src/browser/public/Terminal.ts b/src/browser/public/Terminal.ts index c872f5e17..157ceb145 100644 --- a/src/browser/public/Terminal.ts +++ b/src/browser/public/Terminal.ts @@ -103,7 +103,6 @@ export class Terminal extends Disposable implements ITerminalApi { return this._buffer; } public get markers(): ReadonlyArray { - this._checkProposedApi(); return this._core.markers; } public get modes(): IModes { @@ -166,11 +165,9 @@ export class Terminal extends Disposable implements ITerminalApi { return this._core.registerLinkProvider(linkProvider); } public registerCharacterJoiner(handler: (text: string) => [number, number][]): number { - this._checkProposedApi(); return this._core.registerCharacterJoiner(handler); } public deregisterCharacterJoiner(joinerId: number): void { - this._checkProposedApi(); this._core.deregisterCharacterJoiner(joinerId); } public registerMarker(cursorYOffset: number = 0): IMarker { @@ -178,7 +175,6 @@ export class Terminal extends Disposable implements ITerminalApi { return this._core.registerMarker(cursorYOffset); } public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined { - this._checkProposedApi(); this._verifyPositiveIntegers(decorationOptions.x ?? 0, decorationOptions.width ?? 0, decorationOptions.height ?? 0); return this._core.registerDecoration(decorationOptions); } diff --git a/src/headless/public/Terminal.ts b/src/headless/public/Terminal.ts index 811345b90..e0a47e1f5 100644 --- a/src/headless/public/Terminal.ts +++ b/src/headless/public/Terminal.ts @@ -84,7 +84,6 @@ export class Terminal extends Disposable implements ITerminalApi { public get onWriteParsed(): Event { return this._core.onWriteParsed; } public get parser(): IParser { - this._checkProposedApi(); if (!this._parser) { this._parser = new ParserApi(this._core); } @@ -97,14 +96,12 @@ export class Terminal extends Disposable implements ITerminalApi { public get rows(): number { return this._core.rows; } public get cols(): number { return this._core.cols; } public get buffer(): IBufferNamespaceApi { - this._checkProposedApi(); if (!this._buffer) { this._buffer = this._register(new BufferNamespaceApi(this._core)); } return this._buffer; } public get markers(): ReadonlyArray { - this._checkProposedApi(); return this._core.markers; } public get modes(): IModes { @@ -146,7 +143,6 @@ export class Terminal extends Disposable implements ITerminalApi { this._core.resize(columns, rows); } public registerMarker(cursorYOffset: number = 0): IMarker | undefined { - this._checkProposedApi(); this._verifyIntegers(cursorYOffset); return this._core.addMarker(cursorYOffset); } diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts index 3c018a3ba..abc28d081 100644 --- a/typings/xterm-headless.d.ts +++ b/typings/xterm-headless.d.ts @@ -584,21 +584,18 @@ declare module '@xterm/headless' { readonly cols: number; /** - * (EXPERIMENTAL) The terminal's current buffer, this might be either the - * normal buffer or the alt buffer depending on what's running in the - * terminal. + * Access to the terminal's normal and alt buffer. */ readonly buffer: IBufferNamespace; /** - * (EXPERIMENTAL) Get all markers registered against the buffer. If the alt - * buffer is active this will always return []. + * Get all markers registered against the buffer. If the alt buffer is + * active this will always return []. */ readonly markers: ReadonlyArray; /** - * (EXPERIMENTAL) Get the parser interface to register - * custom escape sequence handlers. + * Get the parser interface to register custom escape sequence handlers. */ readonly parser: IParser; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 1084cae80..9e60e74ca 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -856,8 +856,8 @@ declare module '@xterm/xterm' { readonly buffer: IBufferNamespace; /** - * (EXPERIMENTAL) Get all markers registered against the buffer. If the alt - * buffer is active this will always return []. + * Get all markers registered against the buffer. If the alt buffer is + * active this will always return []. */ readonly markers: ReadonlyArray; @@ -867,8 +867,8 @@ declare module '@xterm/xterm' { readonly parser: IParser; /** - * (EXPERIMENTAL) Get the Unicode handling interface - * to register and switch Unicode version. + * (EXPERIMENTAL) Get the Unicode handling interface to register and switch + * Unicode version. */ readonly unicode: IUnicodeHandling; @@ -1129,9 +1129,9 @@ declare module '@xterm/xterm' { registerLinkProvider(linkProvider: ILinkProvider): IDisposable; /** - * (EXPERIMENTAL) Registers a character joiner, allowing custom sequences of - * characters to be rendered as a single unit. This is useful in particular - * for rendering ligatures and graphemes, among other things. + * Registers a character joiner, allowing custom sequences of characters to + * be rendered as a single unit. This is useful in particular for rendering + * ligatures and graphemes, among other things. * * Each registered character joiner is called with a string of text * representing a portion of a line in the terminal that can be rendered as @@ -1160,8 +1160,8 @@ declare module '@xterm/xterm' { registerCharacterJoiner(handler: (text: string) => [number, number][]): number; /** - * (EXPERIMENTAL) Deregisters the character joiner if one was registered. - * NOTE: character joiners are only used by the webgl renderer. + * Deregisters the character joiner if one was registered. Note that + * character joiners are only used by the webgl renderer. * @param joinerId The character joiner's ID (returned after register) */ deregisterCharacterJoiner(joinerId: number): void; @@ -1174,7 +1174,7 @@ declare module '@xterm/xterm' { registerMarker(cursorYOffset?: number): IMarker; /** - * (EXPERIMENTAL) Adds a decoration to the terminal using + * Registers a decoration to the terminal. * @param decorationOptions, which takes a marker and an optional anchor, * width, height, and x offset from the anchor. Returns the decoration or * undefined if the alt buffer is active or the marker has already been From 0c7b1cb448626009807311adace0b5ce06c52ff2 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 3 Jan 2026 14:52:32 -0800 Subject: [PATCH 26/38] Update demo dims on font family change Fixes #5572 --- demo/client/components/window/optionsWindow.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/demo/client/components/window/optionsWindow.ts b/demo/client/components/window/optionsWindow.ts index c2f810b8e..923865819 100644 --- a/demo/client/components/window/optionsWindow.ts +++ b/demo/client/components/window/optionsWindow.ts @@ -226,6 +226,9 @@ export class OptionsWindow extends BaseWindow implements IControlWindow { if (o === 'theme') { this._handlers.updateTerminalContainerBackground(); } + if (o === 'fontFamily') { + this._handlers.updateTerminalSize(); + } }); }); } From 7389a1097df0041583c3e727225b7cbb4a82e1ad Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 3 Jan 2026 14:57:55 -0800 Subject: [PATCH 27/38] Update tests to use unicode for proposed check --- src/headless/public/Terminal.test.ts | 2 +- test/playwright/Terminal.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/headless/public/Terminal.test.ts b/src/headless/public/Terminal.test.ts index 8beffc692..059b4a505 100644 --- a/src/headless/public/Terminal.test.ts +++ b/src/headless/public/Terminal.test.ts @@ -22,7 +22,7 @@ describe('Headless API Tests', function (): void { it('Proposed API check', async () => { term = new Terminal({ allowProposedApi: false }); - throws(() => term.markers, (error: any) => error.message === 'You must set the allowProposedApi option to true to use proposed API'); + throws(() => term.unicode, (error: any) => error.message === 'You must set the allowProposedApi option to true to use proposed API'); }); it('write', async () => { diff --git a/test/playwright/Terminal.test.ts b/test/playwright/Terminal.test.ts index f03d687d9..acd89f735 100644 --- a/test/playwright/Terminal.test.ts +++ b/test/playwright/Terminal.test.ts @@ -25,7 +25,7 @@ test.describe('API Integration Tests', () => { await openTerminal(ctx, { allowProposedApi: false }, { loadUnicodeGraphemesAddon: false }); await ctx.page.evaluate(` try { - window.term.markers; + window.term.unicode; } catch (e) { window.throwMessage = e.message; } From 5e95dc1f1014c3c56969fea4b1176acaaf604890 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 3 Jan 2026 15:24:11 -0800 Subject: [PATCH 28/38] Sync package lock --- addons/addon-web-fonts/package.json | 3 --- package-lock.json | 20 +++++++++----------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/addons/addon-web-fonts/package.json b/addons/addon-web-fonts/package.json index 1daac3768..856c7e258 100644 --- a/addons/addon-web-fonts/package.json +++ b/addons/addon-web-fonts/package.json @@ -22,8 +22,5 @@ "prepublishOnly": "npm run package", "start": "node ../../demo/start" }, - "peerDependencies": { - "@xterm/xterm": "^5.0.0" - }, "dependencies": {} } diff --git a/package-lock.json b/package-lock.json index 2e55f3de8..4c218b709 100644 --- a/package-lock.json +++ b/package-lock.json @@ -145,6 +145,11 @@ "version": "0.9.0", "license": "MIT" }, + "addons/addon-web-fonts": { + "name": "@xterm/addon-web-fonts", + "version": "0.1.0", + "license": "MIT" + }, "addons/addon-web-links": { "name": "@xterm/addon-web-links", "version": "0.12.0", @@ -239,7 +244,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -553,7 +557,6 @@ "url": "https://opencollective.com/csstools" } ], - "peer": true, "engines": { "node": ">=18" }, @@ -595,7 +598,6 @@ "url": "https://opencollective.com/csstools" } ], - "peer": true, "engines": { "node": ">=18" } @@ -1897,7 +1899,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.1.tgz", "integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==", "dev": true, - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.50.1", "@typescript-eslint/types": "8.50.1", @@ -2297,6 +2298,10 @@ "resolved": "addons/addon-unicode11", "link": true }, + "node_modules/@xterm/addon-web-fonts": { + "resolved": "addons/addon-web-fonts", + "link": true + }, "node_modules/@xterm/addon-web-links": { "resolved": "addons/addon-web-links", "link": true @@ -2342,7 +2347,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2732,7 +2736,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3734,7 +3737,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7090,7 +7092,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -7958,7 +7959,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8142,7 +8142,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz", "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", "dev": true, - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -8191,7 +8190,6 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz", "integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==", "dev": true, - "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^1.2.0", From a35223a819eb588c3ddc01a3f239e0055d658a8b Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 3 Jan 2026 15:31:24 -0800 Subject: [PATCH 29/38] Add web fonts window to demo --- demo/client.ts | 1450 ----------------- demo/client/client.ts | 9 +- .../components/window/addonWebFontsWindow.ts | 60 + demo/client/types.ts | 5 +- 4 files changed, 72 insertions(+), 1452 deletions(-) delete mode 100644 demo/client.ts create mode 100644 demo/client/components/window/addonWebFontsWindow.ts diff --git a/demo/client.ts b/demo/client.ts deleted file mode 100644 index 732efda5b..000000000 --- a/demo/client.ts +++ /dev/null @@ -1,1450 +0,0 @@ -/* eslint-disable no-restricted-syntax */ -/** - * Copyright (c) 2018 The xterm.js authors. All rights reserved. - * @license MIT - * - * This file is the entry point for browserify. - */ - -/// - -// HACK: Playwright/WebKit on Windows does not support WebAssembly https://stackoverflow.com/q/62311688/1156119 -import type { ImageAddon as ImageAddonType, IImageAddonOptions } from '@xterm/addon-image'; -let ImageAddon: typeof ImageAddonType | undefined; // eslint-disable-line @typescript-eslint/naming-convention -if ('WebAssembly' in window) { - const imageAddon = require('@xterm/addon-image'); - ImageAddon = imageAddon.ImageAddon; -} - -import { Terminal, ITerminalOptions, type IDisposable } from '@xterm/xterm'; -import { AttachAddon } from '@xterm/addon-attach'; -import { ClipboardAddon } from '@xterm/addon-clipboard'; -import { FitAddon } from '@xterm/addon-fit'; -import { LigaturesAddon } from '@xterm/addon-ligatures'; -import { SearchAddon, ISearchOptions } from '@xterm/addon-search'; -import { SerializeAddon } from '@xterm/addon-serialize'; -import { WebFontsAddon, loadFonts } from '@xterm/addon-web-fonts'; -import { WebLinksAddon } from '@xterm/addon-web-links'; -import { WebglAddon } from '@xterm/addon-webgl'; -import { Unicode11Addon } from '@xterm/addon-unicode11'; -import { UnicodeGraphemesAddon } from '@xterm/addon-unicode-graphemes'; - -export interface IWindowWithTerminal extends Window { - term: typeof Terminal; - Terminal: typeof Terminal; - AttachAddon?: typeof AttachAddon; // eslint-disable-line @typescript-eslint/naming-convention - ClipboardAddon?: typeof ClipboardAddon; // eslint-disable-line @typescript-eslint/naming-convention - FitAddon?: typeof FitAddon; // eslint-disable-line @typescript-eslint/naming-convention - ImageAddon?: typeof ImageAddon; // eslint-disable-line @typescript-eslint/naming-convention - SearchAddon?: typeof SearchAddon; // eslint-disable-line @typescript-eslint/naming-convention - SerializeAddon?: typeof SerializeAddon; // eslint-disable-line @typescript-eslint/naming-convention - WebFontsAddon?: typeof WebFontsAddon; // eslint-disable-line @typescript-eslint/naming-convention - WebLinksAddon?: typeof WebLinksAddon; // eslint-disable-line @typescript-eslint/naming-convention - WebglAddon?: typeof WebglAddon; // eslint-disable-line @typescript-eslint/naming-convention - Unicode11Addon?: typeof Unicode11Addon; // eslint-disable-line @typescript-eslint/naming-convention - UnicodeGraphemesAddon?: typeof UnicodeGraphemesAddon; // eslint-disable-line @typescript-eslint/naming-convention - LigaturesAddon?: typeof LigaturesAddon; // eslint-disable-line @typescript-eslint/naming-convention -} -declare let window: IWindowWithTerminal; - -let term; -let protocol; -let socketURL; -let socket; -let pid; -let autoResize: boolean = true; - -type AddonType = 'attach' | 'clipboard' | 'fit' | 'image' | 'search' | 'serialize' | 'unicode11' | 'unicodeGraphemes' | 'webFonts' | 'webLinks' | 'webgl' | 'ligatures'; - -interface IDemoAddon { - name: T; - canChange: boolean; - ctor: ( - T extends 'attach' ? typeof AttachAddon : - T extends 'clipboard' ? typeof ClipboardAddon : - T extends 'fit' ? typeof FitAddon : - T extends 'image' ? typeof ImageAddonType : - T extends 'ligatures' ? typeof LigaturesAddon : - T extends 'search' ? typeof SearchAddon : - T extends 'serialize' ? typeof SerializeAddon : - T extends 'webFonts' ? typeof WebFontsAddon : - T extends 'webLinks' ? typeof WebLinksAddon : - T extends 'unicode11' ? typeof Unicode11Addon : - T extends 'unicodeGraphemes' ? typeof UnicodeGraphemesAddon : - T extends 'webgl' ? typeof WebglAddon : - never - ); - instance?: ( - T extends 'attach' ? AttachAddon : - T extends 'clipboard' ? ClipboardAddon : - T extends 'fit' ? FitAddon : - T extends 'image' ? ImageAddonType : - T extends 'ligatures' ? LigaturesAddon : - T extends 'search' ? SearchAddon : - T extends 'serialize' ? SerializeAddon : - T extends 'webFonts' ? WebFontsAddon : - T extends 'webLinks' ? WebLinksAddon : - T extends 'unicode11' ? Unicode11Addon : - T extends 'unicodeGraphemes' ? UnicodeGraphemesAddon : - T extends 'webgl' ? WebglAddon : - never - ); -} - -const addons: { [T in AddonType]: IDemoAddon } = { - attach: { name: 'attach', ctor: AttachAddon, canChange: false }, - clipboard: { name: 'clipboard', ctor: ClipboardAddon, canChange: true }, - fit: { name: 'fit', ctor: FitAddon, canChange: false }, - image: { name: 'image', ctor: ImageAddon, canChange: true }, - search: { name: 'search', ctor: SearchAddon, canChange: true }, - serialize: { name: 'serialize', ctor: SerializeAddon, canChange: true }, - webFonts: { name: 'webFonts', ctor: WebFontsAddon, canChange: true }, - webLinks: { name: 'webLinks', ctor: WebLinksAddon, canChange: true }, - webgl: { name: 'webgl', ctor: WebglAddon, canChange: true }, - unicode11: { name: 'unicode11', ctor: Unicode11Addon, canChange: true }, - unicodeGraphemes: { name: 'unicodeGraphemes', ctor: UnicodeGraphemesAddon, canChange: true }, - ligatures: { name: 'ligatures', ctor: LigaturesAddon, canChange: true } -}; - -let terminalContainer = document.getElementById('terminal-container'); -const actionElements = { - find: document.querySelector('#find') as HTMLInputElement, - findNext: document.querySelector('#find-next') as HTMLInputElement, - findPrevious: document.querySelector('#find-previous') as HTMLInputElement, - findResults: document.querySelector('#find-results') -}; -const paddingElement = document.getElementById('padding') as HTMLInputElement; - -const xtermjsTheme = { - foreground: '#F8F8F8', - background: '#2D2E2C', - selectionBackground: '#5DA5D533', - selectionInactiveBackground: '#555555AA', - black: '#1E1E1D', - brightBlack: '#262625', - red: '#CE5C5C', - brightRed: '#FF7272', - green: '#5BCC5B', - brightGreen: '#72FF72', - yellow: '#CCCC5B', - brightYellow: '#FFFF72', - blue: '#5D5DD3', - brightBlue: '#7279FF', - magenta: '#BC5ED1', - brightMagenta: '#E572FF', - cyan: '#5DA5D5', - brightCyan: '#72F0FF', - white: '#F8F8F8', - brightWhite: '#FFFFFF' -}; -function setPadding(): void { - term.element.style.padding = parseInt(paddingElement.value, 10).toString() + 'px'; - addons.fit.instance.fit(); -} - -function getSearchOptions(): ISearchOptions { - return { - regex: (document.getElementById('regex') as HTMLInputElement).checked, - wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked, - caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked, - decorations: (document.getElementById('highlight-all-matches') as HTMLInputElement).checked ? { - matchBackground: '#232422', - matchBorder: '#555753', - matchOverviewRuler: '#555753', - activeMatchBackground: '#ef2929', - activeMatchBorder: '#ffffff', - activeMatchColorOverviewRuler: '#ef2929' - } : undefined - }; -} - -const disposeRecreateButtonHandler: () => void = () => { - // If the terminal exists dispose of it, otherwise recreate it - if (term) { - term.dispose(); - term = null; - window.term = null; - socket = null; - addons.attach.instance = undefined; - addons.clipboard.instance = undefined; - addons.fit.instance = undefined; - addons.image.instance = undefined; - addons.search.instance = undefined; - addons.serialize.instance = undefined; - addons.unicode11.instance = undefined; - addons.unicodeGraphemes.instance = undefined; - addons.ligatures.instance = undefined; - addons.webFonts.instance = undefined; - addons.webLinks.instance = undefined; - addons.webgl.instance = undefined; - document.getElementById('dispose').innerHTML = 'Recreate Terminal'; - } else { - createTerminal(); - document.getElementById('dispose').innerHTML = 'Dispose terminal'; - } -}; - -const createNewWindowButtonHandler: () => void = () => { - if (term) { - disposeRecreateButtonHandler(); - } - const win = window.open(); - terminalContainer = win.document.createElement('div'); - terminalContainer.id = 'terminal-container'; - win.document.body.appendChild(terminalContainer); - - // Stylesheets are needed to get the terminal in the popout window to render - // correctly. We also need to wait for them to load before creating the - // terminal, otherwise we will not compute the correct metrics when rendering. - let pendingStylesheets = 0; - for (const linkNode of document.querySelectorAll('head link[rel=stylesheet]')) { - const newLink = document.createElement('link'); - newLink.rel = 'stylesheet'; - newLink.href = (linkNode as HTMLLinkElement).href; - win.document.head.appendChild(newLink); - - pendingStylesheets++; - newLink.addEventListener('load', () => { - pendingStylesheets--; - if (pendingStylesheets === 0) { - createTerminal(); - } - }); - } -}; - -if (document.location.pathname === '/test') { - window.Terminal = Terminal; - window.AttachAddon = AttachAddon; - window.ClipboardAddon = ClipboardAddon; - window.FitAddon = FitAddon; - window.ImageAddon = ImageAddon; - window.SearchAddon = SearchAddon; - window.SerializeAddon = SerializeAddon; - window.Unicode11Addon = Unicode11Addon; - window.UnicodeGraphemesAddon = UnicodeGraphemesAddon; - window.LigaturesAddon = LigaturesAddon; - window.WebFontsAddon = WebFontsAddon; - window.WebLinksAddon = WebLinksAddon; - window.WebglAddon = WebglAddon; -} else { - createTerminal(); - document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler); - document.getElementById('create-new-window').addEventListener('click', createNewWindowButtonHandler); - document.getElementById('serialize').addEventListener('click', serializeButtonHandler); - document.getElementById('htmlserialize').addEventListener('click', htmlSerializeButtonHandler); - document.getElementById('custom-glyph').addEventListener('click', writeCustomGlyphHandler); - document.getElementById('load-test').addEventListener('click', loadTest); - document.getElementById('load-test-long-lines').addEventListener('click', loadTestLongLines); - document.getElementById('print-cjk').addEventListener('click', addCjk); - document.getElementById('print-cjk-sgr').addEventListener('click', addCjkRandomSgr); - document.getElementById('powerline-symbol-test').addEventListener('click', powerlineSymbolTest); - document.getElementById('underline-test').addEventListener('click', underlineTest); - document.getElementById('ansi-colors').addEventListener('click', ansiColorsTest); - document.getElementById('osc-hyperlinks').addEventListener('click', addAnsiHyperlink); - document.getElementById('sgr-test').addEventListener('click', sgrTest); - document.getElementById('add-grapheme-clusters').addEventListener('click', addGraphemeClusters); - document.getElementById('add-decoration').addEventListener('click', addDecoration); - document.getElementById('add-overview-ruler').addEventListener('click', addOverviewRuler); - document.getElementById('decoration-stress-test').addEventListener('click', decorationStressTest); - document.getElementById('weblinks-test').addEventListener('click', testWeblinks); - document.getElementById('bce').addEventListener('click', coloredErase); - addVtButtons(); - initImageAddonExposed(); - testEvents(); - testWebfonts(); -} - -function createTerminal(): void { - // Clean terminal - while (terminalContainer.children.length) { - terminalContainer.removeChild(terminalContainer.children[0]); - } - - const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0; - term = new Terminal({ - allowProposedApi: true, - windowsPty: isWindows ? { - // In a real scenario, these values should be verified on the backend - backend: 'conpty', - buildNumber: 22621 - } : undefined, - fontFamily: '"Fira Code", courier-new, courier, monospace, "Powerline Extra Symbols"', - theme: xtermjsTheme - } as ITerminalOptions); - - // Load addons - const typedTerm = term as Terminal; - addons.search.instance = new SearchAddon(); - addons.serialize.instance = new SerializeAddon(); - addons.fit.instance = new FitAddon(); - addons.image.instance = new ImageAddon(); - addons.unicodeGraphemes.instance = new UnicodeGraphemesAddon(); - addons.clipboard.instance = new ClipboardAddon(); - try { // try to start with webgl renderer (might throw on older safari/webkit) - addons.webgl.instance = new WebglAddon(); - } catch (e) { - console.warn(e); - } - addons.webFonts.instance = new WebFontsAddon(); - addons.webLinks.instance = new WebLinksAddon(); - typedTerm.loadAddon(addons.fit.instance); - typedTerm.loadAddon(addons.image.instance); - typedTerm.loadAddon(addons.search.instance); - typedTerm.loadAddon(addons.serialize.instance); - typedTerm.loadAddon(addons.unicodeGraphemes.instance); - typedTerm.loadAddon(addons.webFonts.instance); - typedTerm.loadAddon(addons.webLinks.instance); - typedTerm.loadAddon(addons.clipboard.instance); - - window.term = term; // Expose `term` to window for debugging purposes - term.onResize((size: { cols: number, rows: number }) => { - if (!pid) { - return; - } - const cols = size.cols; - const rows = size.rows; - const url = '/terminals/' + pid + '/size?cols=' + cols + '&rows=' + rows; - - fetch(url, { method: 'POST' }); - }); - protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://'; - socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/'; - - addons.fit.instance!.fit(); - - if (addons.webgl.instance) { - try { - typedTerm.loadAddon(addons.webgl.instance); - term.open(terminalContainer); - setTextureAtlas(addons.webgl.instance.textureAtlas); - addons.webgl.instance.onChangeTextureAtlas(e => setTextureAtlas(e)); - addons.webgl.instance.onAddTextureAtlasCanvas(e => appendTextureAtlas(e)); - addons.webgl.instance.onRemoveTextureAtlasCanvas(e => removeTextureAtlas(e)); - } catch (e) { - console.warn('error during loading webgl addon:', e); - addons.webgl.instance.dispose(); - addons.webgl.instance = undefined; - } - } - if (!typedTerm.element) { - // webgl loading failed for some reason, attach with DOM renderer - term.open(terminalContainer); - } - - term.focus(); - - const resizeObserver = new ResizeObserver(entries => { - if (autoResize) { - addons.fit.instance.fit(); - } - }); - resizeObserver.observe(terminalContainer); - - addDomListener(paddingElement, 'change', setPadding); - - addDomListener(actionElements.findNext, 'keydown', (e) => { - if (e.key === 'Enter') { - addons.search.instance.findNext(actionElements.findNext.value, getSearchOptions()); - e.preventDefault(); - } - }); - addDomListener(actionElements.findNext, 'input', (e) => { - addons.search.instance.findNext(actionElements.findNext.value, getSearchOptions()); - }); - addDomListener(actionElements.findPrevious, 'keydown', (e) => { - if (e.key === 'Enter') { - addons.search.instance.findPrevious(actionElements.findPrevious.value, getSearchOptions()); - e.preventDefault(); - } - }); - addDomListener(actionElements.findPrevious, 'input', (e) => { - addons.search.instance.findPrevious(actionElements.findPrevious.value, getSearchOptions()); - }); - addDomListener(actionElements.findNext, 'blur', (e) => { - addons.search.instance.clearActiveDecoration(); - }); - addDomListener(actionElements.findPrevious, 'blur', (e) => { - addons.search.instance.clearActiveDecoration(); - }); - - // fit is called within a setTimeout, cols and rows need this. - setTimeout(async () => { - initOptions(term); - paddingElement.value = '0'; - - // Set terminal size again to set the specific dimensions on the demo - updateTerminalSize(); - - const res = await fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, { method: 'POST' }); - const processId = await res.text(); - pid = processId; - socketURL += processId; - socket = new WebSocket(socketURL); - socket.onopen = runRealTerminal; - socket.onclose = runFakeTerminal; - socket.onerror = runFakeTerminal; - }, 0); -} - -function runRealTerminal(): void { - addons.attach.instance = new AttachAddon(socket); - term.loadAddon(addons.attach.instance); - term._initialized = true; - initAddons(term); -} - -function runFakeTerminal(): void { - if (term._initialized) { - return; - } - - term._initialized = true; - initAddons(term); - - term.prompt = () => { - term.write('\r\n$ '); - }; - - term.writeln('Welcome to xterm.js'); - term.writeln('This is a local terminal emulation, without a real terminal in the back-end.'); - term.writeln('Type some keys and commands to play around.'); - term.writeln(''); - term.prompt(); - - term.onKey((e: { key: string, domEvent: KeyboardEvent }) => { - const ev = e.domEvent; - const printable = !ev.altKey && !ev.ctrlKey && !ev.metaKey; - - if (ev.keyCode === 13) { - term.prompt(); - } else if (ev.keyCode === 8) { - // Do not delete the prompt - if (term._core.buffer.x > 2) { - term.write('\b \b'); - } - } else if (printable) { - term.write(e.key); - } - }); -} - -function initOptions(term: Terminal): void { - const blacklistedOptions = [ - // Internal only options - 'cancelEvents', - 'convertEol', - 'termName', - 'cols', 'rows', // subsumed by "size" (colsRows) option - // Complex option - 'documentOverride', - 'linkHandler', - 'logger', - 'overviewRuler', - 'theme', - 'windowOptions', - 'windowsPty', - // Deprecated - 'fastScrollModifier' - ]; - const stringOptions = { - cursorStyle: ['block', 'underline', 'bar'], - cursorInactiveStyle: ['outline', 'block', 'bar', 'underline', 'none'], - fontFamily: null, - fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], - fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], - logLevel: ['trace', 'debug', 'info', 'warn', 'error', 'off'], - theme: ['default', 'xtermjs', 'sapphire', 'light'], - wordSeparator: null, - colsRows: null - }; - const options = Object.getOwnPropertyNames(term.options); - const booleanOptions = []; - const numberOptions = []; - options.filter(o => blacklistedOptions.indexOf(o) === -1).forEach(o => { - switch (typeof term.options[o]) { - case 'boolean': - booleanOptions.push(o); - break; - case 'number': - numberOptions.push(o); - break; - default: - if (Object.keys(stringOptions).indexOf(o) === -1 && numberOptions.indexOf(o) === -1 && booleanOptions.indexOf(o) === -1) { - console.warn(`Unrecognized option: "${o}"`); - } - } - }); - - let html = ''; - html += '

'; - - const container = document.getElementById('options-container'); - container.innerHTML = html; - - // Attach listeners - booleanOptions.forEach(o => { - const input = document.getElementById(`opt-${o}`) as HTMLInputElement; - addDomListener(input, 'change', () => { - console.log('change', o, input.checked); - term.options[o] = input.checked; - }); - }); - numberOptions.forEach(o => { - const input = document.getElementById(`opt-${o}`) as HTMLInputElement; - addDomListener(input, 'change', () => { - console.log('change', o, input.value); - if (o === 'lineHeight') { - term.options.lineHeight = parseFloat(input.value); - } else if (o === 'scrollSensitivity') { - term.options.scrollSensitivity = parseFloat(input.value); - } else if (o === 'scrollback') { - term.options.scrollback = parseInt(input.value); - setTimeout(() => updateTerminalSize(), 5); - } else { - term.options[o] = parseInt(input.value); - } - // Always update terminal size in case the option changes the dimensions - updateTerminalSize(); - }); - }); - Object.keys(stringOptions).forEach(o => { - const input = document.getElementById(`opt-${o}`) as HTMLInputElement; - addDomListener(input, 'change', () => { - console.log('change', o, input.value); - let value: any = input.value; - if (o === 'colsRows') { - const m = input.value.match(/^([0-9]+)x([0-9]+)$/); - if (m) { - autoResize = false; - term.resize(parseInt(m[1]), parseInt(m[2])); - } else { - autoResize = true; - input.value = 'auto'; - updateTerminalSize(); - } - } else if (o === 'theme') { - switch (input.value) { - case 'default': - value = undefined; - break; - case 'xtermjs': - // Custom theme to match style of xterm.js logo - value = xtermjsTheme; - case 'sapphire': - // Color source: https://github.com/Tyriar/vscode-theme-sapphire - value = { - background: '#1c2431', - foreground: '#cccccc', - selectionBackground: '#399ef440', - black: '#666666', - blue: '#399ef4', - brightBlack: '#666666', - brightBlue: '#399ef4', - brightCyan: '#21c5c7', - brightGreen: '#4eb071', - brightMagenta: '#b168df', - brightRed: '#da6771', - brightWhite: '#efefef', - brightYellow: '#fff099', - cyan: '#21c5c7', - green: '#4eb071', - magenta: '#b168df', - red: '#da6771', - white: '#efefef', - yellow: '#fff099' - }; - break; - case 'light': - // Color source: https://github.com/microsoft/vscode/blob/main/extensions/theme-defaults/themes/light_plus.json - value = { - background: '#ffffff', - foreground: '#333333', - cursor: '#333333', - cursorAccent: '#ffffff', - selectionBackground: '#add6ff', - overviewRulerBorder: '#aaaaaa', - black: '#000000', - blue: '#0451a5', - brightBlack: '#666666', - brightBlue: '#0451a5', - brightCyan: '#0598bc', - brightGreen: '#14ce14', - brightMagenta: '#bc05bc', - brightRed: '#cd3131', - brightWhite: '#a5a5a5', - brightYellow: '#b5ba00', - cyan: '#0598bc', - green: '#00bc00', - magenta: '#bc05bc', - red: '#cd3131', - white: '#555555', - yellow: '#949800' - }; - break; - } - } - term.options[o] = value; - }); - }); -} - -function initAddons(term: Terminal): void { - const fragment = document.createDocumentFragment(); - Object.keys(addons).forEach((name: AddonType) => { - const addon = addons[name]; - const checkbox = document.createElement('input') as HTMLInputElement; - checkbox.type = 'checkbox'; - checkbox.checked = !!addon.instance; - if (!addon.canChange) { - checkbox.disabled = true; - } - if (name === 'unicode11' && checkbox.checked) { - term.unicode.activeVersion = '11'; - } - if (name === 'unicodeGraphemes' && checkbox.checked) { - term.unicode.activeVersion = '15-graphemes'; - } - if (name === 'search' && checkbox.checked) { - addons[name].instance.onDidChangeResults(e => updateFindResults(e)); - } - addDomListener(checkbox, 'change', () => { - if (name === 'image') { - if (checkbox.checked) { - const ctorOptionsJson = document.querySelector('#image-options').value; - addon.instance = ctorOptionsJson - ? new addons[name].ctor(JSON.parse(ctorOptionsJson)) - : new addons[name].ctor(); - term.loadAddon(addon.instance); - } else { - addon.instance!.dispose(); - addon.instance = undefined; - } - return; - } - if (checkbox.checked) { - // HACK: Manually remove addons that cannot be changes - addon.instance = new (addon as IDemoAddon>).ctor(); - try { - term.loadAddon(addon.instance); - if (name === 'webgl') { - setTimeout(() => { - setTextureAtlas(addons.webgl.instance.textureAtlas); - addons.webgl.instance.onChangeTextureAtlas(e => setTextureAtlas(e)); - addons.webgl.instance.onAddTextureAtlasCanvas(e => appendTextureAtlas(e)); - }, 0); - } else if (name === 'unicode11') { - term.unicode.activeVersion = '11'; - } else if (name === 'unicodeGraphemes') { - term.unicode.activeVersion = '15-graphemes'; - } else if (name === 'search') { - addons[name].instance.onDidChangeResults(e => updateFindResults(e)); - } - } - catch { - addon.instance = undefined; - checkbox.checked = false; - checkbox.disabled = true; - } - } else { - if (name === 'webgl') { - addons.webgl.instance.textureAtlas.remove(); - } else if (name === 'unicode11' || name === 'unicodeGraphemes') { - term.unicode.activeVersion = '6'; - } - addon.instance!.dispose(); - addon.instance = undefined; - } - }); - const label = document.createElement('label'); - label.classList.add('addon'); - if (!addon.canChange) { - label.title = 'This addon is needed for the demo to operate'; - } - label.appendChild(checkbox); - label.appendChild(document.createTextNode(name)); - const wrapper = document.createElement('div'); - wrapper.classList.add('addon'); - wrapper.appendChild(label); - fragment.appendChild(wrapper); - }); - const container = document.getElementById('addons-container'); - container.innerHTML = ''; - container.appendChild(fragment); -} - -function updateFindResults(e: { resultIndex: number, resultCount: number } | undefined): void { - let content: string; - if (e === undefined) { - content = 'undefined'; - } else { - content = `index: ${e.resultIndex}, count: ${e.resultCount}`; - } - actionElements.findResults.textContent = content; -} - -function addDomListener(element: HTMLElement, type: string, handler: (...args: any[]) => any): void { - element.addEventListener(type, handler); - term._core._register({ dispose: () => element.removeEventListener(type, handler) }); -} - -function updateTerminalSize(): void { - const width = autoResize ? '100%' - : (term._core._renderService.dimensions.css.canvas.width + term._core.viewport.scrollBarWidth).toString() + 'px'; - const height = autoResize ? '100%' - : (term._core._renderService.dimensions.css.canvas.height).toString() + 'px'; - terminalContainer.style.width = width; - terminalContainer.style.height = height; - addons.fit.instance.fit(); -} - -function serializeButtonHandler(): void { - const output = addons.serialize.instance.serialize(); - const outputString = JSON.stringify(output); - - document.getElementById('serialize-output').innerText = outputString; - if ((document.getElementById('write-to-terminal') as HTMLInputElement).checked) { - term.reset(); - term.write(output); - } -} - -function htmlSerializeButtonHandler(): void { - const output = addons.serialize.instance.serializeAsHTML(); - document.getElementById('htmlserialize-output').innerText = output; - - // Deprecated, but the most supported for now. - function listener(e: any): void { - e.clipboardData.setData('text/html', output); - e.preventDefault(); - } - document.addEventListener('copy', listener); - document.execCommand('copy'); - document.removeEventListener('copy', listener); - document.getElementById('htmlserialize-output-result').innerText = 'Copied to clipboard'; -} - -function setTextureAtlas(e: HTMLCanvasElement): void { - styleAtlasPage(e); - document.querySelector('#texture-atlas').replaceChildren(e); -} -function appendTextureAtlas(e: HTMLCanvasElement): void { - styleAtlasPage(e); - document.querySelector('#texture-atlas').appendChild(e); -} -function removeTextureAtlas(e: HTMLCanvasElement): void { - e.remove(); -} -function styleAtlasPage(e: HTMLCanvasElement): void { - e.style.width = `${e.width / window.devicePixelRatio}px`; - e.style.height = `${e.height / window.devicePixelRatio}px`; -} - -function writeCustomGlyphHandler(): void { - term.write('\n\r'); - term.write('\n\r'); - term.write('Box styles: ┎┰┒┍┯┑╓╥╖╒╤╕ ┏┳┓┌┲┓┌┬┐┏┱┐\n\r'); - term.write('┌─┬─┐ ┏━┳━┓ ╔═╦═╗ ┠╂┨┝┿┥╟╫╢╞╪╡ ┡╇┩├╊┫┢╈┪┣╉┤\n\r'); - term.write('│ │ │ ┃ ┃ ┃ ║ ║ ║ ┖┸┚┕┷┙╙╨╜╘╧╛ └┴┘└┺┛┗┻┛┗┹┘\n\r'); - term.write('├─┼─┤ ┣━╋━┫ ╠═╬═╣ ┏┱┐┌┲┓┌┬┐┌┬┐ ┏┳┓┌┮┓┌┬┐┏┭┐\n\r'); - term.write('│ │ │ ┃ ┃ ┃ ║ ║ ║ ┡╃┤├╄┩├╆┪┢╅┤ ┞╀┦├┾┫┟╁┧┣┽┤\n\r'); - term.write('└─┴─┘ ┗━┻━┛ ╚═╩═╝ └┴┘└┴┘└┺┛┗┹┘ └┴┘└┶┛┗┻┛┗┵┘\n\r'); - term.write('\n\r'); - term.write('Other:\n\r'); - term.write('╭─╮ ╲ ╱ ╷╻╎╏┆┇┊┋ ╺╾╴ ╌╌╌ ┄┄┄ ┈┈┈\n\r'); - term.write('│ │ ╳ ╽╿╎╏┆┇┊┋ ╶╼╸ ╍╍╍ ┅┅┅ ┉┉┉\n\r'); - term.write('╰─╯ ╱ ╲ ╹╵╎╏┆┇┊┋\n\r'); - term.write('\n\r'); - term.write('All box drawing characters:\n\r'); - term.write('─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏\n\r'); - term.write('┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟\n\r'); - term.write('┠ ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯\n\r'); - term.write('┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿\n\r'); - term.write('╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏\n\r'); - term.write('═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟\n\r'); - term.write('╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯\n\r'); - term.write('╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿\n\r'); - term.write('Box drawing alignment tests:\x1b[31m █\n\r'); - term.write(' ▉\n\r'); - term.write(' ╔══╦══╗ ┌──┬──┐ ╭──┬──╮ ╭──┬──╮ ┏━━┳━━┓ ┎┒┏┑ ╷ ╻ ┏┯┓ ┌┰┐ ▊ ╱╲╱╲╳╳╳\n\r'); - term.write(' ║┌─╨─┐║ │╔═╧═╗│ │╒═╪═╕│ │╓─╁─╖│ ┃┌─╂─┐┃ ┗╃╄┙ ╶┼╴╺╋╸┠┼┨ ┝╋┥ ▋ ╲╱╲╱╳╳╳\n\r'); - term.write(' ║│╲ ╱│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╿ │┃ ┍╅╆┓ ╵ ╹ ┗┷┛ └┸┘ ▌ ╱╲╱╲╳╳╳\n\r'); - term.write(' ╠╡ ╳ ╞╣ ├╢ ╟┤ ├┼─┼─┼┤ ├╫─╂─╫┤ ┣┿╾┼╼┿┫ ┕┛┖┚ ┌┄┄┐ ╎ ┏┅┅┓ ┋ ▍ ╲╱╲╱╳╳╳\n\r'); - term.write(' ║│╱ ╲│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╽ │┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▎\n\r'); - term.write(' ║└─╥─┘║ │╚═╤═╝│ │╘═╪═╛│ │╙─╀─╜│ ┃└─╂─┘┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▏\n\r'); - term.write(' ╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█\n\r'); - term.write('Box drawing alignment tests:\x1b[32m █\n\r'); - term.write(' ▉\n\r'); - term.write(' ╔══╦══╗ ┌──┬──┐ ╭──┬──╮ ╭──┬──╮ ┏━━┳━━┓ ┎┒┏┑ ╷ ╻ ┏┯┓ ┌┰┐ ▊ ╱╲╱╲╳╳╳\n\r'); - term.write(' ║┌─╨─┐║ │╔═╧═╗│ │╒═╪═╕│ │╓─╁─╖│ ┃┌─╂─┐┃ ┗╃╄┙ ╶┼╴╺╋╸┠┼┨ ┝╋┥ ▋ ╲╱╲╱╳╳╳\n\r'); - term.write(' ║│╲ ╱│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╿ │┃ ┍╅╆┓ ╵ ╹ ┗┷┛ └┸┘ ▌ ╱╲╱╲╳╳╳\n\r'); - term.write(' ╠╡ ╳ ╞╣ ├╢ ╟┤ ├┼─┼─┼┤ ├╫─╂─╫┤ ┣┿╾┼╼┿┫ ┕┛┖┚ ┌┄┄┐ ╎ ┏┅┅┓ ┋ ▍ ╲╱╲╱╳╳╳\n\r'); - term.write(' ║│╱ ╲│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╽ │┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▎\n\r'); - term.write(' ║└─╥─┘║ │╚═╤═╝│ │╘═╪═╛│ │╙─╀─╜│ ┃└─╂─┘┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▏\n\r'); - term.write(' ╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█\n\r'); - term.write('\x1b[0m'); - window.scrollTo(0, 0); -} - -function loadTest(): void { - const rendererName = addons.webgl.instance ? 'webgl' : 'dom'; - const testData = []; - let byteCount = 0; - for (let i = 0; i < 50; i++) { - const count = 1 + Math.floor(Math.random() * 79); - byteCount += count + 2; - const data = new Uint8Array(count + 2); - data[0] = 0x0A; // \n - for (let i = 1; i < count + 1; i++) { - data[i] = 0x61 + Math.floor(Math.random() * (0x7A - 0x61)); - } - // End each line with \r so the cursor remains constant, this is what ls/tree do and improves - // performance significantly due to the cursor DOM element not needing to change - data[data.length - 1] = 0x0D; // \r - testData.push(data); - } - const start = performance.now(); - for (let i = 0; i < 1024; i++) { - for (const d of testData) { - term.write(d); - } - } - // Wait for all data to be parsed before evaluating time - term.write('', () => { - const time = Math.round(performance.now() - start); - const mbs = ((byteCount / 1024) * (1 / (time / 1000))).toFixed(2); - term.write(`\n\r\nWrote ${byteCount}kB in ${time}ms (${mbs}MB/s) using the (${rendererName} renderer)`); - // Send ^C to get a new prompt - term._core._onData.fire('\x03'); - }); -} - -function loadTestLongLines(): void { - const rendererName = addons.webgl.instance ? 'webgl' : 'dom'; - const testData = []; - let byteCount = 0; - for (let i = 0; i < 50; i++) { - const count = 1 + Math.floor(Math.random() * 500); - byteCount += count + 2; - const data = new Uint8Array(count + 2); - data[0] = 0x0A; // \n - for (let i = 1; i < count + 1; i++) { - data[i] = 0x61 + Math.floor(Math.random() * (0x7A - 0x61)); - } - // End each line with \r so the cursor remains constant, this is what ls/tree do and improves - // performance significantly due to the cursor DOM element not needing to change - data[data.length - 1] = 0x0D; // \r - testData.push(data); - } - const start = performance.now(); - for (let i = 0; i < 1024 * 50; i++) { - for (const d of testData) { - term.write(d); - } - } - // Wait for all data to be parsed before evaluating time - term.write('', () => { - const time = Math.round(performance.now() - start); - const mbs = ((byteCount / 1024) * (1 / (time / 1000))).toFixed(2); - term.write(`\n\r\nWrote ${byteCount}kB in ${time}ms (${mbs}MB/s) using the (${rendererName} renderer)`); - // Send ^C to get a new prompt - term._core._onData.fire('\x03'); - }); -} - -function powerlineSymbolTest(): void { - function s(char: string): string { - return `${char} \x1b[7m${char}\x1b[0m `; - } - term.write('\n\n\r'); - term.writeln('Standard powerline symbols:'); - term.writeln(' 0 1 2 3 4 5 6 7 8 9 A B C D E F'); - term.writeln(`0xA_ ${s('\ue0a0')}${s('\ue0a1')}${s('\ue0a2')}`); - term.writeln(`0xB_ ${s('\ue0b0')}${s('\ue0b1')}${s('\ue0b2')}${s('\ue0b3')}`); - term.writeln(''); - term.writeln( - `\x1b[7m` + - ` inverse \ue0b1 \x1b[0;40m\ue0b0` + - ` 0 \ue0b1 \x1b[30;41m\ue0b0\x1b[39m` + - ` 1 \ue0b1 \x1b[31;42m\ue0b0\x1b[39m` + - ` 2 \ue0b1 \x1b[32;43m\ue0b0\x1b[39m` + - ` 3 \ue0b1 \x1b[33;44m\ue0b0\x1b[39m` + - ` 4 \ue0b1 \x1b[34;45m\ue0b0\x1b[39m` + - ` 5 \ue0b1 \x1b[35;46m\ue0b0\x1b[39m` + - ` 6 \ue0b1 \x1b[36;47m\ue0b0\x1b[30m` + - ` 7 \ue0b1 \x1b[37;49m\ue0b0\x1b[0m` - ); - term.writeln(''); - term.writeln( - `\x1b[7m` + - ` inverse \ue0b3 \x1b[0;7;40m\ue0b2\x1b[27m` + - ` 0 \ue0b3 \x1b[7;30;41m\ue0b2\x1b[27;39m` + - ` 1 \ue0b3 \x1b[7;31;42m\ue0b2\x1b[27;39m` + - ` 2 \ue0b3 \x1b[7;32;43m\ue0b2\x1b[27;39m` + - ` 3 \ue0b3 \x1b[7;33;44m\ue0b2\x1b[27;39m` + - ` 4 \ue0b3 \x1b[7;34;45m\ue0b2\x1b[27;39m` + - ` 5 \ue0b3 \x1b[7;35;46m\ue0b2\x1b[27;39m` + - ` 6 \ue0b3 \x1b[7;36;47m\ue0b2\x1b[27;30m` + - ` 7 \ue0b3 \x1b[7;37;49m\ue0b2\x1b[0m` - ); - term.writeln(''); - term.writeln( - `\x1b[7m` + - ` inverse \ue0b5 \x1b[0;40m\ue0b4` + - ` 0 \ue0b5 \x1b[30;41m\ue0b4\x1b[39m` + - ` 1 \ue0b5 \x1b[31;42m\ue0b4\x1b[39m` + - ` 2 \ue0b5 \x1b[32;43m\ue0b4\x1b[39m` + - ` 3 \ue0b5 \x1b[33;44m\ue0b4\x1b[39m` + - ` 4 \ue0b5 \x1b[34;45m\ue0b4\x1b[39m` + - ` 5 \ue0b5 \x1b[35;46m\ue0b4\x1b[39m` + - ` 6 \ue0b5 \x1b[36;47m\ue0b4\x1b[30m` + - ` 7 \ue0b5 \x1b[37;49m\ue0b4\x1b[0m` - ); - term.writeln(''); - term.writeln( - `\x1b[7m` + - ` inverse \ue0b7 \x1b[0;7;40m\ue0b6\x1b[27m` + - ` 0 \ue0b7 \x1b[7;30;41m\ue0b6\x1b[27;39m` + - ` 1 \ue0b7 \x1b[7;31;42m\ue0b6\x1b[27;39m` + - ` 2 \ue0b7 \x1b[7;32;43m\ue0b6\x1b[27;39m` + - ` 3 \ue0b7 \x1b[7;33;44m\ue0b6\x1b[27;39m` + - ` 4 \ue0b7 \x1b[7;34;45m\ue0b6\x1b[27;39m` + - ` 5 \ue0b7 \x1b[7;35;46m\ue0b6\x1b[27;39m` + - ` 6 \ue0b7 \x1b[7;36;47m\ue0b6\x1b[27;30m` + - ` 7 \ue0b7 \x1b[7;37;49m\ue0b6\x1b[0m` - ); - term.writeln(''); - term.writeln('Powerline extra symbols:'); - term.writeln(' 0 1 2 3 4 5 6 7 8 9 A B C D E F'); - term.writeln(`0xA_ ${s('\ue0a3')}`); - term.writeln(`0xB_ ${s('\ue0b4')}${s('\ue0b5')}${s('\ue0b6')}${s('\ue0b7')}${s('\ue0b8')}${s('\ue0b9')}${s('\ue0ba')}${s('\ue0bb')}${s('\ue0bc')}${s('\ue0bd')}${s('\ue0be')}${s('\ue0bf')}`); - term.writeln(`0xC_ ${s('\ue0c0')}${s('\ue0c1')}${s('\ue0c2')}${s('\ue0c3')}${s('\ue0c4')}${s('\ue0c5')}${s('\ue0c6')}${s('\ue0c7')}${s('\ue0c8')}${s('\ue0c9')}${s('\ue0ca')}${s('\ue0cb')}${s('\ue0cc')}${s('\ue0cd')}${s('\ue0be')}${s('\ue0bf')}`); - term.writeln(`0xD_ ${s('\ue0d0')}${s('\ue0d1')}${s('\ue0d2')} ${s('\ue0d4')}`); - term.writeln(''); - term.writeln('Sample of nerd fonts icons:'); - term.writeln(' nf-linux-apple (\\uF302) \uf302'); - term.writeln('nf-mdi-github_face (\\uFbd9) \ufbd9'); -} - -function underlineTest(): void { - function u(style: number): string { - return `\x1b[4:${style}m`; - } - function c(color: string): string { - return `\x1b[58:${color}m`; - } - term.write('\n\n\r'); - term.writeln('Underline styles:'); - term.writeln(''); - function showSequence(id: number, name: string): string { - let alphabet = ''; - for (let i = 97; i < 123; i++) { - alphabet += String.fromCharCode(i); - } - let numbers = ''; - for (let i = 0; i < 10; i++) { - numbers += i.toString(); - } - return `${u(id)}4:${id}m - ${name}\x1b[4:0m`.padEnd(33, ' ') + `${u(id)}${alphabet} ${numbers} 汉语 한국어 👽\x1b[4:0m`; - } - term.writeln(showSequence(0, 'No underline')); - term.writeln(showSequence(1, 'Straight')); - term.writeln(showSequence(2, 'Double')); - term.writeln(showSequence(3, 'Curly')); - term.writeln(showSequence(4, 'Dotted')); - term.writeln(showSequence(5, 'Dashed')); - term.writeln(''); - term.writeln(`Underline colors (256 color mode):`); - term.writeln(''); - for (let i = 0; i < 256; i++) { - term.write((i !== 0 ? '\x1b[0m, ' : '') + u(1 + i % 5) + c('5:' + i) + i); - } - term.writeln(`\x1b[0m\n\n\rUnderline colors (true color mode):`); - term.writeln(''); - for (let i = 0; i < 80; i++) { - const v = Math.round(i / 79 * 255); - term.write(u(1) + c(`2:0:${v}:${v}:${v}`) + (i < 4 ? 'grey'[i] : ' ')); - } - term.write('\n\r'); - for (let i = 0; i < 80; i++) { - const v = Math.round(i / 79 * 255); - term.write(u(1) + c(`2:0:${v}:${0}:${0}`) + (i < 3 ? 'red'[i] : ' ')); - } - term.write('\n\r'); - for (let i = 0; i < 80; i++) { - const v = Math.round(i / 79 * 255); - term.write(u(1) + c(`2:0:${0}:${v}:${0}`) + (i < 5 ? 'green'[i] : ' ')); - } - term.write('\n\r'); - for (let i = 0; i < 80; i++) { - const v = Math.round(i / 79 * 255); - term.write(u(1) + c(`2:0:${0}:${0}:${v}`) + (i < 4 ? 'blue'[i] : ' ')); - } - term.write('\x1b[0m\n\r'); -} - -function ansiColorsTest(): void { - term.writeln(`\x1b[0m\n\n\rStandard colors: Bright colors:`); - for (let i = 0; i < 16; i++) { - term.write(`\x1b[48;5;${i}m ${i.toString().padEnd(2, ' ').padStart(3, ' ')} \x1b[0m`); - } - - term.writeln(`\x1b[0m\n\n\rColors 17-231 from 256 palette:`); - for (let i = 0; i < 6; i++) { - const startId = 16 + i * 36; - const endId = 16 + (i + 1) * 36 - 1; - term.write(`${startId.toString().padStart(3, ' ')}-${endId.toString().padStart(3, ' ')} `); - for (let j = 0; j < 36; j++) { - const id = 16 + i * 36 + j; - term.write(`\x1b[48;5;${id}m${(id % 10).toString().padStart(2, ' ')}\x1b[0m`); - } - term.write(`\r\n`); - } - - term.writeln(`\x1b[0m\n\rGreyscale from 256 palette:`); - term.write('232-255 '); - for (let i = 232; i < 256; i++) { - term.write(`\x1b[48;5;${i}m ${(i % 10)} \x1b[0m`); - } -} - -function writeTestString(): string { - let alphabet = ''; - for (let i = 97; i < 123; i++) { - alphabet += String.fromCharCode(i); - } - let numbers = ''; - for (let i = 0; i < 10; i++) { - numbers += i.toString(); - } - return `${alphabet} ${numbers} 汉语 한국어 👽`; -} -const testString = writeTestString(); - -function sgrTest(): void { - term.write('\n\n\r'); - term.writeln(`Character Attributes (SGR, Select Graphic Rendition)`); - const entries: { ps: number, name: string }[] = [ - { ps: 0, name: 'Normal' }, - { ps: 1, name: 'Bold' }, - { ps: 2, name: 'Faint/dim' }, - { ps: 3, name: 'Italicized' }, - { ps: 4, name: 'Underlined' }, - { ps: 5, name: 'Blink' }, - { ps: 7, name: 'Inverse' }, - { ps: 8, name: 'Invisible' }, - { ps: 9, name: 'Crossed-out characters' }, - { ps: 21, name: 'Doubly-underlined' }, - { ps: 22, name: 'Normal' }, - { ps: 23, name: 'Not italicized' }, - { ps: 24, name: 'Not underlined' }, - { ps: 25, name: 'Steady (not blink)' }, - { ps: 27, name: 'Positive (not inverse)' }, - { ps: 28, name: 'Visible (not hidden)' }, - { ps: 29, name: 'Not crossed-out' }, - { ps: 30, name: 'Foreground Black' }, - { ps: 31, name: 'Foreground Red' }, - { ps: 32, name: 'Foreground Green' }, - { ps: 33, name: 'Foreground Yellow' }, - { ps: 34, name: 'Foreground Blue' }, - { ps: 35, name: 'Foreground Magenta' }, - { ps: 36, name: 'Foreground Cyan' }, - { ps: 37, name: 'Foreground White' }, - { ps: 39, name: 'Foreground default' }, - { ps: 40, name: 'Background Black' }, - { ps: 41, name: 'Background Red' }, - { ps: 42, name: 'Background Green' }, - { ps: 43, name: 'Background Yellow' }, - { ps: 44, name: 'Background Blue' }, - { ps: 45, name: 'Background Magenta' }, - { ps: 46, name: 'Background Cyan' }, - { ps: 47, name: 'Background White' }, - { ps: 49, name: 'Background default' }, - { ps: 53, name: 'Overlined' }, - { ps: 55, name: 'Not overlined' } - ]; - const maxNameLength = entries.reduce((p, c) => Math.max(c.name.length, p), 0); - for (const e of entries) { - term.writeln(`\x1b[0m\x1b[${e.ps}m ${e.ps.toString().padEnd(2, ' ')} ${e.name.padEnd(maxNameLength, ' ')} - ${testString}\x1b[0m`); - } - const entriesByPs: Map = new Map(); - for (const e of entries) { - entriesByPs.set(e.ps, e.name); - } - const comboEntries: { ps: number[] }[] = [ - { ps: [1, 2, 3, 4, 5, 6, 7, 9] }, - { ps: [2, 41] }, - { ps: [4, 53] } - ]; - term.write('\n\n\r'); - term.writeln(`Combinations`); - for (const e of comboEntries) { - const name = e.ps.map(e => entriesByPs.get(e)).join(', '); - term.writeln(`\x1b[0m\x1b[${e.ps.join(';')}m ${name}\n\r${testString}\x1b[0m`); - } -} - -function addAnsiHyperlink(): void { - term.write('\n\n\r'); - term.writeln(`Regular link with no id:`); - term.writeln('\x1b]8;;https://github.com\x07GitHub\x1b]8;;\x07'); - term.writeln('\x1b]8;;https://xtermjs.org\x07https://xtermjs.org\x1b]8;;\x07\x1b[C<- null cell'); - term.writeln(`\nAdjacent links:`); - term.writeln('\x1b]8;;https://github.com\x07GitHub\x1b]8;;https://xtermjs.org\x07\x1b[32mxterm.js\x1b[0m\x1b]8;;\x07'); - term.writeln(`\nShared ID link (underline should be shared):`); - term.writeln('╔════╗'); - term.writeln('║\x1b]8;id=testid;https://github.com\x07GitH\x1b]8;;\x07║'); - term.writeln('║\x1b]8;id=testid;https://github.com\x07ub\x1b]8;;\x07 ║'); - term.writeln('╚════╝'); - term.writeln(`\nWrapped link with no ID (not necessarily meant to share underline):`); - term.writeln('╔════╗'); - term.writeln('║ ║'); - term.writeln('║ ║'); - term.writeln('╚════╝'); - term.write('\x1b[3A\x1b[1C\x1b]8;;https://xtermjs.org\x07xter\x1b[B\x1b[4Dm.js\x1b]8;;\x07\x1b[2B\x1b[5D'); -} - -/** - * Prints the 20977 characters from the CJK Unified Ideographs unicode block. - */ -function addCjk(): void { - term.write('\n\n\r'); - for (let i = 0x4E00; i < 0x9FCC; i++) { - term.write(String.fromCharCode(i)); - } -} - -/** - * Prints the 20977 characters from the CJK Unified Ideographs unicode block with randomized styles. - */ -function addCjkRandomSgr(): void { - term.write('\n\n\r'); - for (let i = 0x4E00; i < 0x9FCC; i++) { - term.write(`\x1b[${getRandomSgr()}m${String.fromCharCode(i)}\x1b[0m`); - } -} -const randomSgrAttributes = [ - '1', '2', '3', '4', '5', '6', '7', '9', - '21', '22', '23', '24', '25', '26', '27', '28', '29', - '30', '31', '32', '33', '34', '35', '36', '37', '38', '39', - '40', '41', '42', '43', '44', '45', '46', '47', '48', '49' -]; -function getRandomSgr(): string { - return randomSgrAttributes[Math.floor(Math.random() * randomSgrAttributes.length)]; -} - -function addGraphemeClusters(): void { - term.write('\n\n\r'); - term.writeln('🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣 [Simple emoji v6: 10 cells, v15: 20 cells]'); - term.writeln('\u{1F476}\u{1F3FF}\u{1F476} [baby with emoji modifier fitzpatrick type-6; baby]'); - term.writeln('\u{1F469}\u200d\u{1f469}\u200d\u{1f466} [woman+zwj+woman+zwj+boy]'); - term.writeln('\u{1F64B}\u{1F64B}\u{200D}\u{2642}\u{FE0F} [person/man raising hand]'); - term.writeln('\u{1F3CB}\u{FE0F}=\u{1F3CB}\u{1F3FE}\u{200D}\u{2640}\u{FE0F} [person lifting weights emoji; woman lighting weights, medium dark]'); - term.writeln('\u{1F469}\u{1F469}\u{200D}\u{1F393}\u{1F468}\u{1F3FF}\u{200D}\u{1F393} [woman; woman student; man student dark]'); - term.writeln('\u{1f1f3}\u{1f1f4}_ [REGIONAL INDICATOR SYMBOL LETTER N and RI O]'); - term.writeln('\u{1f1f3}_\u{1f1f4} {RI N; underscore; RI O]'); - term.writeln('\u0061\u0301 [letter a with acute accent]'); - term.writeln('\u1100\u1161\u11A8=\u1100\u1161= [Korean Jamo]'); - term.writeln('\uAC00=\uD685= [Hangul syllables (pre-composed)]'); - term.writeln('(\u26b0\ufe0e) [coffin with text_presentation]'); - term.writeln('(\u26b0\ufe0f) [coffin with Emoji_presentation]'); - term.writeln(' [Égalité (using separate acute) emoij_presentation]'); -} - -function addDecoration(): void { - term.options['overviewRuler'] = { width: 14 }; - const marker = term.registerMarker(1); - const decoration = term.registerDecoration({ - marker, - backgroundColor: '#00FF00', - foregroundColor: '#00FE00', - overviewRulerOptions: { color: '#ef292980', position: 'left' } - }); - decoration.onRender((e: HTMLElement) => { - e.style.right = '100%'; - e.style.backgroundColor = '#ef292980'; - }); -} - -function addOverviewRuler(): void { - term.options['overviewRuler'] = { width: 14 }; - term.registerDecoration({ marker: term.registerMarker(1), overviewRulerOptions: { color: '#ef2929' } }); - term.registerDecoration({ marker: term.registerMarker(3), overviewRulerOptions: { color: '#8ae234' } }); - term.registerDecoration({ marker: term.registerMarker(5), overviewRulerOptions: { color: '#729fcf' } }); - term.registerDecoration({ marker: term.registerMarker(7), overviewRulerOptions: { color: '#ef2929', position: 'left' } }); - term.registerDecoration({ marker: term.registerMarker(7), overviewRulerOptions: { color: '#8ae234', position: 'center' } }); - term.registerDecoration({ marker: term.registerMarker(7), overviewRulerOptions: { color: '#729fcf', position: 'right' } }); - term.registerDecoration({ marker: term.registerMarker(10), overviewRulerOptions: { color: '#8ae234', position: 'center' } }); - term.registerDecoration({ marker: term.registerMarker(10), overviewRulerOptions: { color: '#ffffff80', position: 'full' } }); -} - -let decorationStressTestDecorations: IDisposable[] | undefined; -function decorationStressTest(): void { - if (decorationStressTestDecorations) { - for (const d of decorationStressTestDecorations) { - d.dispose(); - } - decorationStressTestDecorations = undefined; - } else { - const t = term as Terminal; - const buffer = t.buffer.active; - const cursorY = buffer.baseY + buffer.cursorY; - decorationStressTestDecorations = []; - for (const x of [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]) { - for (let y = 0; y < t.buffer.active.length; y++) { - const cursorOffsetY = y - cursorY; - decorationStressTestDecorations.push(t.registerDecoration({ - marker: t.registerMarker(cursorOffsetY), - x, - width: 4, - backgroundColor: '#FF0000', - overviewRulerOptions: { color: '#FF0000' } - })); - } - } - } -} - -(console as any).image = (source: ImageData | HTMLCanvasElement, scale: number = 1) => { - function getBox(width: number, height: number): any { - return { - string: '+', - style: 'font-size: 1px; padding: ' + Math.floor(height/2) + 'px ' + Math.floor(width/2) + 'px; line-height: ' + height + 'px;' - }; - } - if (source instanceof HTMLCanvasElement) { - source = source.getContext('2d')?.getImageData(0, 0, source.width, source.height)!; - } - const canvas = document.createElement('canvas'); - canvas.width = source.width; - canvas.height = source.height; - const ctx = canvas.getContext('2d')!; - ctx.putImageData(source, 0, 0); - - const sw = source.width * scale; - const sh = source.height * scale; - const dim = getBox(sw, sh); - console.log( - `Image: ${source.width} x ${source.height}\n%c${dim.string}`, - `${dim.style}background: url(${canvas.toDataURL()}); background-size: ${sw}px ${sh}px; background-repeat: no-repeat; color: transparent;` - ); - console.groupCollapsed('Zoomed'); - console.log( - `%c${dim.string}`, - `${getBox(sw * 10, sh * 10).style}background: url(${canvas.toDataURL()}); background-size: ${sw * 10}px ${sh * 10}px; background-repeat: no-repeat; color: transparent; image-rendering: pixelated;-ms-interpolation-mode: nearest-neighbor;` - ); - console.groupEnd(); -}; - -function addVtButtons(): void { - function csi(e: string): string { - return `\x1b[${e}`; - } - - function createButton(name: string, description: string, writeCsi: string, paramCount: number = 1): HTMLElement { - const inputs: HTMLInputElement[] = []; - for (let i = 0; i < paramCount; i++) { - const input = document.createElement('input'); - input.type = 'number'; - input.title = `Input #${i + 1}`; - inputs.push(input); - } - - const element = document.createElement('button'); - element.textContent = name; - writeCsi.split(''); - const prefix = writeCsi.length === 2 ? writeCsi[0] : ''; - const suffix = writeCsi[writeCsi.length - 1]; - element.addEventListener(`click`, () => term.write(csi(`${prefix}${inputs.map(e => e.value).join(';')}${suffix}`))); - - const desc = document.createElement('span'); - desc.textContent = description; - - const container = document.createElement('div'); - container.classList.add('vt-button'); - container.append(element, ...inputs, desc); - return container; - } - const vtFragment = document.createDocumentFragment(); - const buttonSpecs: { [key: string]: { label: string, description: string, paramCount?: number }} = { - A: { label: 'CUU ↑', description: 'Cursor Up Ps Times' }, - B: { label: 'CUD ↓', description: 'Cursor Down Ps Times' }, - C: { label: 'CUF →', description: 'Cursor Forward Ps Times' }, - D: { label: 'CUB ←', description: 'Cursor Backward Ps Times' }, - E: { label: 'CNL', description: 'Cursor Next Line Ps Times' }, - F: { label: 'CPL', description: 'Cursor Preceding Line Ps Times' }, - G: { label: 'CHA', description: 'Cursor Character Absolute' }, - H: { label: 'CUP', description: 'Cursor Position [row;column]', paramCount: 2 }, - I: { label: 'CHT', description: 'Cursor Forward Tabulation Ps tab stops' }, - J: { label: 'ED', description: 'Erase in Display' }, - '?J': { label: 'DECSED', description: 'Erase in Display' }, - K: { label: 'EL', description: 'Erase in Line' }, - '?K': { label: 'DECSEL', description: 'Erase in Line' }, - L: { label: 'IL', description: 'Insert Ps Line(s)' }, - M: { label: 'DL', description: 'Delete Ps Line(s)' }, - P: { label: 'DCH', description: 'Delete Ps Character(s)' } - }; - for (const s of Object.keys(buttonSpecs)) { - const spec = buttonSpecs[s]; - vtFragment.appendChild(createButton(spec.label, spec.description, s, spec.paramCount)); - } - - document.querySelector('#vt-container').appendChild(vtFragment); -} - -function testWeblinks(): void { - const linkExamples = ` -aaa http://example.com aaa http://example.com aaa -¥¥¥ http://example.com aaa http://example.com aaa -aaa http://example.com ¥¥¥ http://example.com aaa -¥¥¥ http://example.com ¥¥¥ http://example.com aaa -aaa https://ko.wikipedia.org/wiki/위키백과:대문 aaa https://ko.wikipedia.org/wiki/위키백과:대문 aaa -¥¥¥ https://ko.wikipedia.org/wiki/위키백과:대문 aaa https://ko.wikipedia.org/wiki/위키백과:대문 ¥¥¥ -aaa http://test:password@example.com/some_path aaa -brackets enclosed: -aaa [http://example.de] aaa -aaa (http://example.de) aaa -aaa aaa -aaa {http://example.de} aaa -ipv6 https://[::1]/with/some?vars=and&a#hash aaa -stop at final '.': This is a sentence with an url to http://example.com. -stop at final '?': Is this the right url http://example.com/? -stop at final '?': Maybe this one http://example.com/with?arguments=false? -`; - term.write(linkExamples.split('\n').join('\r\n')); -} - - -function coloredErase(): void { - const sp5 = ' '; - const data = ` -Test BG-colored Erase (BCE): - The color block in the following lines should look identical. - For newly created rows at the bottom the last color should be applied - for all cells to the right. - - def 41 42 43 44 45 46 47\x1b[47m -\x1b[m${sp5}\x1b[41m${sp5}\x1b[42m${sp5}\x1b[43m${sp5}\x1b[44m${sp5}\x1b[45m${sp5}\x1b[46m${sp5}\x1b[47m${sp5} -\x1b[m\x1b[5X\x1b[41m\x1b[5C\x1b[5X\x1b[42m\x1b[5C\x1b[5X\x1b[43m\x1b[5C\x1b[5X\x1b[44m\x1b[5C\x1b[5X\x1b[45m\x1b[5C\x1b[5X\x1b[46m\x1b[5C\x1b[5X\x1b[47m\x1b[5C\x1b[5X\x1b[m -`; - term.write(data.split('\n').join('\r\n')); -} - - -function initImageAddonExposed(): void { - const DEFAULT_OPTIONS: IImageAddonOptions = (addons.image.instance as any)._defaultOpts; - const limitStorageElement = document.querySelector('#image-storagelimit'); - limitStorageElement.valueAsNumber = addons.image.instance.storageLimit; - addDomListener(limitStorageElement, 'change', () => { - try { - addons.image.instance.storageLimit = limitStorageElement.valueAsNumber; - limitStorageElement.valueAsNumber = addons.image.instance.storageLimit; - console.log('changed storageLimit to', addons.image.instance.storageLimit); - } catch (e) { - limitStorageElement.valueAsNumber = addons.image.instance.storageLimit; - console.log('storageLimit at', addons.image.instance.storageLimit); - throw e; - } - }); - const showPlaceholderElement = document.querySelector('#image-showplaceholder'); - showPlaceholderElement.checked = addons.image.instance.showPlaceholder; - addDomListener(showPlaceholderElement, 'change', () => { - addons.image.instance.showPlaceholder = showPlaceholderElement.checked; - }); - const ctorOptionsElement = document.querySelector('#image-options'); - ctorOptionsElement.value = JSON.stringify(DEFAULT_OPTIONS, null, 2); - - const sixelDemo = (url: string) => () => fetch(url) - .then(resp => resp.arrayBuffer()) - .then(buffer => { - term.write('\r\n'); - term.write(new Uint8Array(buffer)); - }); - - const iipDemo = (url: string) => () => fetch(url) - .then(resp => resp.arrayBuffer()) - .then(buffer => { - const data = new Uint8Array(buffer); - let sdata = ''; - for (let i = 0; i < data.length; ++i) sdata += String.fromCharCode(data[i]); - term.write('\r\n'); - term.write(`\x1b]1337;File=inline=1;size=${data.length}:${btoa(sdata)}\x1b\\`); - }); - - document.getElementById('image-demo1').addEventListener('click', - sixelDemo('https://raw.githubusercontent.com/saitoha/libsixel/master/images/snake.six')); - document.getElementById('image-demo2').addEventListener('click', - sixelDemo('https://raw.githubusercontent.com/jerch/node-sixel/master/testfiles/test2.sixel')); - document.getElementById('image-demo3').addEventListener('click', - iipDemo('https://raw.githubusercontent.com/jerch/node-sixel/master/palette.png')); - - // demo for image retrieval API - term.element.addEventListener('click', (ev: MouseEvent) => { - if (!ev.ctrlKey || !addons.image.instance) return; - - // TODO... - // if (ev.altKey) { - // const sel = term.getSelectionPosition(); - // if (sel) { - // addons.image.instance - // .extractCanvasAtBufferRange(term.getSelectionPosition()) - // ?.toBlob(data => window.open(URL.createObjectURL(data), '_blank')); - // return; - // } - // } - - const pos = term._core._mouseService!.getCoords(ev, term._core.screenElement!, term.cols, term.rows); - const x = pos[0] - 1; - const y = pos[1] - 1; - const canvas = ev.shiftKey - // ctrl+shift+click: get single tile - ? addons.image.instance.extractTileAtBufferCell(x, term.buffer.active.viewportY + y) - // ctrl+click: get original image - : addons.image.instance.getImageAtBufferCell(x, term.buffer.active.viewportY + y); - canvas?.toBlob(data => window.open(URL.createObjectURL(data), '_blank')); - }); -} - -function testEvents(): void { - document.getElementById('event-focus').addEventListener('click', ()=> term.focus()); - document.getElementById('event-blur').addEventListener('click', ()=> term.blur()); -} - -function testWebfonts() { - document.getElementById('webfont-kongtext').addEventListener('click', async () => { - const ff = new FontFace('Kongtext', "url(/kongtext.regular.ttf) format('truetype')"); - await loadFonts([ff]); - term.options.fontFamily = 'Kongtext'; - term.options.lineHeight = 1.3; - addons.fit.instance?.fit(); - setTimeout(() => term.write('\x1b[?12h\x1b]12;#776CF9\x07\x1b[38;2;119;108;249;48;2;21;8;150m\x1b[2J\x1b[2;5H**** COMMODORE 64 BASIC V2 ****\r\n\r\n 64K RAM SYSTEM 38911 BASIC BYTES FREE\r\n\r\nREADY.\r\nLOAD '), 1000); - setTimeout(() => {term.write('🤣\x1b[m\x1b[99;1H'); term.input('\r');}, 5000); - }); - document.getElementById('webfont-bpdots').addEventListener('click', async () => { - document.styleSheets[0].insertRule("@font-face { font-family: 'BPdots'; src: url(/bpdots.regular.otf) format('opentype'); weight: 400 }", 0); - await loadFonts(['BPdots']); - term.options.fontFamily = 'BPdots'; - term.options.lineHeight = 1.3; - term.options.fontSize = 20; - addons.fit.instance?.fit(); - }); -} diff --git a/demo/client/client.ts b/demo/client/client.ts index 6a0cdead8..68202ad7c 100644 --- a/demo/client/client.ts +++ b/demo/client/client.ts @@ -18,6 +18,7 @@ import { AttachAddon } from '@xterm/addon-attach'; import { AddonImageWindow } from './components/window/addonImageWindow'; import { AddonSearchWindow } from './components/window/addonSearchWindow'; import { AddonSerializeWindow } from './components/window/addonSerializeWindow'; +import { AddonWebFontsWindow } from './components/window/addonWebFontsWindow'; import { AddonsWindow } from './components/window/addonsWindow'; import { CellInspectorWindow } from './components/window/cellInspectorWindow'; import { ControlBar } from './components/controlBar'; @@ -32,6 +33,7 @@ import { LigaturesAddon } from '@xterm/addon-ligatures'; import { ProgressAddon } from '@xterm/addon-progress'; import { SearchAddon, ISearchOptions } from '@xterm/addon-search'; import { SerializeAddon } from '@xterm/addon-serialize'; +import { WebFontsAddon } from '@xterm/addon-web-fonts'; import { WebLinksAddon } from '@xterm/addon-web-links'; import { WebglAddon } from '@xterm/addon-webgl'; import { Unicode11Addon } from '@xterm/addon-unicode11'; @@ -75,6 +77,7 @@ const addons: AddonCollection = { progress: { name: 'progress', ctor: ProgressAddon, canChange: true }, search: { name: 'search', ctor: SearchAddon, canChange: true }, serialize: { name: 'serialize', ctor: SerializeAddon, canChange: true }, + webFonts: { name: 'webFonts', ctor: WebFontsAddon, canChange: true }, webLinks: { name: 'webLinks', ctor: WebLinksAddon, canChange: true }, webgl: { name: 'webgl', ctor: WebglAddon, canChange: true }, unicode11: { name: 'unicode11', ctor: Unicode11Addon, canChange: true }, @@ -213,7 +216,8 @@ if (document.location.pathname === '/test') { addonSearchWindow = controlBar.registerWindow(new AddonSearchWindow(typedTerm, addons), { afterId: 'addons', hidden: true, italics: true }); controlBar.registerWindow(new AddonSerializeWindow(typedTerm, addons), { afterId: 'addon-search', hidden: true, italics: true }); controlBar.registerWindow(new AddonImageWindow(typedTerm, addons), { afterId: 'addon-serialize', hidden: true, italics: true }); - addonWebglWindow = controlBar.registerWindow(new WebglWindow(typedTerm, addons), { afterId: 'addon-image', hidden: true, italics: true }); + controlBar.registerWindow(new AddonWebFontsWindow(typedTerm, addons), { afterId: 'addon-image', hidden: true, italics: true }); + addonWebglWindow = controlBar.registerWindow(new WebglWindow(typedTerm, addons), { afterId: 'addon-web-fonts', hidden: true, italics: true }); controlBar.registerWindow(new TestWindow(typedTerm, addons, { disposeRecreateButtonHandler, createNewWindowButtonHandler }), { afterId: 'options' }); actionElements = { findNext: addonSearchWindow.findNextInput, @@ -229,6 +233,7 @@ if (document.location.pathname === '/test') { controlBar.setTabVisible('addon-search', true); controlBar.setTabVisible('addon-serialize', true); controlBar.setTabVisible('addon-image', true); + controlBar.setTabVisible('addon-web-fonts', true); addonWebglWindow.setTextureAtlas(addons.webgl.instance.textureAtlas); addons.webgl.instance.onChangeTextureAtlas(e => addonWebglWindow.setTextureAtlas(e)); addons.webgl.instance.onAddTextureAtlasCanvas(e => addonWebglWindow.appendTextureAtlas(e)); @@ -295,6 +300,7 @@ function createTerminal(): Terminal { console.warn(e); } addons.webLinks.instance = new WebLinksAddon(); + addons.webFonts.instance = new WebFontsAddon(); typedTerm.loadAddon(addons.fit.instance); typedTerm.loadAddon(addons.image.instance); typedTerm.loadAddon(addons.progress.instance); @@ -302,6 +308,7 @@ function createTerminal(): Terminal { typedTerm.loadAddon(addons.serialize.instance); typedTerm.loadAddon(addons.unicodeGraphemes.instance); typedTerm.loadAddon(addons.webLinks.instance); + typedTerm.loadAddon(addons.webFonts.instance); typedTerm.loadAddon(addons.clipboard.instance); window.term = term; // Expose `term` to window for debugging purposes diff --git a/demo/client/components/window/addonWebFontsWindow.ts b/demo/client/components/window/addonWebFontsWindow.ts new file mode 100644 index 000000000..a56f0a562 --- /dev/null +++ b/demo/client/components/window/addonWebFontsWindow.ts @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2024 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { loadFonts } from '@xterm/addon-web-fonts'; +import { BaseWindow } from './baseWindow'; +import type { IControlWindow } from '../controlBar'; + +export class AddonWebFontsWindow extends BaseWindow implements IControlWindow { + public readonly id = 'addon-web-fonts'; + public readonly label = 'web-fonts'; + + public build(container: HTMLElement): void { + const dl = document.createElement('dl'); + + // Kongtext font button + const dtKongtext = document.createElement('dt'); + dtKongtext.textContent = 'Kongtext'; + dl.appendChild(dtKongtext); + + const ddKongtext = document.createElement('dd'); + const btnKongtext = document.createElement('button'); + btnKongtext.textContent = 'Load Kongtext'; + btnKongtext.title = 'Load Kongtext font and apply C64 style'; + btnKongtext.addEventListener('click', async () => { + const ff = new FontFace('Kongtext', 'url(/kongtext.regular.ttf) format(\'truetype\')'); + await loadFonts([ff]); + this._terminal.options.fontFamily = 'Kongtext'; + this._terminal.options.lineHeight = 1.3; + this._addons.fit.instance?.fit(); + setTimeout(() => this._terminal.write('\x1b[?12h\x1b]12;#776CF9\x07\x1b[38;2;119;108;249;48;2;21;8;150m\x1b[2J\x1b[2;5H**** COMMODORE 64 BASIC V2 ****\r\n\r\n 64K RAM SYSTEM 38911 BASIC BYTES FREE\r\n\r\nREADY.\r\nLOAD '), 1000); + setTimeout(() => { this._terminal.write('🤣\x1b[m\x1b[99;1H'); this._terminal.input('\r'); }, 5000); + }); + ddKongtext.appendChild(btnKongtext); + dl.appendChild(ddKongtext); + + // BPdots font button + const dtBpdots = document.createElement('dt'); + dtBpdots.textContent = 'BPdots'; + dl.appendChild(dtBpdots); + + const ddBpdots = document.createElement('dd'); + const btnBpdots = document.createElement('button'); + btnBpdots.textContent = 'Load BPdots'; + btnBpdots.title = 'Load BPdots font'; + btnBpdots.addEventListener('click', async () => { + document.styleSheets[0].insertRule('@font-face { font-family: "BPdots"; src: url(/bpdots.regular.otf) format("opentype"); weight: 400 }', 0); + await loadFonts(['BPdots']); + this._terminal.options.fontFamily = 'BPdots'; + this._terminal.options.lineHeight = 1.3; + this._terminal.options.fontSize = 20; + this._addons.fit.instance?.fit(); + }); + ddBpdots.appendChild(btnBpdots); + dl.appendChild(ddBpdots); + + container.appendChild(dl); + } +} diff --git a/demo/client/types.ts b/demo/client/types.ts index bc73ae0eb..c48452571 100644 --- a/demo/client/types.ts +++ b/demo/client/types.ts @@ -15,10 +15,11 @@ import type { SearchAddon } from '@xterm/addon-search'; import type { SerializeAddon } from '@xterm/addon-serialize'; import type { UnicodeGraphemesAddon } from '@xterm/addon-unicode-graphemes'; import type { Unicode11Addon } from '@xterm/addon-unicode11'; +import type { WebFontsAddon } from '@xterm/addon-web-fonts'; import type { WebLinksAddon } from '@xterm/addon-web-links'; import type { WebglAddon } from '@xterm/addon-webgl'; -export type AddonType = 'attach' | 'clipboard' | 'fit' | 'image' | 'progress' | 'search' | 'serialize' | 'unicode11' | 'unicodeGraphemes' | 'webLinks' | 'webgl' | 'ligatures'; +export type AddonType = 'attach' | 'clipboard' | 'fit' | 'image' | 'progress' | 'search' | 'serialize' | 'unicode11' | 'unicodeGraphemes' | 'webFonts' | 'webLinks' | 'webgl' | 'ligatures'; export interface IDemoAddon { name: T; @@ -32,6 +33,7 @@ export interface IDemoAddon { T extends 'progress' ? typeof ProgressAddon : T extends 'search' ? typeof SearchAddon : T extends 'serialize' ? typeof SerializeAddon : + T extends 'webFonts' ? typeof WebFontsAddon : T extends 'webLinks' ? typeof WebLinksAddon : T extends 'unicode11' ? typeof Unicode11Addon : T extends 'unicodeGraphemes' ? typeof UnicodeGraphemesAddon : @@ -47,6 +49,7 @@ export interface IDemoAddon { T extends 'progress' ? ProgressAddon : T extends 'search' ? SearchAddon : T extends 'serialize' ? SerializeAddon : + T extends 'webFonts' ? WebFontsAddon : T extends 'webLinks' ? WebLinksAddon : T extends 'unicode11' ? Unicode11Addon : T extends 'unicodeGraphemes' ? UnicodeGraphemesAddon : From 0e57643f1625b44c8e30a60808d544d09e128f1a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 3 Jan 2026 15:33:13 -0800 Subject: [PATCH 30/38] Move fonts into folder --- .../client/components/window/addonWebFontsWindow.ts | 4 ++-- demo/{ => fonts}/bpdots.regular.otf | Bin demo/{ => fonts}/font-licenses.txt | 0 demo/{ => fonts}/kongtext.regular.ttf | Bin demo/server/server.ts | 3 +-- 5 files changed, 3 insertions(+), 4 deletions(-) rename demo/{ => fonts}/bpdots.regular.otf (100%) rename demo/{ => fonts}/font-licenses.txt (100%) rename demo/{ => fonts}/kongtext.regular.ttf (100%) diff --git a/demo/client/components/window/addonWebFontsWindow.ts b/demo/client/components/window/addonWebFontsWindow.ts index a56f0a562..c8fb288d1 100644 --- a/demo/client/components/window/addonWebFontsWindow.ts +++ b/demo/client/components/window/addonWebFontsWindow.ts @@ -24,7 +24,7 @@ export class AddonWebFontsWindow extends BaseWindow implements IControlWindow { btnKongtext.textContent = 'Load Kongtext'; btnKongtext.title = 'Load Kongtext font and apply C64 style'; btnKongtext.addEventListener('click', async () => { - const ff = new FontFace('Kongtext', 'url(/kongtext.regular.ttf) format(\'truetype\')'); + const ff = new FontFace('Kongtext', 'url(/fonts/kongtext.regular.ttf) format(\'truetype\')'); await loadFonts([ff]); this._terminal.options.fontFamily = 'Kongtext'; this._terminal.options.lineHeight = 1.3; @@ -45,7 +45,7 @@ export class AddonWebFontsWindow extends BaseWindow implements IControlWindow { btnBpdots.textContent = 'Load BPdots'; btnBpdots.title = 'Load BPdots font'; btnBpdots.addEventListener('click', async () => { - document.styleSheets[0].insertRule('@font-face { font-family: "BPdots"; src: url(/bpdots.regular.otf) format("opentype"); weight: 400 }', 0); + document.styleSheets[0].insertRule('@font-face { font-family: "BPdots"; src: url(/fonts/bpdots.regular.otf) format("opentype"); weight: 400 }', 0); await loadFonts(['BPdots']); this._terminal.options.fontFamily = 'BPdots'; this._terminal.options.lineHeight = 1.3; diff --git a/demo/bpdots.regular.otf b/demo/fonts/bpdots.regular.otf similarity index 100% rename from demo/bpdots.regular.otf rename to demo/fonts/bpdots.regular.otf diff --git a/demo/font-licenses.txt b/demo/fonts/font-licenses.txt similarity index 100% rename from demo/font-licenses.txt rename to demo/fonts/font-licenses.txt diff --git a/demo/kongtext.regular.ttf b/demo/fonts/kongtext.regular.ttf similarity index 100% rename from demo/kongtext.regular.ttf rename to demo/fonts/kongtext.regular.ttf diff --git a/demo/server/server.ts b/demo/server/server.ts index 8733ff627..0f4a79b3d 100644 --- a/demo/server/server.ts +++ b/demo/server/server.ts @@ -45,8 +45,7 @@ function startServer(): void { res.sendFile(demoRoot + '/index.css'); }); - app.get('/kongtext.regular.ttf', (req, res) => res.sendFile(demoRoot + '/kongtext.regular.ttf')); - app.get('/bpdots.regular.otf', (req, res) => res.sendFile(demoRoot + '/bpdots.regular.otf')); + app.use('/fonts', express.static(demoRoot + '/fonts')); app.use('/dist', express.static(demoRoot + '/dist')); app.use('/src', express.static(demoRoot + '/src')); From b0145998c59c8350fd7710bc4fbfa93e52470046 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 3 Jan 2026 15:36:31 -0800 Subject: [PATCH 31/38] Fix lint --- demo/client/types.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/demo/client/types.ts b/demo/client/types.ts index c48452571..4790fd4f9 100644 --- a/demo/client/types.ts +++ b/demo/client/types.ts @@ -33,12 +33,12 @@ export interface IDemoAddon { T extends 'progress' ? typeof ProgressAddon : T extends 'search' ? typeof SearchAddon : T extends 'serialize' ? typeof SerializeAddon : - T extends 'webFonts' ? typeof WebFontsAddon : - T extends 'webLinks' ? typeof WebLinksAddon : - T extends 'unicode11' ? typeof Unicode11Addon : - T extends 'unicodeGraphemes' ? typeof UnicodeGraphemesAddon : - T extends 'webgl' ? typeof WebglAddon : - never + T extends 'webFonts' ? typeof WebFontsAddon : + T extends 'webLinks' ? typeof WebLinksAddon : + T extends 'unicode11' ? typeof Unicode11Addon : + T extends 'unicodeGraphemes' ? typeof UnicodeGraphemesAddon : + T extends 'webgl' ? typeof WebglAddon : + never ); instance?: ( T extends 'attach' ? AttachAddon : @@ -49,12 +49,12 @@ export interface IDemoAddon { T extends 'progress' ? ProgressAddon : T extends 'search' ? SearchAddon : T extends 'serialize' ? SerializeAddon : - T extends 'webFonts' ? WebFontsAddon : - T extends 'webLinks' ? WebLinksAddon : - T extends 'unicode11' ? Unicode11Addon : - T extends 'unicodeGraphemes' ? UnicodeGraphemesAddon : - T extends 'webgl' ? WebglAddon : - never + T extends 'webFonts' ? WebFontsAddon : + T extends 'webLinks' ? WebLinksAddon : + T extends 'unicode11' ? Unicode11Addon : + T extends 'unicodeGraphemes' ? UnicodeGraphemesAddon : + T extends 'webgl' ? WebglAddon : + never ); } From 2cf292986926cf960f29b93a69306ac52a0e8567 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 3 Jan 2026 15:36:36 -0800 Subject: [PATCH 32/38] Publish webfonts --- bin/publish.js | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/publish.js b/bin/publish.js index fd676b3aa..0fa2faddc 100644 --- a/bin/publish.js +++ b/bin/publish.js @@ -51,6 +51,7 @@ const addonPackageDirs = [ path.resolve(__dirname, '../addons/addon-serialize'), path.resolve(__dirname, '../addons/addon-unicode11'), path.resolve(__dirname, '../addons/addon-unicode-graphemes'), + path.resolve(__dirname, '../addons/addon-web-fonts'), path.resolve(__dirname, '../addons/addon-web-links'), path.resolve(__dirname, '../addons/addon-webgl') ]; From c6c4a2ffbce18a022ea40c761b15d79ac9c0d0e4 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 3 Jan 2026 15:39:24 -0800 Subject: [PATCH 33/38] Delete unwanted eslintrc --- .eslintrc.json | 273 ------------------------------------------------- 1 file changed, 273 deletions(-) delete mode 100644 .eslintrc.json diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 3d9322d36..000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,273 +0,0 @@ -{ - "env": { - "browser": true, - "es2021": true, - "node": true - }, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": [ - "demo/tsconfig.json", - "src/browser/tsconfig.json", - "src/common/tsconfig.json", - "src/headless/tsconfig.json", - "src/vs/tsconfig.json", - "test/benchmark/tsconfig.json", - "test/playwright/tsconfig.json", - "addons/addon-attach/src/tsconfig.json", - "addons/addon-attach/test/tsconfig.json", - "addons/addon-clipboard/src/tsconfig.json", - "addons/addon-clipboard/test/tsconfig.json", - "addons/addon-fit/src/tsconfig.json", - "addons/addon-fit/test/tsconfig.json", - "addons/addon-image/src/tsconfig.json", - "addons/addon-image/test/tsconfig.json", - "addons/addon-ligatures/src/tsconfig.json", - "addons/addon-search/src/tsconfig.json", - "addons/addon-search/test/tsconfig.json", - "addons/addon-serialize/src/tsconfig.json", - "addons/addon-serialize/test/tsconfig.json", - "addons/addon-serialize/benchmark/tsconfig.json", - "addons/addon-unicode11/src/tsconfig.json", - "addons/addon-unicode11/test/tsconfig.json", - "addons/addon-unicode-graphemes/src/tsconfig.json", - "addons/addon-unicode-graphemes/test/tsconfig.json", - "addons/addon-unicode-graphemes/benchmark/tsconfig.json", - "addons/addon-web-fonts/src/tsconfig.json", - "addons/addon-web-fonts/test/tsconfig.json", - "addons/addon-web-links/src/tsconfig.json", - "addons/addon-web-links/test/tsconfig.json", - "addons/addon-webgl/src/tsconfig.json", - "addons/addon-webgl/test/tsconfig.json" - ], - "sourceType": "module" - }, - "ignorePatterns": [ - "addons/*/src/third-party/*.ts", - "src/vs/*", - "out/*", - "out-test/*", - "out-esbuild/*", - "out-esbuild-test/*", - "**/inwasm-sdks/*", - "**/typings/*.d.ts", - "**/node_modules", - "**/*.js", - "**/*.mjs" - ], - "plugins": [ - "@stylistic/ts", - "@typescript-eslint", - "jsdoc" - ], - "rules": { - "@stylistic/ts/indent": [ - "warn", - 2 - ], - "@stylistic/ts/semi": [ - "warn", - "always" - ], - "@stylistic/ts/quotes": [ - "warn", - "single", - { "allowTemplateLiterals": true } - ], - - "@typescript-eslint/array-type": [ - "warn", - { - "default": "array", - "readonly": "generic" - } - ], - "@typescript-eslint/consistent-type-assertions": "warn", - "@typescript-eslint/consistent-type-definitions": "warn", - "@typescript-eslint/explicit-function-return-type": [ - "warn", - { - "allowExpressions": true - } - ], - "@typescript-eslint/explicit-member-accessibility": [ - "warn", - { - "accessibility": "explicit", - "overrides": { - "constructors": "off" - } - } - ], - "@typescript-eslint/member-delimiter-style": [ - "warn", - { - "multiline": { - "delimiter": "semi", - "requireLast": true - }, - "singleline": { - "delimiter": "comma", - "requireLast": false - } - } - ], - "@typescript-eslint/naming-convention": [ - "warn", - { "selector": "default", "format": ["camelCase"], - "filter": { - "regex": "^[a-z]", - "match": true - } - }, - // variableLike - { "selector": "variable", "format": ["camelCase", "UPPER_CASE"] }, - { "selector": "variable", "filter": "^I.+Service$", "format": ["PascalCase"], "prefix": ["I"] }, - // memberLike - { "selector": "memberLike", "modifiers": ["private"], "format": ["camelCase"], "leadingUnderscore": "require" }, - { "selector": "memberLike", "modifiers": ["protected"], "format": ["camelCase"], "leadingUnderscore": "require" }, - { "selector": "enumMember", "format": ["UPPER_CASE"] }, - // memberLike - Allow enum-like objects to use UPPER_CASE - { "selector": "property", "modifiers": ["public"], "format": ["camelCase", "UPPER_CASE"], - "filter": { - "regex": "^[a-z]", - "match": true - } - }, - // restrict on* naming for events only - { "selector": "method", "modifiers": ["public"], "format": ["camelCase", "UPPER_CASE"], "custom": { - "regex": "^on[A-Z].+", - "match": false - } }, - { "selector": "method", "modifiers": ["private"], "format": ["camelCase"], "leadingUnderscore": "require", "custom": { - "regex": "^on[A-Z].+", - "match": false - } }, - { "selector": "method", "modifiers": ["protected"], "format": ["camelCase"], "leadingUnderscore": "require", "custom": { - "regex": "^on[A-Z].+", - "match": false - } }, - // typeLike - { "selector": "typeLike", "format": ["PascalCase"] }, - { "selector": "interface", "format": ["PascalCase"], "prefix": ["I"] } - ], - "@typescript-eslint/no-confusing-void-expression": [ - "warn", - { "ignoreArrowShorthand": true } - ], - "@typescript-eslint/no-useless-constructor": "warn", - "@typescript-eslint/prefer-namespace-keyword": "warn", - "@typescript-eslint/type-annotation-spacing": "warn", - - "comma-dangle": [ - "warn", - { - "objects": "never", - "arrays": "never", - "functions": "never" - } - ], - "curly": [ - "warn", - "multi-line" - ], - "eol-last": "warn", - "eqeqeq": [ - "warn", - "always" - ], - "jsdoc/check-alignment": 1, - "jsdoc/check-param-names": 1, - "jsdoc/no-multi-asterisks": 1, - "keyword-spacing": "warn", - "max-len": [ - "warn", - { - "code": 1000, // Don't enforce for code - "comments": 100, - "ignoreTrailingComments": true, - "ignoreUrls": true, - "ignorePattern": "^ *((?(//|\\*) @vt)|(?\\* \\| )|(?// ))" - } - ], - "new-parens": "warn", - "no-duplicate-imports": "warn", - "no-else-return": [ - "warn", - { - "allowElseIf": false - } - ], - "no-eval": "warn", - "no-extra-semi": "error", - "no-irregular-whitespace": "warn", - "no-restricted-imports": [ - "warn", - { - "patterns": [ - ".*\\/out\\/.*" - ] - } - ], - "no-restricted-syntax": [ - "warn", - { - "selector": "CallExpression[callee.name='requestAnimationFrame']", - "message": "The global requestAnimationFrame() should be avoided, call it on the parent window from ICoreBrowserService." - }, - { - "selector": "CallExpression[callee.name='cancelAnimationFrame']", - "message": "The global cancelAnimationFrame() should be avoided, call it on the parent window from ICoreBrowserService." - }, - { - "selector": "CallExpression > MemberExpression[object.name='window'][property.name='requestAnimationFrame']", - "message": "window.requestAnimationFrame() should be avoided, call it on the parent window from ICoreBrowserService." - }, - { - "selector": "CallExpression > MemberExpression[object.name='window'][property.name='cancelAnimationFrame']", - "message": "window.cancelAnimationFrame() should be avoided, call it on the parent window from ICoreBrowserService." - }, - { - "selector": "MemberExpression[object.name='window'][property.name='devicePixelRatio']", - "message": "window.devicePixelRatio should be avoided, get it from ICoreBrowserService." - } - ], - "no-trailing-spaces": "warn", - "no-unsafe-finally": "warn", - "no-unused-vars": ["warn", { - "vars": "all", - "args": "none" - }], - "no-var": "warn", - "one-var": [ - "warn", - "never" - ], - "object-curly-spacing": [ - "warn", - "always" - ], - "prefer-const": "warn", - "spaced-comment": [ - "warn", - "always", - { - "markers": ["/"], - "exceptions": ["-"] - } - ] - }, - "overrides": [ - { - "files": [ - "**/*.api.ts", - "**/*.test.ts" - ], - "rules": { - "object-curly-spacing": "off", - "max-len": "off", - "no-unused-vars": "off" - } - } - ] -} From 9148c80d1ac0e561f75cafea657d0b1ba4626442 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 4 Jan 2026 06:18:50 -0800 Subject: [PATCH 34/38] Update readme - Added x2 scale logo - Revised wording - Added new addons --- README.md | 44 +++++++++++++++++++++------------------- images/logo-full-x2.png | Bin 0 -> 23129 bytes typings/xterm.d.ts | 2 -- 3 files changed, 23 insertions(+), 23 deletions(-) create mode 100644 images/logo-full-x2.png diff --git a/README.md b/README.md index 4ff46aa67..e183bb563 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,29 @@ -# [![xterm.js logo](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 `
` 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 +56,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,33 +80,35 @@ 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. +Stable releases are done on an as needed basis, beta releases are continuously published off the `master` branch and this is what the principal implementation VS Code uses. Generally the beta builds are quite stable but can potentially contain breaking changes. -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). +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 @@ -238,6 +240,6 @@ Do you use xterm.js in your application as well? Please [open a Pull Request](ht 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)
+Copyright (c) 2017-2026, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)
Copyright (c) 2014-2017, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License)
Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) diff --git a/images/logo-full-x2.png b/images/logo-full-x2.png new file mode 100644 index 0000000000000000000000000000000000000000..5581282d5f24c8b7c4bdeeed79b3f93c6fc0efb2 GIT binary patch literal 23129 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 Date: Sun, 4 Jan 2026 06:27:22 -0800 Subject: [PATCH 35/38] Update readme 2 --- README.md | 21 +++++++++------------ images/logo-full-x2.png | Bin 23129 -> 0 bytes 2 files changed, 9 insertions(+), 12 deletions(-) delete mode 100644 images/logo-full-x2.png diff --git a/README.md b/README.md index e183bb563..d001f1606 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,4 @@ -
- -
+# [![xterm.js](images/logo-full.png)](https://xtermjs.org) 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/). @@ -106,26 +104,25 @@ Some APIs may be marked with *experimental*, these are added to enable experimen ## Releases -Stable releases are done on an as needed basis, beta releases are continuously published off the `master` branch and this is what the principal implementation VS Code uses. Generally the beta builds are quite stable but can potentially contain breaking changes. - -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). +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. @@ -234,7 +231,7 @@ 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 diff --git a/images/logo-full-x2.png b/images/logo-full-x2.png deleted file mode 100644 index 5581282d5f24c8b7c4bdeeed79b3f93c6fc0efb2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23129 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 Date: Sun, 4 Jan 2026 06:38:53 -0800 Subject: [PATCH 36/38] Clean up root, remove auto approve settings These are automatically auto approved in upcoming vscode --- .mailmap | 7 ------- .mocha.env | 1 - .mocharc.yml | 5 ----- .vscode/settings.json | 13 ------------- 4 files changed, 26 deletions(-) delete mode 100644 .mailmap delete mode 100644 .mocha.env delete mode 100644 .mocharc.yml diff --git a/.mailmap b/.mailmap deleted file mode 100644 index a8de62a9a..000000000 --- a/.mailmap +++ /dev/null @@ -1,7 +0,0 @@ -Antonis Kalipetis -Antonis Kalipetis -Daniel Imms -Paris Kasidiaris -Paris Kasidiaris -Thanasis Daglis -Thanasis Daglis diff --git a/.mocha.env b/.mocha.env deleted file mode 100644 index 78a9f9302..000000000 --- a/.mocha.env +++ /dev/null @@ -1 +0,0 @@ -NODE_PATH=./out diff --git a/.mocharc.yml b/.mocharc.yml deleted file mode 100644 index fb397fdd4..000000000 --- a/.mocharc.yml +++ /dev/null @@ -1,5 +0,0 @@ -require: - - source-map-support/register -reporter: spec -color: true -check-leaks: true diff --git a/.vscode/settings.json b/.vscode/settings.json index 1fe5ba080..849808a6a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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" } From 9db31db8861563b871a8d1160140c5614993392c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 5 Jan 2026 11:10:56 -0800 Subject: [PATCH 37/38] Add undefined checks in erase calls This is likely a race condition during a resize which is hard to say what the right behavior is. Instead of hard failing by throwing an error, silently ignore if this happens. Fixes #5587 --- src/common/InputHandler.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 1dd1640fd..e0e6253c3 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -1154,7 +1154,10 @@ export class InputHandler extends Disposable implements IInputHandler { * @param respectProtect Whether to respect the protection attribute (DECSCA). */ private _eraseInBufferLine(y: number, start: number, end: number, clearWrap: boolean = false, respectProtect: boolean = false): void { - const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!; + const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y); + if (!line) { + return; + } line.replaceCells( start, end, @@ -1224,7 +1227,10 @@ export class InputHandler extends Disposable implements IInputHandler { this._eraseInBufferLine(j, 0, this._activeBuffer.x + 1, true, respectProtect); if (this._activeBuffer.x + 1 >= this._bufferService.cols) { // Deleted entire previous line. This next line can no longer be wrapped. - this._activeBuffer.lines.get(j + 1)!.isWrapped = false; + const nextLine = this._activeBuffer.lines.get(j + 1); + if (nextLine) { + nextLine.isWrapped = false; + } } while (j--) { this._resetBufferLine(j, respectProtect); From cb35d1b4879f97b9b557eaf21e530898620e58aa Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 5 Jan 2026 12:31:54 -0800 Subject: [PATCH 38/38] Ensure mouse listeners are removed on dispose Also use ? over ! just in case Fixes #5586 --- src/browser/CoreBrowserTerminal.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/browser/CoreBrowserTerminal.ts b/src/browser/CoreBrowserTerminal.ts index d2407062d..abc363ed1 100644 --- a/src/browser/CoreBrowserTerminal.ts +++ b/src/browser/CoreBrowserTerminal.ts @@ -636,8 +636,8 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { // send event to CoreMouseService function sendEvent(ev: MouseEvent | WheelEvent): boolean { - // get mouse coordinates - const pos = self._mouseService!.getMouseReportCoords(ev, self.screenElement!); + // Get mouse coordinates + const pos = self._mouseService?.getMouseReportCoords(ev, self.screenElement!); if (!pos) { return false; } @@ -804,6 +804,16 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { // force initial onProtocolChange so we dont miss early mouse requests this.coreMouseService.activeProtocol = this.coreMouseService.activeProtocol; + // Ensure document-level listeners are removed on dispose + this._register(toDisposable(() => { + if (requestedEvents.mouseup) { + this._document!.removeEventListener('mouseup', requestedEvents.mouseup); + } + if (requestedEvents.mousedrag) { + this._document!.removeEventListener('mousemove', requestedEvents.mousedrag); + } + })); + /** * "Always on" event listeners. */

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