mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
@@ -62,6 +62,9 @@ jobs:
|
||||
./addons/addon-web-links/lib/* \
|
||||
./addons/addon-web-links/out/* \
|
||||
./addons/addon-web-links/out-*/* \
|
||||
./addons/addon-web-fonts/lib/* \
|
||||
./addons/addon-web-fonts/out/* \
|
||||
./addons/addon-web-fonts/out-*/* \
|
||||
./addons/addon-webgl/lib/* \
|
||||
./addons/addon-webgl/out/* \
|
||||
./addons/addon-webgl/out-*st/*
|
||||
@@ -223,6 +226,8 @@ jobs:
|
||||
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-unicode-graphemes
|
||||
- name: Integration tests (addon-unicode11)
|
||||
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-unicode11
|
||||
- name: Integration tests (addon-web-fonts)
|
||||
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-web-fonts
|
||||
- name: Integration tests (addon-web-links)
|
||||
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-web-links
|
||||
- name: Integration tests (addon-webgl)
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,220 @@
|
||||
## @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 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.
|
||||
|
||||
|
||||
### Static Preloading for the Rescue?
|
||||
|
||||
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
|
||||
<link rel="preload" as="font" href="/path/to/your/fontfile1.woff" type="font/woff2" crossorigin="anonymous">
|
||||
<link rel="preload" as="font" href="/path/to/your/fontfile2.woff" type="font/woff2" crossorigin="anonymous">
|
||||
...
|
||||
<!-- CSS with font-face rules matching the URLs above -->
|
||||
```
|
||||
Browsers also will resort to system fonts, if the preloading takes too long.
|
||||
So this solution has only a very limited scope.
|
||||
|
||||
|
||||
### Loading with WebFontsAddon
|
||||
|
||||
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 (Typescript):
|
||||
|
||||
```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 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.
|
||||
|
||||
To fix that, the webfonts addon provides a waiting condition (Typescript):
|
||||
```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 xyInstance = new XYAddon();
|
||||
terminal.loadAddon(xyInstance);
|
||||
|
||||
const webFontsInstance = new WebFontsAddon();
|
||||
terminal.loadAddon(webFontsInstance);
|
||||
|
||||
// wait for webfonts to be fully loaded
|
||||
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 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 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 xyInstance = new XYAddon();
|
||||
terminal.loadAddon(xyInstance);
|
||||
|
||||
// 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:
|
||||
```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
|
||||
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
|
||||
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();
|
||||
});
|
||||
```
|
||||
|
||||
### 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).
|
||||
|
||||
---
|
||||
|
||||
Also see the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/addon-web-fonts/typings/addon-web-fonts.d.ts).
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* 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 a font 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 a font family name conditionally.
|
||||
* @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(/"/g, '\\"')}"`;
|
||||
}
|
||||
|
||||
|
||||
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(', ');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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<FontFace[]> {
|
||||
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 => hashFontFace(ff));
|
||||
for (const font of fonts) {
|
||||
if (font instanceof FontFace) {
|
||||
const fontHashed = 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) {
|
||||
return Promise.reject(`font family "${font}" not registered in document.fonts`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.all(toLoad.map(ff => ff.load()));
|
||||
}
|
||||
|
||||
|
||||
export function loadFonts(fonts?: (string | FontFace)[]): Promise<FontFace[]> {
|
||||
return document.fonts.ready.then(() => _loadFonts(fonts));
|
||||
}
|
||||
|
||||
|
||||
export class WebFontsAddon implements ITerminalAddon, IWebFontsApi {
|
||||
private _term: Terminal | undefined;
|
||||
|
||||
constructor(public initialRelayout: boolean = true) { }
|
||||
|
||||
public dispose(): void {
|
||||
this._term = undefined;
|
||||
}
|
||||
|
||||
public activate(term: Terminal): void {
|
||||
this._term = term;
|
||||
if (this.initialRelayout) {
|
||||
document.fonts.ready.then(() => this.relayout());
|
||||
}
|
||||
}
|
||||
|
||||
public loadFonts(fonts?: (string | FontFace)[]): Promise<FontFace[]> {
|
||||
return loadFonts(fonts);
|
||||
}
|
||||
|
||||
public async relayout(): Promise<void> {
|
||||
if (!this._term) {
|
||||
return;
|
||||
}
|
||||
await document.fonts.ready;
|
||||
const family = this._term.options.fontFamily;
|
||||
const families = splitFamily(family);
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
await _loadFonts(dirty);
|
||||
if (this._term) {
|
||||
this._term.options.fontFamily = clean.length ? createFamily(clean) : 'monospace';
|
||||
this._term.options.fontFamily = family;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* 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.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<any> {
|
||||
return ctx.page.evaluate(`Array.from(document.fonts).map(ff => ({family: ff.family, status: ff.status}))`);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{ "path": "./src" },
|
||||
{ "path": "./test" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Copyright (c) 2024 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
|
||||
import { Terminal, ITerminalAddon } from '@xterm/xterm';
|
||||
|
||||
declare module '@xterm/addon-web-fonts' {
|
||||
|
||||
/**
|
||||
* Addon to use webfonts in xterm.js
|
||||
*/
|
||||
export class WebFontsAddon implements ITerminalAddon {
|
||||
/**
|
||||
* @param initialRelayout Force initial relayout, if a webfont was found (default true).
|
||||
*/
|
||||
constructor(initialRelayout?: 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<FontFace[]>;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* The returned promise will resolve, when font loading and layouting are done.
|
||||
*/
|
||||
public relayout(): Promise<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.
|
||||
*/
|
||||
function loadFonts(fonts?: (string | FontFace)[]): Promise<FontFace[]>;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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'
|
||||
};
|
||||
@@ -140,6 +140,7 @@ if (config.addon) {
|
||||
"@xterm/addon-progress": "./addons/addon-progress/lib/addon-progress.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",
|
||||
|
||||
@@ -51,6 +51,7 @@ const addonPackageDirs = [
|
||||
path.resolve(__dirname, '../addons/addon-serialize'),
|
||||
path.resolve(__dirname, '../addons/addon-unicode11'),
|
||||
path.resolve(__dirname, '../addons/addon-unicode-graphemes'),
|
||||
path.resolve(__dirname, '../addons/addon-web-fonts'),
|
||||
path.resolve(__dirname, '../addons/addon-web-links'),
|
||||
path.resolve(__dirname, '../addons/addon-webgl')
|
||||
];
|
||||
|
||||
@@ -30,6 +30,7 @@ const addons = [
|
||||
'serialize',
|
||||
'unicode-graphemes',
|
||||
'unicode11',
|
||||
'web-fonts',
|
||||
'web-links',
|
||||
'webgl',
|
||||
];
|
||||
|
||||
@@ -18,6 +18,7 @@ import { AttachAddon } from '@xterm/addon-attach';
|
||||
import { AddonImageWindow } from './components/window/addonImageWindow';
|
||||
import { AddonSearchWindow } from './components/window/addonSearchWindow';
|
||||
import { AddonSerializeWindow } from './components/window/addonSerializeWindow';
|
||||
import { AddonWebFontsWindow } from './components/window/addonWebFontsWindow';
|
||||
import { AddonsWindow } from './components/window/addonsWindow';
|
||||
import { CellInspectorWindow } from './components/window/cellInspectorWindow';
|
||||
import { ControlBar } from './components/controlBar';
|
||||
@@ -32,6 +33,7 @@ import { LigaturesAddon } from '@xterm/addon-ligatures';
|
||||
import { ProgressAddon } from '@xterm/addon-progress';
|
||||
import { SearchAddon, ISearchOptions } from '@xterm/addon-search';
|
||||
import { SerializeAddon } from '@xterm/addon-serialize';
|
||||
import { WebFontsAddon } from '@xterm/addon-web-fonts';
|
||||
import { WebLinksAddon } from '@xterm/addon-web-links';
|
||||
import { WebglAddon } from '@xterm/addon-webgl';
|
||||
import { Unicode11Addon } from '@xterm/addon-unicode11';
|
||||
@@ -75,6 +77,7 @@ const addons: AddonCollection = {
|
||||
progress: { name: 'progress', ctor: ProgressAddon, canChange: true },
|
||||
search: { name: 'search', ctor: SearchAddon, canChange: true },
|
||||
serialize: { name: 'serialize', ctor: SerializeAddon, canChange: true },
|
||||
webFonts: { name: 'webFonts', ctor: WebFontsAddon, canChange: true },
|
||||
webLinks: { name: 'webLinks', ctor: WebLinksAddon, canChange: true },
|
||||
webgl: { name: 'webgl', ctor: WebglAddon, canChange: true },
|
||||
unicode11: { name: 'unicode11', ctor: Unicode11Addon, canChange: true },
|
||||
@@ -213,7 +216,8 @@ if (document.location.pathname === '/test') {
|
||||
addonSearchWindow = controlBar.registerWindow(new AddonSearchWindow(typedTerm, addons), { afterId: 'addons', hidden: true, italics: true });
|
||||
controlBar.registerWindow(new AddonSerializeWindow(typedTerm, addons), { afterId: 'addon-search', hidden: true, italics: true });
|
||||
controlBar.registerWindow(new AddonImageWindow(typedTerm, addons), { afterId: 'addon-serialize', hidden: true, italics: true });
|
||||
addonWebglWindow = controlBar.registerWindow(new WebglWindow(typedTerm, addons), { afterId: 'addon-image', hidden: true, italics: true });
|
||||
controlBar.registerWindow(new AddonWebFontsWindow(typedTerm, addons), { afterId: 'addon-image', hidden: true, italics: true });
|
||||
addonWebglWindow = controlBar.registerWindow(new WebglWindow(typedTerm, addons), { afterId: 'addon-web-fonts', hidden: true, italics: true });
|
||||
controlBar.registerWindow(new TestWindow(typedTerm, addons, { disposeRecreateButtonHandler, createNewWindowButtonHandler }), { afterId: 'options' });
|
||||
actionElements = {
|
||||
findNext: addonSearchWindow.findNextInput,
|
||||
@@ -229,6 +233,7 @@ if (document.location.pathname === '/test') {
|
||||
controlBar.setTabVisible('addon-search', true);
|
||||
controlBar.setTabVisible('addon-serialize', true);
|
||||
controlBar.setTabVisible('addon-image', true);
|
||||
controlBar.setTabVisible('addon-web-fonts', true);
|
||||
addonWebglWindow.setTextureAtlas(addons.webgl.instance.textureAtlas);
|
||||
addons.webgl.instance.onChangeTextureAtlas(e => addonWebglWindow.setTextureAtlas(e));
|
||||
addons.webgl.instance.onAddTextureAtlasCanvas(e => addonWebglWindow.appendTextureAtlas(e));
|
||||
@@ -295,6 +300,7 @@ function createTerminal(): Terminal {
|
||||
console.warn(e);
|
||||
}
|
||||
addons.webLinks.instance = new WebLinksAddon();
|
||||
addons.webFonts.instance = new WebFontsAddon();
|
||||
typedTerm.loadAddon(addons.fit.instance);
|
||||
typedTerm.loadAddon(addons.image.instance);
|
||||
typedTerm.loadAddon(addons.progress.instance);
|
||||
@@ -302,6 +308,7 @@ function createTerminal(): Terminal {
|
||||
typedTerm.loadAddon(addons.serialize.instance);
|
||||
typedTerm.loadAddon(addons.unicodeGraphemes.instance);
|
||||
typedTerm.loadAddon(addons.webLinks.instance);
|
||||
typedTerm.loadAddon(addons.webFonts.instance);
|
||||
typedTerm.loadAddon(addons.clipboard.instance);
|
||||
|
||||
window.term = term; // Expose `term` to window for debugging purposes
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) 2024 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { loadFonts } from '@xterm/addon-web-fonts';
|
||||
import { BaseWindow } from './baseWindow';
|
||||
import type { IControlWindow } from '../controlBar';
|
||||
|
||||
export class AddonWebFontsWindow extends BaseWindow implements IControlWindow {
|
||||
public readonly id = 'addon-web-fonts';
|
||||
public readonly label = 'web-fonts';
|
||||
|
||||
public build(container: HTMLElement): void {
|
||||
const dl = document.createElement('dl');
|
||||
|
||||
// Kongtext font button
|
||||
const dtKongtext = document.createElement('dt');
|
||||
dtKongtext.textContent = 'Kongtext';
|
||||
dl.appendChild(dtKongtext);
|
||||
|
||||
const ddKongtext = document.createElement('dd');
|
||||
const btnKongtext = document.createElement('button');
|
||||
btnKongtext.textContent = 'Load Kongtext';
|
||||
btnKongtext.title = 'Load Kongtext font and apply C64 style';
|
||||
btnKongtext.addEventListener('click', async () => {
|
||||
const ff = new FontFace('Kongtext', 'url(/fonts/kongtext.regular.ttf) format(\'truetype\')');
|
||||
await loadFonts([ff]);
|
||||
this._terminal.options.fontFamily = 'Kongtext';
|
||||
this._terminal.options.lineHeight = 1.3;
|
||||
this._addons.fit.instance?.fit();
|
||||
setTimeout(() => this._terminal.write('\x1b[?12h\x1b]12;#776CF9\x07\x1b[38;2;119;108;249;48;2;21;8;150m\x1b[2J\x1b[2;5H**** COMMODORE 64 BASIC V2 ****\r\n\r\n 64K RAM SYSTEM 38911 BASIC BYTES FREE\r\n\r\nREADY.\r\nLOAD '), 1000);
|
||||
setTimeout(() => { this._terminal.write('🤣\x1b[m\x1b[99;1H'); this._terminal.input('\r'); }, 5000);
|
||||
});
|
||||
ddKongtext.appendChild(btnKongtext);
|
||||
dl.appendChild(ddKongtext);
|
||||
|
||||
// BPdots font button
|
||||
const dtBpdots = document.createElement('dt');
|
||||
dtBpdots.textContent = 'BPdots';
|
||||
dl.appendChild(dtBpdots);
|
||||
|
||||
const ddBpdots = document.createElement('dd');
|
||||
const btnBpdots = document.createElement('button');
|
||||
btnBpdots.textContent = 'Load BPdots';
|
||||
btnBpdots.title = 'Load BPdots font';
|
||||
btnBpdots.addEventListener('click', async () => {
|
||||
document.styleSheets[0].insertRule('@font-face { font-family: "BPdots"; src: url(/fonts/bpdots.regular.otf) format("opentype"); weight: 400 }', 0);
|
||||
await loadFonts(['BPdots']);
|
||||
this._terminal.options.fontFamily = 'BPdots';
|
||||
this._terminal.options.lineHeight = 1.3;
|
||||
this._terminal.options.fontSize = 20;
|
||||
this._addons.fit.instance?.fit();
|
||||
});
|
||||
ddBpdots.appendChild(btnBpdots);
|
||||
dl.appendChild(ddBpdots);
|
||||
|
||||
container.appendChild(dl);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
"@xterm/addon-progress": ["../../addons/addon-progress"],
|
||||
"@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"],
|
||||
|
||||
+14
-11
@@ -15,10 +15,11 @@ import type { SearchAddon } from '@xterm/addon-search';
|
||||
import type { SerializeAddon } from '@xterm/addon-serialize';
|
||||
import type { UnicodeGraphemesAddon } from '@xterm/addon-unicode-graphemes';
|
||||
import type { Unicode11Addon } from '@xterm/addon-unicode11';
|
||||
import type { WebFontsAddon } from '@xterm/addon-web-fonts';
|
||||
import type { WebLinksAddon } from '@xterm/addon-web-links';
|
||||
import type { WebglAddon } from '@xterm/addon-webgl';
|
||||
|
||||
export type AddonType = 'attach' | 'clipboard' | 'fit' | 'image' | 'progress' | 'search' | 'serialize' | 'unicode11' | 'unicodeGraphemes' | 'webLinks' | 'webgl' | 'ligatures';
|
||||
export type AddonType = 'attach' | 'clipboard' | 'fit' | 'image' | 'progress' | 'search' | 'serialize' | 'unicode11' | 'unicodeGraphemes' | 'webFonts' | 'webLinks' | 'webgl' | 'ligatures';
|
||||
|
||||
export interface IDemoAddon<T extends AddonType> {
|
||||
name: T;
|
||||
@@ -32,11 +33,12 @@ export interface IDemoAddon<T extends AddonType> {
|
||||
T extends 'progress' ? typeof ProgressAddon :
|
||||
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 :
|
||||
@@ -47,11 +49,12 @@ export interface IDemoAddon<T extends AddonType> {
|
||||
T extends 'progress' ? ProgressAddon :
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user