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/13] 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 8c47598205..3d9322d36c 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 0000000000..447eb79f4d --- /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 0000000000..bf506cf1cb --- /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 0000000000..1daac3768f --- /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 0000000000..91800abdb3 --- /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 0000000000..cd297308fd --- /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 0000000000..8682c53359 --- /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 0000000000..22834be116 --- /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 0000000000..120fccdc31 --- /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 0000000000..2d820dd1a6 --- /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 0000000000..42cab9b638 --- /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 0000000000..c73a60ba5c --- /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 49ba67438e..d125bfeedd 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 1fb8f7ac1e..3e32b42fac 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 5569bd1d46..db8939fa8a 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 7ca3b7a791..9bc8896eca 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/13] 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 91800abdb3..3cce50928c 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/13] 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 3cce50928c..e31326026c 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/13] fix integration test runner --- bin/esbuild.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/esbuild.mjs b/bin/esbuild.mjs index 9ed69edab8..95b3856e6a 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/13] 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 d2f39e6b88..eb9066d98f 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 0000000000..09bb01a98c --- /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 8682c53359..0000000000 --- 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 936467cc17..b7773e575f 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/13] 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 bf506cf1cb..bd1dedf4bd 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/13] cleanup API --- .github/workflows/ci.yml | 2 + addons/addon-web-fonts/README.md | 12 +- addons/addon-web-fonts/src/WebFontsAddon.ts | 134 +++++++++--------- .../typings/addon-web-fonts.d.ts | 55 ++++++- 4 files changed, 126 insertions(+), 77 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb9066d98f..f7616b7c02 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 bd1dedf4bd..e910c21e5b 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 e31326026c..219a64afeb 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 { + private _term: Terminal | undefined; + constructor(public forceInitialRelayout: boolean = true) { } - public dispose(): void { } - public activate(terminal: Terminal): void { + 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 42cab9b638..c353a421e9 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/13] 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 c353a421e9..9be3de216a 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 c73a60ba5c..f75d2d501b 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 064518475b..a567348190 100644 --- a/demo/index.html +++ b/demo/index.html @@ -116,6 +116,10 @@

Test

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 3e32b42fac..1fb8f7ac1e 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/13] 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 0000000000..5b1b5ba664 --- /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/13] 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 d5e9fc08cc..732efda5bf 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 a567348190..691871ee06 100644 --- a/demo/index.html +++ b/demo/index.html @@ -118,8 +118,8 @@

Test

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/13] 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 09bb01a98c..1315b1ee6a 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/13] 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 219a64afeb..4a9f7e7ab0 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/13] fix promise return, reject on missing family name, doc polish --- addons/addon-web-fonts/README.md | 159 +++++++++++++----- addons/addon-web-fonts/src/WebFontsAddon.ts | 13 +- .../typings/addon-web-fonts.d.ts | 6 +- 3 files changed, 130 insertions(+), 48 deletions(-) diff --git a/addons/addon-web-fonts/README.md b/addons/addon-web-fonts/README.md index e910c21e5b..ce43cbe5c5 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). - -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): +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 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 + +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). + +--- -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. +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 4a9f7e7ab0..cc0ccd1f5c 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 9be3de216a..c7a159ed1a 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; }