@xterm/addon-ligatures 0.8.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@xterm/addon-ligatures",
3
+ "version": "0.8.0-beta.1",
4
+ "description": "Add support for programming ligatures to xterm.js",
5
+ "author": {
6
+ "name": "The xterm.js authors",
7
+ "url": "https://xtermjs.org/"
8
+ },
9
+ "main": "lib/addon-ligatures.js",
10
+ "types": "typings/addon-ligatures.d.ts",
11
+ "repository": "https://github.com/xtermjs/xterm.js/tree/master/addons/addon-ligatures",
12
+ "engines": {
13
+ "node": ">8.0.0"
14
+ },
15
+ "scripts": {
16
+ "prepare": "node bin/download-fonts.js",
17
+ "build": "tsc -p src",
18
+ "watch": "tsc -w -p src",
19
+ "prepackage": "npm run build",
20
+ "package": "webpack",
21
+ "pretest": "npm run build",
22
+ "test": "nyc mocha out/**/*.test.js",
23
+ "prepublish": "npm run package"
24
+ },
25
+ "keywords": [
26
+ "font",
27
+ "ligature",
28
+ "terminal",
29
+ "xterm",
30
+ "xterm.js"
31
+ ],
32
+ "license": "MIT",
33
+ "dependencies": {
34
+ "font-finder": "^1.1.0",
35
+ "font-ligatures": "^1.4.1"
36
+ },
37
+ "devDependencies": {
38
+ "@types/sinon": "^5.0.1",
39
+ "axios": "^0.21.2",
40
+ "mkdirp": "0.5.5",
41
+ "sinon": "6.3.5",
42
+ "yauzl": "^2.10.0"
43
+ },
44
+ "peerDependencies": {
45
+ "xterm": "^5.0.0"
46
+ }
47
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Copyright (c) 2018 The xterm.js authors. All rights reserved.
3
+ * @license MIT
4
+ */
5
+
6
+ import { Terminal } from '@xterm/xterm';
7
+ import { enableLigatures } from '.';
8
+ import { ILigatureOptions } from './Types';
9
+
10
+ export interface ITerminalAddon {
11
+ activate(terminal: Terminal): void;
12
+ dispose(): void;
13
+ }
14
+
15
+ export class LigaturesAddon implements ITerminalAddon {
16
+ private readonly _fallbackLigatures: string[];
17
+
18
+ private _terminal: Terminal | undefined;
19
+ private _characterJoinerId: number | undefined;
20
+
21
+ constructor(options?: Partial<ILigatureOptions>) {
22
+ this._fallbackLigatures = (options?.fallbackLigatures || [
23
+ '<--', '<---', '<<-', '<-', '->', '->>', '-->', '--->',
24
+ '<==', '<===', '<<=', '<=', '=>', '=>>', '==>', '===>', '>=', '>>=',
25
+ '<->', '<-->', '<--->', '<---->', '<=>', '<==>', '<===>', '<====>', '-------->',
26
+ '<~~', '<~', '~>', '~~>', '::', ':::', '==', '!=', '===', '!==',
27
+ ':=', ':-', ':+', '<*', '<*>', '*>', '<|', '<|>', '|>', '+:', '-:', '=:', ':>',
28
+ '++', '+++', '<!--', '<!---', '<***>'
29
+ ]).sort((a, b) => b.length - a.length);
30
+ }
31
+
32
+ public activate(terminal: Terminal): void {
33
+ this._terminal = terminal;
34
+ this._characterJoinerId = enableLigatures(terminal, this._fallbackLigatures);
35
+ }
36
+
37
+ public dispose(): void {
38
+ if (this._characterJoinerId !== undefined) {
39
+ this._terminal?.deregisterCharacterJoiner(this._characterJoinerId);
40
+ this._characterJoinerId = undefined;
41
+ }
42
+ }
43
+ }
package/src/Types.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Copyright (c) 2022 The xterm.js authors. All rights reserved.
3
+ * @license MIT
4
+ */
5
+
6
+ export interface ILigatureOptions {
7
+ fallbackLigatures: string[];
8
+ }
package/src/font.ts ADDED
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Copyright (c) 2018 The xterm.js authors. All rights reserved.
3
+ * @license MIT
4
+ */
5
+
6
+ import { Font, loadBuffer } from 'font-ligatures';
7
+
8
+ import parse from './parse';
9
+
10
+ interface IFontMetadata {
11
+ family: string;
12
+ fullName: string;
13
+ postscriptName: string;
14
+ blob: () => Promise<Blob>;
15
+ }
16
+
17
+ interface IFontAccessNavigator {
18
+ fonts: {
19
+ query: () => Promise<IFontMetadata[]>;
20
+ };
21
+ permissions: {
22
+ request?: (permission: { name: string }) => Promise<{state: string}>;
23
+ };
24
+ }
25
+
26
+ let fontsPromise: Promise<Record<string, IFontMetadata[]>> | undefined = undefined;
27
+
28
+ /**
29
+ * Loads the font ligature wrapper for the specified font family if it could be
30
+ * resolved, throwing if it is unable to find a suitable match.
31
+ * @param fontFamily The CSS font family definition to resolve
32
+ * @param cacheSize The size of the ligature cache to maintain if the font is resolved
33
+ */
34
+ export default async function load(fontFamily: string, cacheSize: number): Promise<Font | undefined> {
35
+ if (!fontsPromise) {
36
+ // Web environment that supports font access API
37
+ if (typeof navigator !== 'undefined' && 'fonts' in navigator) {
38
+ try {
39
+ const status = await (navigator as unknown as IFontAccessNavigator).permissions.request?.({
40
+ name: 'local-fonts'
41
+ });
42
+ if (status && status.state !== 'granted') {
43
+ throw new Error('Permission to access local fonts not granted.');
44
+ }
45
+ } catch (err: any) {
46
+ // A `TypeError` indicates the 'local-fonts'
47
+ // permission is not yet implemented, so
48
+ // only `throw` if this is _not_ the problem.
49
+ if (err.name !== 'TypeError') {
50
+ throw err;
51
+ }
52
+ }
53
+ const fonts: Record<string, IFontMetadata[]> = {};
54
+ try {
55
+ const fontsIterator = await (navigator as unknown as IFontAccessNavigator).fonts.query();
56
+ for (const metadata of fontsIterator) {
57
+ if (!fonts.hasOwnProperty(metadata.family)) {
58
+ fonts[metadata.family] = [];
59
+ }
60
+ fonts[metadata.family].push(metadata);
61
+ }
62
+ fontsPromise = Promise.resolve(fonts);
63
+ } catch (err: any) {
64
+ console.error(err.name, err.message);
65
+ }
66
+ }
67
+ // Latest proposal https://bugs.chromium.org/p/chromium/issues/detail?id=1312603
68
+ else if (typeof window !== 'undefined' && 'queryLocalFonts' in window) {
69
+ const fonts: Record<string, IFontMetadata[]> = {};
70
+ try {
71
+ const fontsIterator = await (window as any).queryLocalFonts();
72
+ for (const metadata of fontsIterator) {
73
+ if (!fonts.hasOwnProperty(metadata.family)) {
74
+ fonts[metadata.family] = [];
75
+ }
76
+ fonts[metadata.family].push(metadata);
77
+ }
78
+ fontsPromise = Promise.resolve(fonts);
79
+ } catch (err: any) {
80
+ console.error(err.name, err.message);
81
+ }
82
+ }
83
+ if (!fontsPromise) {
84
+ fontsPromise = Promise.resolve({});
85
+ }
86
+ }
87
+
88
+ const fonts = await fontsPromise;
89
+ for (const family of parse(fontFamily)) {
90
+ // If we reach one of the generic font families, the font resolution
91
+ // will end for the browser and we can't determine the specific font
92
+ // used. Throw.
93
+ if (genericFontFamilies.includes(family)) {
94
+ return undefined;
95
+ }
96
+
97
+ if (fonts.hasOwnProperty(family) && fonts[family].length > 0) {
98
+ const font = fonts[family][0];
99
+ if ('blob' in font) {
100
+ const bytes = await font.blob();
101
+ const buffer = await bytes.arrayBuffer();
102
+ return loadBuffer(buffer, { cacheSize });
103
+ }
104
+ return undefined;
105
+ }
106
+ }
107
+
108
+ // If none of the fonts could resolve, throw an error
109
+ return undefined;
110
+ }
111
+
112
+ // https://drafts.csswg.org/css-fonts-4/#generic-font-families
113
+ const genericFontFamilies = [
114
+ 'serif',
115
+ 'sans-serif',
116
+ 'cursive',
117
+ 'fantasy',
118
+ 'monospace',
119
+ 'system-ui',
120
+ 'emoji',
121
+ 'math',
122
+ 'fangsong'
123
+ ];
package/src/index.ts ADDED
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Copyright (c) 2018 The xterm.js authors. All rights reserved.
3
+ * @license MIT
4
+ */
5
+
6
+ import type { Terminal } from '@xterm/xterm';
7
+ import { Font } from 'font-ligatures';
8
+
9
+ import load from './font';
10
+
11
+ const enum LoadingState {
12
+ UNLOADED,
13
+ LOADING,
14
+ LOADED,
15
+ FAILED
16
+ }
17
+
18
+ // Caches 100K characters worth of ligatures. In practice this works out to
19
+ // about 650 KB worth of cache, when a moderate number of ligatures are present.
20
+ const CACHE_SIZE = 100000;
21
+
22
+ /**
23
+ * Enable ligature support for the provided Terminal instance. To function
24
+ * properly, this must be called after `open()` is called on the therminal. If
25
+ * the font currently in use supports ligatures, the terminal will automatically
26
+ * start to render them.
27
+ * @param term Terminal instance from xterm.js
28
+ */
29
+ export function enableLigatures(term: Terminal, fallbackLigatures: string[] = []): number {
30
+ let currentFontName: string | undefined = undefined;
31
+ let font: Font | undefined = undefined;
32
+ let loadingState: LoadingState = LoadingState.UNLOADED;
33
+ let loadError: any | undefined = undefined;
34
+
35
+ return term.registerCharacterJoiner((text: string): [number, number][] => {
36
+ // If the font hasn't been loaded yet, load it and return an empty result
37
+ const termFont = term.options.fontFamily;
38
+ if (
39
+ termFont &&
40
+ (loadingState === LoadingState.UNLOADED || currentFontName !== termFont)
41
+ ) {
42
+ font = undefined;
43
+ loadingState = LoadingState.LOADING;
44
+ currentFontName = termFont;
45
+ const currentCallFontName = currentFontName;
46
+
47
+ load(currentCallFontName, CACHE_SIZE)
48
+ .then(f => {
49
+ // Another request may have come in while we were waiting, so make
50
+ // sure our font is still vaild.
51
+ if (currentCallFontName === term.options.fontFamily) {
52
+ loadingState = LoadingState.LOADED;
53
+ font = f;
54
+
55
+ // Only refresh things if we actually found a font
56
+ if (f) {
57
+ term.refresh(0, term.rows - 1);
58
+ }
59
+ }
60
+ })
61
+ .catch(e => {
62
+ // Another request may have come in while we were waiting, so make
63
+ // sure our font is still vaild.
64
+ if (currentCallFontName === term.options.fontFamily) {
65
+ loadingState = LoadingState.FAILED;
66
+ if (term.options.logLevel === 'debug') {
67
+ console.debug(loadError, new Error('Failure while loading font'));
68
+ }
69
+ font = undefined;
70
+ loadError = e;
71
+ }
72
+ });
73
+ }
74
+
75
+ if (font && loadingState === LoadingState.LOADED) {
76
+ // We clone the entries to avoid the internal cache of the ligature finder
77
+ // getting messed up.
78
+ return font.findLigatureRanges(text).map<[number, number]>(
79
+ range => [range[0], range[1]]
80
+ );
81
+ }
82
+
83
+ return getFallbackRanges(text, fallbackLigatures);
84
+ });
85
+ }
86
+
87
+ function getFallbackRanges(text: string, fallbackLigatures: string[]): [number, number][] {
88
+ const ranges: [number, number][] = [];
89
+ for (let i = 0; i < text.length; i++) {
90
+ for (let j = 0; j < fallbackLigatures.length; j++) {
91
+ if (text.startsWith(fallbackLigatures[j], i)) {
92
+ ranges.push([i, i + fallbackLigatures[j].length]);
93
+ i += fallbackLigatures[j].length - 1;
94
+ break;
95
+ }
96
+ }
97
+ }
98
+ return ranges;
99
+ }
package/src/parse.ts ADDED
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Copyright (c) 2018 The xterm.js authors. All rights reserved.
3
+ * @license MIT
4
+ */
5
+
6
+ interface IParseContext {
7
+ input: string;
8
+ offset: number;
9
+ }
10
+
11
+ /**
12
+ * Parses a CSS font family value, returning the component font families
13
+ * contained within.
14
+ *
15
+ * @param family The CSS font family input string to parse
16
+ */
17
+ export default function parse(family: string): string[] {
18
+ if (typeof family !== 'string') {
19
+ throw new Error('Font family must be a string');
20
+ }
21
+
22
+ const context: IParseContext = {
23
+ input: family,
24
+ offset: 0
25
+ };
26
+
27
+ const families = [];
28
+ let currentFamily = '';
29
+
30
+ // Work through the input character by character until there are none left.
31
+ // This lexing and parsing in one pass.
32
+ while (context.offset < context.input.length) {
33
+ const char = context.input[context.offset++];
34
+ switch (char) {
35
+ // String
36
+ case '\'':
37
+ case '"':
38
+ currentFamily += parseString(context, char);
39
+ break;
40
+ // End of family
41
+ case ',':
42
+ families.push(currentFamily);
43
+ currentFamily = '';
44
+ break;
45
+ default:
46
+ // Identifiers (whitespace between families is swallowed)
47
+ if (!/\s/.test(char)) {
48
+ context.offset--;
49
+ currentFamily += parseIdentifier(context);
50
+ families.push(currentFamily);
51
+ currentFamily = '';
52
+ }
53
+ }
54
+ }
55
+
56
+ return families;
57
+ }
58
+
59
+ /**
60
+ * Parse a CSS string.
61
+ *
62
+ * @param context Parsing input and offset
63
+ * @param quoteChar The quote character for the string (' or ")
64
+ */
65
+ function parseString(context: IParseContext, quoteChar: '\'' | '"'): string {
66
+ let str = '';
67
+ let escaped = false;
68
+ while (context.offset < context.input.length) {
69
+ const char = context.input[context.offset++];
70
+ if (escaped) {
71
+ if (/[\dA-Fa-f]/.test(char)) {
72
+ // Unicode escape
73
+ context.offset--;
74
+ str += parseUnicode(context);
75
+ } else if (char !== '\n') {
76
+ // Newlines are ignored if escaped. Other characters are used as is.
77
+ str += char;
78
+ }
79
+ escaped = false;
80
+ } else {
81
+ switch (char) {
82
+ // Terminated quote
83
+ case quoteChar:
84
+ return str;
85
+ // Begin escape
86
+ case '\\':
87
+ escaped = true;
88
+ break;
89
+ // Add character to string
90
+ default:
91
+ str += char;
92
+ }
93
+ }
94
+ }
95
+
96
+ throw new Error('Unterminated string');
97
+ }
98
+
99
+ /**
100
+ * Parse a CSS custom identifier.
101
+ *
102
+ * @param context Parsing input and offset
103
+ */
104
+ function parseIdentifier(context: IParseContext): string {
105
+ let str = '';
106
+ let escaped = false;
107
+ while (context.offset < context.input.length) {
108
+ const char = context.input[context.offset++];
109
+ if (escaped) {
110
+ if (/[\dA-Fa-f]/.test(char)) {
111
+ // Unicode escape
112
+ context.offset--;
113
+ str += parseUnicode(context);
114
+ } else {
115
+ // Everything else is used as is
116
+ str += char;
117
+ }
118
+ escaped = false;
119
+ } else {
120
+ switch (char) {
121
+ // Begin escape
122
+ case '\\':
123
+ escaped = true;
124
+ break;
125
+ // Terminate identifier
126
+ case ',':
127
+ return str;
128
+ default:
129
+ if (/\s/.test(char)) {
130
+ // Whitespace is collapsed into a single space within an identifier
131
+ if (!str.endsWith(' ')) {
132
+ str += ' ';
133
+ }
134
+ } else {
135
+ // Add other characters directly
136
+ str += char;
137
+ }
138
+ }
139
+ }
140
+ }
141
+
142
+ return str;
143
+ }
144
+
145
+ /**
146
+ * Parse a CSS unicode escape.
147
+ *
148
+ * @param context Parsing input and offset
149
+ */
150
+ function parseUnicode(context: IParseContext): string {
151
+ let str = '';
152
+ while (context.offset < context.input.length) {
153
+ const char = context.input[context.offset++];
154
+ if (/\s/.test(char)) {
155
+ // The first whitespace character after a unicode escape indicates the end
156
+ // of the escape and is swallowed.
157
+ return unicodeToString(str);
158
+ }
159
+ if (str.length >= 6 || !/[\dA-Fa-f]/.test(char)) {
160
+ // If the next character is not a valid hex digit or we have reached the
161
+ // maximum of 6 digits in the escape, terminate the escape.
162
+ context.offset--;
163
+ return unicodeToString(str);
164
+ }
165
+
166
+ // Otherwise, just add it to the escape
167
+ str += char;
168
+ }
169
+
170
+ return unicodeToString(str);
171
+ }
172
+
173
+ /**
174
+ * Convert a unicode code point from a hex string to a utf8 string.
175
+ *
176
+ * @param codePoint Unicode code point represented as a hex string
177
+ */
178
+ function unicodeToString(codePoint: string): string {
179
+ return String.fromCodePoint(parseInt(codePoint, 16));
180
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Copyright (c) 2018 The xterm.js authors. All rights reserved.
3
+ * @license MIT
4
+ *
5
+ * This contains the type declarations for the @xterm/addon-ligatures library.
6
+ * Note that some interfaces may differ between this file and the actual
7
+ * implementation in src/, that's because this file declares the *public* API
8
+ * which is intended to be stable and consumed by external programs.
9
+ */
10
+
11
+ import { Terminal, ITerminalAddon } from '@xterm/xterm';
12
+
13
+ declare module '@xterm/addon-ligatures' {
14
+ /**
15
+ * An xterm.js addon that enables web links.
16
+ */
17
+ export class LigaturesAddon implements ITerminalAddon {
18
+ /**
19
+ * Creates a new ligatures addon.
20
+ *
21
+ * @param options Options for the ligatures addon.
22
+ */
23
+ constructor(options?: Partial<ILigatureOptions>);
24
+
25
+ /**
26
+ * Activates the addon
27
+ *
28
+ * @param terminal The terminal the addon is being loaded in.
29
+ */
30
+ public activate(terminal: Terminal): void;
31
+
32
+ /**
33
+ * Disposes the addon.
34
+ */
35
+ public dispose(): void;
36
+ }
37
+
38
+ /**
39
+ * Options for the ligatures addon.
40
+ */
41
+ export interface ILigatureOptions {
42
+ /**
43
+ * Fallback ligatures to use when the font access API is either not supported by the browser or
44
+ * access is denied. The default set of ligatures is taken from Iosevka's default "calt"
45
+ * ligation set: https://typeof.net/Iosevka/
46
+ *
47
+ * ```
48
+ * <-- <--- <<- <- -> ->> --> --->
49
+ * <== <=== <<= <= => =>> ==> ===> >= >>=
50
+ * <-> <--> <---> <----> <=> <==> <===> <====> -------->
51
+ * <~~ <~ ~> ~~> :: ::: == != === !==
52
+ * := :- :+ <* <*> *> <| <|> |> +: -: =: :>
53
+ * ++ +++ <!-- <!--- <***>
54
+ * ```
55
+ */
56
+ fallbackLigatures: string[]
57
+ }
58
+ }