Merge remote-tracking branch 'upstream/master' into pr/tisilent/4847
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal } from '@xterm/xterm';
|
||||
import { LigaturesAddon as ILigaturesApi } from '@xterm/addon-ligatures';
|
||||
import { enableLigatures } from '.';
|
||||
import { ILigatureOptions } from './Types';
|
||||
|
||||
export interface ITerminalAddon {
|
||||
activate(terminal: Terminal): void;
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export class LigaturesAddon implements ITerminalAddon , ILigaturesApi {
|
||||
private readonly _fallbackLigatures: string[];
|
||||
|
||||
private _terminal: Terminal | undefined;
|
||||
private _characterJoinerId: number | undefined;
|
||||
|
||||
constructor(options?: Partial<ILigatureOptions>) {
|
||||
this._fallbackLigatures = (options?.fallbackLigatures || [
|
||||
'<--', '<---', '<<-', '<-', '->', '->>', '-->', '--->',
|
||||
'<==', '<===', '<<=', '<=', '=>', '=>>', '==>', '===>', '>=', '>>=',
|
||||
'<->', '<-->', '<--->', '<---->', '<=>', '<==>', '<===>', '<====>', '-------->',
|
||||
'<~~', '<~', '~>', '~~>', '::', ':::', '==', '!=', '===', '!==',
|
||||
':=', ':-', ':+', '<*', '<*>', '*>', '<|', '<|>', '|>', '+:', '-:', '=:', ':>',
|
||||
'++', '+++', '<!--', '<!---', '<***>'
|
||||
]).sort((a, b) => b.length - a.length);
|
||||
}
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
this._terminal = terminal;
|
||||
this._characterJoinerId = enableLigatures(terminal, this._fallbackLigatures);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._characterJoinerId !== undefined) {
|
||||
this._terminal?.deregisterCharacterJoiner(this._characterJoinerId);
|
||||
this._characterJoinerId = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
export interface ILigatureOptions {
|
||||
fallbackLigatures: string[];
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Font, loadBuffer } from 'font-ligatures';
|
||||
|
||||
import parse from './parse';
|
||||
|
||||
interface IFontMetadata {
|
||||
family: string;
|
||||
fullName: string;
|
||||
postscriptName: string;
|
||||
blob: () => Promise<Blob>;
|
||||
}
|
||||
|
||||
interface IFontAccessNavigator {
|
||||
fonts: {
|
||||
query: () => Promise<IFontMetadata[]>;
|
||||
};
|
||||
permissions: {
|
||||
request?: (permission: { name: string }) => Promise<{state: string}>;
|
||||
};
|
||||
}
|
||||
|
||||
let fontsPromise: Promise<Record<string, IFontMetadata[]>> | undefined = undefined;
|
||||
|
||||
/**
|
||||
* Loads the font ligature wrapper for the specified font family if it could be
|
||||
* resolved, throwing if it is unable to find a suitable match.
|
||||
* @param fontFamily The CSS font family definition to resolve
|
||||
* @param cacheSize The size of the ligature cache to maintain if the font is resolved
|
||||
*/
|
||||
export default async function load(fontFamily: string, cacheSize: number): Promise<Font | undefined> {
|
||||
if (!fontsPromise) {
|
||||
// Web environment that supports font access API
|
||||
if (typeof navigator !== 'undefined' && 'fonts' in navigator) {
|
||||
try {
|
||||
const status = await (navigator as unknown as IFontAccessNavigator).permissions.request?.({
|
||||
name: 'local-fonts'
|
||||
});
|
||||
if (status && status.state !== 'granted') {
|
||||
throw new Error('Permission to access local fonts not granted.');
|
||||
}
|
||||
} catch (err: any) {
|
||||
// A `TypeError` indicates the 'local-fonts'
|
||||
// permission is not yet implemented, so
|
||||
// only `throw` if this is _not_ the problem.
|
||||
if (err.name !== 'TypeError') {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
const fonts: Record<string, IFontMetadata[]> = {};
|
||||
try {
|
||||
const fontsIterator = await (navigator as unknown as IFontAccessNavigator).fonts.query();
|
||||
for (const metadata of fontsIterator) {
|
||||
if (!fonts.hasOwnProperty(metadata.family)) {
|
||||
fonts[metadata.family] = [];
|
||||
}
|
||||
fonts[metadata.family].push(metadata);
|
||||
}
|
||||
fontsPromise = Promise.resolve(fonts);
|
||||
} catch (err: any) {
|
||||
console.error(err.name, err.message);
|
||||
}
|
||||
}
|
||||
// Latest proposal https://bugs.chromium.org/p/chromium/issues/detail?id=1312603
|
||||
else if (typeof window !== 'undefined' && 'queryLocalFonts' in window) {
|
||||
const fonts: Record<string, IFontMetadata[]> = {};
|
||||
try {
|
||||
const fontsIterator = await (window as any).queryLocalFonts();
|
||||
for (const metadata of fontsIterator) {
|
||||
if (!fonts.hasOwnProperty(metadata.family)) {
|
||||
fonts[metadata.family] = [];
|
||||
}
|
||||
fonts[metadata.family].push(metadata);
|
||||
}
|
||||
fontsPromise = Promise.resolve(fonts);
|
||||
} catch (err: any) {
|
||||
console.error(err.name, err.message);
|
||||
}
|
||||
}
|
||||
if (!fontsPromise) {
|
||||
fontsPromise = Promise.resolve({});
|
||||
}
|
||||
}
|
||||
|
||||
const fonts = await fontsPromise;
|
||||
for (const family of parse(fontFamily)) {
|
||||
// If we reach one of the generic font families, the font resolution
|
||||
// will end for the browser and we can't determine the specific font
|
||||
// used. Throw.
|
||||
if (genericFontFamilies.includes(family)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (fonts.hasOwnProperty(family) && fonts[family].length > 0) {
|
||||
const font = fonts[family][0];
|
||||
if ('blob' in font) {
|
||||
const bytes = await font.blob();
|
||||
const buffer = await bytes.arrayBuffer();
|
||||
return loadBuffer(buffer, { cacheSize });
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// If none of the fonts could resolve, throw an error
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// https://drafts.csswg.org/css-fonts-4/#generic-font-families
|
||||
const genericFontFamilies = [
|
||||
'serif',
|
||||
'sans-serif',
|
||||
'cursive',
|
||||
'fantasy',
|
||||
'monospace',
|
||||
'system-ui',
|
||||
'emoji',
|
||||
'math',
|
||||
'fangsong'
|
||||
];
|
||||
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
import * as sinon from 'sinon';
|
||||
import { assert } from 'chai';
|
||||
import * as fontFinder from 'font-finder';
|
||||
import * as fontLigatures from 'font-ligatures';
|
||||
|
||||
import * as ligatureSupport from '.';
|
||||
|
||||
describe('LigaturesAddon', () => {
|
||||
let onRefresh: sinon.SinonStub;
|
||||
let term: MockTerminal;
|
||||
|
||||
// -> forms a ligature in Fira Code and Iosevka, but www only forms a ligature
|
||||
// in Fira Code
|
||||
const input = 'a -> b www c';
|
||||
|
||||
before(() => {
|
||||
sinon.stub(fontFinder, 'list').returns(Promise.resolve({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'Fira Code': [{
|
||||
path: path.join(__dirname, '../fonts/firaCode.otf'),
|
||||
style: fontFinder.Style.Regular,
|
||||
type: fontFinder.Type.Monospace,
|
||||
weight: 400
|
||||
}],
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'Iosevka': [{
|
||||
path: path.join(__dirname, '../fonts/iosevka.ttf'),
|
||||
style: fontFinder.Style.Regular,
|
||||
type: fontFinder.Type.Monospace,
|
||||
weight: 400
|
||||
}],
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'Nonexistant Font': [{
|
||||
path: path.join(__dirname, '../fonts/nonexistant.ttf'),
|
||||
style: fontFinder.Style.Regular,
|
||||
type: fontFinder.Type.Monospace,
|
||||
weight: 400
|
||||
}]
|
||||
} as fontFinder.FontList));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
onRefresh = sinon.stub();
|
||||
term = new MockTerminal(onRefresh);
|
||||
ligatureSupport.enableLigatures(term as any);
|
||||
});
|
||||
|
||||
it('registers itself correctly', () => {
|
||||
const term = new MockTerminal(sinon.spy());
|
||||
assert.isUndefined(term.joiner);
|
||||
ligatureSupport.enableLigatures(term as any);
|
||||
assert.isFunction(term.joiner);
|
||||
});
|
||||
|
||||
it('registers itself correctly when called directly', () => {
|
||||
const term = new MockTerminal(sinon.spy());
|
||||
assert.isUndefined(term.joiner);
|
||||
ligatureSupport.enableLigatures(term as any);
|
||||
assert.isFunction(term.joiner);
|
||||
});
|
||||
|
||||
it('returns an empty set of ranges on the first call while the font is loading', () => {
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
});
|
||||
|
||||
it('fails if it finds but cannot load the font', async () => {
|
||||
term.options.fontFamily = 'Nonexistant Font, monospace';
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
await delay(500);
|
||||
assert.isTrue(onRefresh.notCalled);
|
||||
});
|
||||
|
||||
it('returns nothing if the font is not present on the system', async () => {
|
||||
term.options.fontFamily = 'notinstalled';
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
await delay(500);
|
||||
assert.isTrue(onRefresh.notCalled);
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
});
|
||||
|
||||
it('returns nothing if no specific font is specified', async () => {
|
||||
term.options.fontFamily = 'monospace';
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
await delay(500);
|
||||
assert.isTrue(onRefresh.notCalled);
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
});
|
||||
|
||||
it('returns nothing if no fonts are provided', async () => {
|
||||
term.options.fontFamily = '';
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
await delay(500);
|
||||
assert.isTrue(onRefresh.notCalled);
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
});
|
||||
|
||||
it('fails when given malformed inputs', async () => {
|
||||
term.options.fontFamily = {} as any;
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
await delay(500);
|
||||
assert.isTrue(onRefresh.notCalled);
|
||||
});
|
||||
|
||||
it('ensures no empty errors are thrown', async () => {
|
||||
sinon.stub(fontLigatures, 'loadFile').callsFake(async () => { throw undefined; });
|
||||
term.options.fontFamily = 'Iosevka';
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
await delay(500);
|
||||
assert.isTrue(onRefresh.notCalled);
|
||||
(fontLigatures.loadFile as sinon.SinonStub).restore();
|
||||
});
|
||||
});
|
||||
|
||||
class MockTerminal {
|
||||
private _options: { [name: string]: string | number } = {
|
||||
fontFamily: 'Fira Code, monospace',
|
||||
rows: 50
|
||||
};
|
||||
public joiner?: (text: string) => [number, number][];
|
||||
public refresh: (start: number, end: number) => void;
|
||||
|
||||
constructor(onRefresh: (start: number, end: number) => void) {
|
||||
this.refresh = onRefresh;
|
||||
}
|
||||
|
||||
public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
|
||||
this.joiner = handler;
|
||||
return 1;
|
||||
}
|
||||
public deregisterCharacterJoiner(id: number): void {
|
||||
this.joiner = undefined;
|
||||
}
|
||||
public get options(): { [name: string]: string | number } { return this._options; }
|
||||
public set options(options: { [name: string]: string | number }) {
|
||||
for (const key in this._options) {
|
||||
this._options[key] = options[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function delay(delayMs: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, delayMs));
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import type { Terminal } from '@xterm/xterm';
|
||||
import { Font } from 'font-ligatures';
|
||||
|
||||
import load from './font';
|
||||
|
||||
const enum LoadingState {
|
||||
UNLOADED,
|
||||
LOADING,
|
||||
LOADED,
|
||||
FAILED
|
||||
}
|
||||
|
||||
// Caches 100K characters worth of ligatures. In practice this works out to
|
||||
// about 650 KB worth of cache, when a moderate number of ligatures are present.
|
||||
const CACHE_SIZE = 100000;
|
||||
|
||||
/**
|
||||
* Enable ligature support for the provided Terminal instance. To function
|
||||
* properly, this must be called after `open()` is called on the therminal. If
|
||||
* the font currently in use supports ligatures, the terminal will automatically
|
||||
* start to render them.
|
||||
* @param term Terminal instance from xterm.js
|
||||
*/
|
||||
export function enableLigatures(term: Terminal, fallbackLigatures: string[] = []): number {
|
||||
let currentFontName: string | undefined = undefined;
|
||||
let font: Font | undefined = undefined;
|
||||
let loadingState: LoadingState = LoadingState.UNLOADED;
|
||||
let loadError: any | undefined = undefined;
|
||||
|
||||
return term.registerCharacterJoiner((text: string): [number, number][] => {
|
||||
// If the font hasn't been loaded yet, load it and return an empty result
|
||||
const termFont = term.options.fontFamily;
|
||||
if (
|
||||
termFont &&
|
||||
(loadingState === LoadingState.UNLOADED || currentFontName !== termFont)
|
||||
) {
|
||||
font = undefined;
|
||||
loadingState = LoadingState.LOADING;
|
||||
currentFontName = termFont;
|
||||
const currentCallFontName = currentFontName;
|
||||
|
||||
load(currentCallFontName, CACHE_SIZE)
|
||||
.then(f => {
|
||||
// Another request may have come in while we were waiting, so make
|
||||
// sure our font is still vaild.
|
||||
if (currentCallFontName === term.options.fontFamily) {
|
||||
loadingState = LoadingState.LOADED;
|
||||
font = f;
|
||||
|
||||
// Only refresh things if we actually found a font
|
||||
if (f) {
|
||||
term.refresh(0, term.rows - 1);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
// Another request may have come in while we were waiting, so make
|
||||
// sure our font is still vaild.
|
||||
if (currentCallFontName === term.options.fontFamily) {
|
||||
loadingState = LoadingState.FAILED;
|
||||
if (term.options.logLevel === 'debug') {
|
||||
console.debug(loadError, new Error('Failure while loading font'));
|
||||
}
|
||||
font = undefined;
|
||||
loadError = e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (font && loadingState === LoadingState.LOADED) {
|
||||
// We clone the entries to avoid the internal cache of the ligature finder
|
||||
// getting messed up.
|
||||
return font.findLigatureRanges(text).map<[number, number]>(
|
||||
range => [range[0], range[1]]
|
||||
);
|
||||
}
|
||||
|
||||
return getFallbackRanges(text, fallbackLigatures);
|
||||
});
|
||||
}
|
||||
|
||||
function getFallbackRanges(text: string, fallbackLigatures: string[]): [number, number][] {
|
||||
const ranges: [number, number][] = [];
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
for (let j = 0; j < fallbackLigatures.length; j++) {
|
||||
if (text.startsWith(fallbackLigatures[j], i)) {
|
||||
ranges.push([i, i + fallbackLigatures[j].length]);
|
||||
i += fallbackLigatures[j].length - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import parse from './parse';
|
||||
|
||||
// TODO: integrate tests from http://test.csswg.org/suites/css-fonts-4_dev/nightly-unstable/
|
||||
describe('parse', () => {
|
||||
it('parses individual families', () => {
|
||||
assert.deepEqual(parse('monospace'), ['monospace']);
|
||||
});
|
||||
|
||||
it('parses multiple families', () => {
|
||||
assert.deepEqual(parse('Arial, Verdana, serif'), ['Arial', 'Verdana', 'serif']);
|
||||
});
|
||||
|
||||
it('parses quoted families', () => {
|
||||
assert.deepEqual(parse('"Times New Roman", serif'), ['Times New Roman', 'serif']);
|
||||
});
|
||||
|
||||
it('parses single quoted families', () => {
|
||||
assert.deepEqual(parse('\'Times New Roman\', serif'), ['Times New Roman', 'serif']);
|
||||
});
|
||||
|
||||
it('parses families with spaces in their names', () => {
|
||||
assert.deepEqual(parse('Times New Roman, serif'), ['Times New Roman', 'serif']);
|
||||
});
|
||||
|
||||
it('collapses multiple spaces together in identifiers', () => {
|
||||
assert.deepEqual(parse('Times New Roman, serif'), ['Times New Roman', 'serif']);
|
||||
});
|
||||
|
||||
it('does not collapse multiple spaces together in quoted strings', () => {
|
||||
assert.deepEqual(parse('"Times New Roman", serif'), ['Times New Roman', 'serif']);
|
||||
});
|
||||
|
||||
it('handles escaped characters in strings', () => {
|
||||
assert.deepEqual(parse('"quote \\" slash \\\\ slashquote \\\\\\"", serif'), ['quote " slash \\ slashquote \\"', 'serif']);
|
||||
});
|
||||
|
||||
it('fails if a family has an unterminated string', () => {
|
||||
assert.throws(() => parse('"Unterminated, serif'));
|
||||
});
|
||||
|
||||
it('handles unicode escape sequences', () => {
|
||||
assert.deepEqual(parse('"space\\20 between", serif'), ['space between', 'serif']);
|
||||
});
|
||||
|
||||
it('swallows only the first space after a unicode escape', () => {
|
||||
assert.deepEqual(parse('"two-space\\20 between", serif'), ['two-space between', 'serif']);
|
||||
});
|
||||
|
||||
it('automatically ends the unicode escape after six digits', () => {
|
||||
assert.deepEqual(parse('space\\000020between, serif'), ['space between', 'serif']);
|
||||
});
|
||||
|
||||
it('handles unicode escapes at the end of the family', () => {
|
||||
assert.deepEqual(parse('endswithbrace \\7b, serif'), ['endswithbrace {', 'serif']);
|
||||
});
|
||||
|
||||
it('handles unicode escapes at the end of the input', () => {
|
||||
assert.deepEqual(parse('endswithbrace \\7b'), ['endswithbrace {']);
|
||||
});
|
||||
|
||||
it('handles other escaped characters in identifiers', () => {
|
||||
assert.deepEqual(parse('has\\,comma'), ['has,comma']);
|
||||
});
|
||||
|
||||
it('swallows escaped newlines in strings', () => {
|
||||
assert.deepEqual(parse('"multi \\\nline", serif'), ['multi line', 'serif']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
interface IParseContext {
|
||||
input: string;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a CSS font family value, returning the component font families
|
||||
* contained within.
|
||||
*
|
||||
* @param family The CSS font family input string to parse
|
||||
*/
|
||||
export default function parse(family: string): string[] {
|
||||
if (typeof family !== 'string') {
|
||||
throw new Error('Font family must be a string');
|
||||
}
|
||||
|
||||
const context: IParseContext = {
|
||||
input: family,
|
||||
offset: 0
|
||||
};
|
||||
|
||||
const families = [];
|
||||
let currentFamily = '';
|
||||
|
||||
// Work through the input character by character until there are none left.
|
||||
// This lexing and parsing in one pass.
|
||||
while (context.offset < context.input.length) {
|
||||
const char = context.input[context.offset++];
|
||||
switch (char) {
|
||||
// String
|
||||
case '\'':
|
||||
case '"':
|
||||
currentFamily += parseString(context, char);
|
||||
break;
|
||||
// End of family
|
||||
case ',':
|
||||
families.push(currentFamily);
|
||||
currentFamily = '';
|
||||
break;
|
||||
default:
|
||||
// Identifiers (whitespace between families is swallowed)
|
||||
if (!/\s/.test(char)) {
|
||||
context.offset--;
|
||||
currentFamily += parseIdentifier(context);
|
||||
families.push(currentFamily);
|
||||
currentFamily = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return families;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a CSS string.
|
||||
*
|
||||
* @param context Parsing input and offset
|
||||
* @param quoteChar The quote character for the string (' or ")
|
||||
*/
|
||||
function parseString(context: IParseContext, quoteChar: '\'' | '"'): string {
|
||||
let str = '';
|
||||
let escaped = false;
|
||||
while (context.offset < context.input.length) {
|
||||
const char = context.input[context.offset++];
|
||||
if (escaped) {
|
||||
if (/[\dA-Fa-f]/.test(char)) {
|
||||
// Unicode escape
|
||||
context.offset--;
|
||||
str += parseUnicode(context);
|
||||
} else if (char !== '\n') {
|
||||
// Newlines are ignored if escaped. Other characters are used as is.
|
||||
str += char;
|
||||
}
|
||||
escaped = false;
|
||||
} else {
|
||||
switch (char) {
|
||||
// Terminated quote
|
||||
case quoteChar:
|
||||
return str;
|
||||
// Begin escape
|
||||
case '\\':
|
||||
escaped = true;
|
||||
break;
|
||||
// Add character to string
|
||||
default:
|
||||
str += char;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Unterminated string');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a CSS custom identifier.
|
||||
*
|
||||
* @param context Parsing input and offset
|
||||
*/
|
||||
function parseIdentifier(context: IParseContext): string {
|
||||
let str = '';
|
||||
let escaped = false;
|
||||
while (context.offset < context.input.length) {
|
||||
const char = context.input[context.offset++];
|
||||
if (escaped) {
|
||||
if (/[\dA-Fa-f]/.test(char)) {
|
||||
// Unicode escape
|
||||
context.offset--;
|
||||
str += parseUnicode(context);
|
||||
} else {
|
||||
// Everything else is used as is
|
||||
str += char;
|
||||
}
|
||||
escaped = false;
|
||||
} else {
|
||||
switch (char) {
|
||||
// Begin escape
|
||||
case '\\':
|
||||
escaped = true;
|
||||
break;
|
||||
// Terminate identifier
|
||||
case ',':
|
||||
return str;
|
||||
default:
|
||||
if (/\s/.test(char)) {
|
||||
// Whitespace is collapsed into a single space within an identifier
|
||||
if (!str.endsWith(' ')) {
|
||||
str += ' ';
|
||||
}
|
||||
} else {
|
||||
// Add other characters directly
|
||||
str += char;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a CSS unicode escape.
|
||||
*
|
||||
* @param context Parsing input and offset
|
||||
*/
|
||||
function parseUnicode(context: IParseContext): string {
|
||||
let str = '';
|
||||
while (context.offset < context.input.length) {
|
||||
const char = context.input[context.offset++];
|
||||
if (/\s/.test(char)) {
|
||||
// The first whitespace character after a unicode escape indicates the end
|
||||
// of the escape and is swallowed.
|
||||
return unicodeToString(str);
|
||||
}
|
||||
if (str.length >= 6 || !/[\dA-Fa-f]/.test(char)) {
|
||||
// If the next character is not a valid hex digit or we have reached the
|
||||
// maximum of 6 digits in the escape, terminate the escape.
|
||||
context.offset--;
|
||||
return unicodeToString(str);
|
||||
}
|
||||
|
||||
// Otherwise, just add it to the escape
|
||||
str += char;
|
||||
}
|
||||
|
||||
return unicodeToString(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a unicode code point from a hex string to a utf8 string.
|
||||
*
|
||||
* @param codePoint Unicode code point represented as a hex string
|
||||
*/
|
||||
function unicodeToString(codePoint: string): string {
|
||||
return String.fromCodePoint(parseInt(codePoint, 16));
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2017",
|
||||
"module": "commonjs",
|
||||
"sourceMap": true,
|
||||
"outDir": "../out",
|
||||
"rootDir": ".",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"preserveWatchOutput": true,
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha"
|
||||
],
|
||||
"paths": {
|
||||
"@xterm/addon-ligatures" : [
|
||||
"../typings/addon-ligatures.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"./**/*",
|
||||
"../../../typings/xterm.d.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user