Merge pull request #4519 from PerBothner/clusters

New unicode-graphemes addon.
This commit is contained in:
Daniel Imms
2023-09-12 13:56:27 -07:00
committed by GitHub
43 changed files with 1480 additions and 103 deletions
+4
View File
@@ -30,6 +30,9 @@
"addons/xterm-addon-serialize/benchmark/tsconfig.json",
"addons/xterm-addon-unicode11/src/tsconfig.json",
"addons/xterm-addon-unicode11/test/tsconfig.json",
"addons/xterm-addon-unicode-graphemes/src/tsconfig.json",
"addons/xterm-addon-unicode-graphemes/test/tsconfig.json",
"addons/xterm-addon-unicode-graphemes/benchmark/tsconfig.json",
"addons/xterm-addon-web-links/src/tsconfig.json",
"addons/xterm-addon-web-links/test/tsconfig.json",
"addons/xterm-addon-webgl/src/tsconfig.json",
@@ -38,6 +41,7 @@
"sourceType": "module"
},
"ignorePatterns": [
"addons/*/src/third-party/*.ts",
"**/inwasm-sdks/*",
"**/typings/*.d.ts",
"**/node_modules",
+4
View File
@@ -42,6 +42,8 @@ jobs:
./addons/xterm-addon-serialize/out-test/* \
./addons/xterm-addon-unicode11/out/* \
./addons/xterm-addon-unicode11/out-test/* \
./addons/xterm-addon-unicode-graphemes/out/* \
./addons/xterm-addon-unicode-graphemes/out-test/* \
./addons/xterm-addon-web-links/out/* \
./addons/xterm-addon-web-links/out-test/* \
./addons/xterm-addon-webgl/out/* \
@@ -68,6 +70,8 @@ jobs:
yarn --frozen-lockfile
yarn install-addons
- name: Lint code
env:
NODE_OPTIONS: --max_old_space_size=4096
run: yarn lint
- name: Lint API
run: yarn lint-api
@@ -0,0 +1,3 @@
lib
node_modules
out-benchmark
@@ -0,0 +1,29 @@
# Blacklist - exclude everything except npm defaults such as LICENSE, etc
*
!*/
# Whitelist - lib/
!lib/**/*.d.ts
!lib/**/*.js
!lib/**/*.js.map
!lib/**/*.css
# Whitelist - src/
!src/**/*.ts
!src/**/*.d.ts
!src/**/*.js
!src/**/*.js.map
!src/**/*.css
# Blacklist - src/ test files
src/**/*.test.ts
src/**/*.test.d.ts
src/**/*.test.js
src/**/*.test.js.map
# Whitelist - typings/
!typings/*.d.ts
@@ -0,0 +1,19 @@
Copyright (c) 2023, 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,24 @@
## xterm-addon-unicode-graphemes
⚠️ **This addon is currently experimental and may introduce unexpected and non-standard behavior**
An addon providing enhanced Unicode support (include grapheme clustering) for xterm.js.
The file `src/UnicodeProperties.ts` is generated and depends on the Unicode version. See [the unicode-properties project](https://github.com/PerBothner/unicode-properties) for credits and re-generation instructions.
### Install
```bash
npm install --save xterm-addon-unicode-graphemes
```
### Usage
```ts
import { Terminal } from 'xterm';
import { UnicodeGraphemeAddon } from 'xterm-addon-unicode-graphemes';
const terminal = new Terminal();
const unicodeGraphemeAddon = new UnicodeGraphemeAddon();
terminal.loadAddon(unicodeGraphemeAddon);
```
@@ -0,0 +1,78 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { perfContext, before, ThroughputRuntimeCase } from 'xterm-benchmark';
import { spawn } from 'node-pty';
import { Utf8ToUtf32, stringFromCodePoint } from 'common/input/TextDecoder';
import { Terminal } from 'browser/Terminal';
import { UnicodeGraphemeProvider } from 'UnicodeGraphemeProvider';
function fakedAddonLoad(terminal: any): void {
// resembles what UnicodeGraphemesAddon.activate does under the hood
terminal.unicodeService.register(new UnicodeGraphemeProvider());
terminal.unicodeService.activeVersion = '15-graphemes';
}
perfContext('Terminal: ls -lR /usr/lib', () => {
let content = '';
let contentUtf8: Uint8Array;
before(async () => {
// grab output from "ls -lR /usr"
const p = spawn('ls', ['--color=auto', '-lR', '/usr/lib'], {
name: 'xterm-256color',
cols: 80,
rows: 25,
cwd: process.env.HOME,
env: process.env,
encoding: (null as unknown as string) // needs to be fixed in node-pty
});
const chunks: Buffer[] = [];
let length = 0;
p.on('data', data => {
chunks.push(data as unknown as Buffer);
length += data.length;
});
await new Promise<void>(resolve => p.on('exit', () => resolve()));
contentUtf8 = Buffer.concat(chunks, length);
// translate to content string
const buffer = new Uint32Array(contentUtf8.length);
const decoder = new Utf8ToUtf32();
const codepoints = decoder.decode(contentUtf8, buffer);
for (let i = 0; i < codepoints; ++i) {
content += stringFromCodePoint(buffer[i]);
// peek into content to force flat repr in v8
if (!(i % 10000000)) {
content[i];
}
}
});
perfContext('write/string/async', () => {
let terminal: Terminal;
before(() => {
terminal = new Terminal({ cols: 80, rows: 25, scrollback: 1000 });
fakedAddonLoad(terminal);
});
new ThroughputRuntimeCase('', async () => {
await new Promise<void>(res => terminal.write(content, res));
return { payloadSize: contentUtf8.length };
}, { fork: false }).showAverageThroughput();
});
perfContext('write/Utf8/async', () => {
let terminal: Terminal;
before(() => {
terminal = new Terminal({ cols: 80, rows: 25, scrollback: 1000 });
});
new ThroughputRuntimeCase('', async () => {
await new Promise<void>(res => terminal.write(content, res));
return { payloadSize: contentUtf8.length };
}, { fork: false }).showAverageThroughput();
});
});
@@ -0,0 +1,19 @@
{
"APP_PATH": ".benchmark",
"evalConfig": {
"tolerance": {
"*": [0.75, 1.5],
"*.dev": [0.01, 1.5],
"*.cv": [0.01, 1.5],
"EscapeSequenceParser.benchmark.js.*.averageThroughput.mean": [0.9, 5]
},
"skip": [
"*.median",
"*.runs",
"*.dev",
"*.cv",
"EscapeSequenceParser.benchmark.js.*.averageRuntime",
"Terminal.benchmark.js.*.averageRuntime"
]
}
}
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"lib": ["dom", "es6"],
"outDir": "../out-benchmark",
"types": ["../../../node_modules/@types/node"],
"moduleResolution": "node",
"strict": false,
"target": "es2015",
"module": "commonjs",
"baseUrl": ".",
"paths": {
"common/*": ["../../../src/common/*"],
"browser/*": ["../../../src/browser/*"],
"UnicodeGraphemeProvider": ["../src/UnicodeGraphemeProvider"]
}
},
"include": ["../**/*", "../../../typings/xterm.d.ts"],
"exclude": ["../../../**/*test.ts", "../../**/*api.ts"],
"references": [
{ "path": "../../../src/common" },
{ "path": "../../../src/browser" }
]
}
@@ -0,0 +1,29 @@
{
"name": "xterm-addon-unicode-graphemes",
"version": "0.1.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
},
"main": "lib/xterm-addon-unicode-graphemes.js",
"types": "typings/xterm-addon-unicode-graphemes.d.ts",
"repository": "https://github.com/xtermjs/xterm.js/tree/master/addons/xterm-addon-unicode-graphemes",
"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",
"benchmark": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json out-benchmark/benchmark/*benchmark.js",
"benchmark-baseline": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json --baseline out-benchmark/benchmark/*benchmark.js",
"benchmark-eval": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json --eval out-benchmark/benchmark/*benchmark.js"
},
"peerDependencies": {
"xterm": "^5.0.0"
}
}
@@ -0,0 +1,72 @@
/**
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IUnicodeVersionProvider } from 'xterm';
import { UnicodeCharProperties, UnicodeCharWidth } from 'common/services/Services';
import { UnicodeService } from 'common/services/UnicodeService';
import * as UC from './third-party/UnicodeProperties';
export class UnicodeGraphemeProvider implements IUnicodeVersionProvider {
public readonly version;
public ambiguousCharsAreWide: boolean = false;
public readonly handleGraphemes: boolean;
constructor(handleGraphemes: boolean = true) {
this.version = handleGraphemes ? '15-graphemes' : '15';
this.handleGraphemes = handleGraphemes;
}
private static readonly _plainNarrowProperties: UnicodeCharProperties
= UnicodeService.createPropertyValue(UC.GRAPHEME_BREAK_Other, 1, false);
public charProperties(codepoint: number, preceding: UnicodeCharProperties): UnicodeCharProperties {
// Optimize the simple ASCII case, under the condition that
// UnicodeService.extractCharKind(preceding) === GRAPHEME_BREAK_Other
// (which also covers the case that preceding === 0).
if ((codepoint >= 32 && codepoint < 127) && (preceding >> 3) === 0) {
return UnicodeGraphemeProvider._plainNarrowProperties;
}
let charInfo = UC.getInfo(codepoint);
let w = UC.infoToWidthInfo(charInfo);
let shouldJoin = false;
if (w >= 2) {
// Treat emoji_presentation_selector as WIDE.
w = w === 3 || this.ambiguousCharsAreWide || codepoint === 0xfe0f ? 2 : 1;
} else {
w = 1;
}
if (preceding !== 0) {
const oldWidth = UnicodeService.extractWidth(preceding);
if (this.handleGraphemes) {
charInfo = UC.shouldJoin(UnicodeService.extractCharKind(preceding), charInfo);
} else {
charInfo = w === 0 ? 1 : 0;
}
shouldJoin = charInfo > 0;
if (shouldJoin) {
if (oldWidth > w) {
w = oldWidth;
} else if (charInfo === 32) { // UC.GRAPHEME_BREAK_SAW_Regional_Pair)
w = 2;
}
}
}
return UnicodeService.createPropertyValue(charInfo, w, shouldJoin);
}
public wcwidth(codepoint: number): UnicodeCharWidth {
const charInfo = UC.getInfo(codepoint);
const w = UC.infoToWidthInfo(charInfo);
const kind = (charInfo & UC.GRAPHEME_BREAK_MASK) >> UC.GRAPHEME_BREAK_SHIFT;
if (kind === UC.GRAPHEME_BREAK_Extend || kind === UC.GRAPHEME_BREAK_Prepend) {
return 0;
}
if (w >= 2 && (w === 3 || this.ambiguousCharsAreWide)) {
return 2;
}
return 1;
}
}
@@ -0,0 +1,38 @@
/**
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
* @license MIT
*
* UnicodeVersionProvider for V15 with grapeme cluster handleing.
*/
import { Terminal, ITerminalAddon, IUnicodeHandling } from 'xterm';
import { UnicodeGraphemeProvider } from './UnicodeGraphemeProvider';
export class UnicodeGraphemesAddon implements ITerminalAddon {
private _provider15Graphemes?: UnicodeGraphemeProvider;
private _provider15?: UnicodeGraphemeProvider;
private _unicode?: IUnicodeHandling;
private _oldVersion: string = '';
public activate(terminal: Terminal): void {
if (! this._provider15) {
this._provider15 = new UnicodeGraphemeProvider(false);
}
if (! this._provider15Graphemes) {
this._provider15Graphemes = new UnicodeGraphemeProvider(true);
}
const unicode = terminal.unicode;
this._unicode = unicode;
unicode.register(this._provider15);
unicode.register(this._provider15Graphemes);
this._oldVersion = unicode.activeVersion;
unicode.activeVersion = '15-graphemes';
}
public dispose(): void {
if (this._unicode) {
this._unicode.activeVersion = this._oldVersion;
}
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,380 @@
var TINF_OK = 0;
var TINF_DATA_ERROR = -3;
class Tree {
table = new Uint16Array(16); /* table of code length counts */
trans = new Uint16Array(288); /* code -> symbol translation table */
};
class Data {
tag: number = 0;
bitcount: number = 0;
destLen: number = 0;
ltree: Tree;
dtree: Tree;
source: Uint8Array;
dest: Uint8Array;
sourceIndex: number = 0;
constructor(source: Uint8Array, dest: Uint8Array) {
this.source = source;
this.dest = dest;
this.ltree = new Tree(); /* dynamic length/symbol tree */
this.dtree = new Tree(); /* dynamic distance tree */
}
}
/* --------------------------------------------------- *
* -- uninitialized global data (static structures) -- *
* --------------------------------------------------- */
var sltree = new Tree();
var sdtree = new Tree();
/* extra bits and base tables for length codes */
var length_bits = new Uint8Array(30);
var length_base = new Uint16Array(30);
/* extra bits and base tables for distance codes */
var dist_bits = new Uint8Array(30);
var dist_base = new Uint16Array(30);
/* special ordering of code length codes */
var clcidx = new Uint8Array([
16, 17, 18, 0, 8, 7, 9, 6,
10, 5, 11, 4, 12, 3, 13, 2,
14, 1, 15
]);
/* used by tinf_decode_trees, avoids allocations every call */
const code_tree = new Tree();
const lengths = new Uint8Array(288 + 32);
/* ----------------------- *
* -- utility functions -- *
* ----------------------- */
/* build extra bits and base tables */
function tinf_build_bits_base(bits: Uint8Array, base: Uint16Array, delta: number, first: number): void {
var i, sum;
/* build bits table */
for (i = 0; i < delta; ++i) bits[i] = 0;
for (i = 0; i < 30 - delta; ++i) bits[i + delta] = i / delta | 0;
/* build base table */
for (sum = first, i = 0; i < 30; ++i) {
base[i] = sum;
sum += 1 << bits[i];
}
}
/* build the fixed huffman trees */
function tinf_build_fixed_trees(lt: Tree, dt: Tree): void {
var i;
/* build fixed length tree */
for (i = 0; i < 7; ++i) lt.table[i] = 0;
lt.table[7] = 24;
lt.table[8] = 152;
lt.table[9] = 112;
for (i = 0; i < 24; ++i) lt.trans[i] = 256 + i;
for (i = 0; i < 144; ++i) lt.trans[24 + i] = i;
for (i = 0; i < 8; ++i) lt.trans[24 + 144 + i] = 280 + i;
for (i = 0; i < 112; ++i) lt.trans[24 + 144 + 8 + i] = 144 + i;
/* build fixed distance tree */
for (i = 0; i < 5; ++i) dt.table[i] = 0;
dt.table[5] = 32;
for (i = 0; i < 32; ++i) dt.trans[i] = i;
}
/* given an array of code lengths, build a tree */
var offs = new Uint16Array(16);
function tinf_build_tree(t: Tree, lengths: Uint8Array, off: number, num: number): void {
var i, sum;
/* clear code length count table */
for (i = 0; i < 16; ++i) t.table[i] = 0;
/* scan symbol lengths, and sum code length counts */
for (i = 0; i < num; ++i) t.table[lengths[off + i]]++;
t.table[0] = 0;
/* compute offset table for distribution sort */
for (sum = 0, i = 0; i < 16; ++i) {
offs[i] = sum;
sum += t.table[i];
}
/* create code->symbol translation table (symbols sorted by code) */
for (i = 0; i < num; ++i) {
if (lengths[off + i]) t.trans[offs[lengths[off + i]]++] = i;
}
}
/* ---------------------- *
* -- decode functions -- *
* ---------------------- */
/* get one bit from source stream */
function tinf_getbit(d: Data): number {
/* check if tag is empty */
if (!d.bitcount--) {
/* load next tag */
d.tag = d.source[d.sourceIndex++];
d.bitcount = 7;
}
/* shift bit out of tag */
var bit = d.tag & 1;
d.tag >>>= 1;
return bit;
}
/* read a num bit value from a stream and add base */
function tinf_read_bits(d: Data, num: number, base: number): number {
if (!num)
return base;
while (d.bitcount < 24) {
d.tag |= d.source[d.sourceIndex++] << d.bitcount;
d.bitcount += 8;
}
var val = d.tag & (0xffff >>> (16 - num));
d.tag >>>= num;
d.bitcount -= num;
return val + base;
}
/* given a data stream and a tree, decode a symbol */
function tinf_decode_symbol(d: Data, t: Tree): number {
while (d.bitcount < 24) {
d.tag |= d.source[d.sourceIndex++] << d.bitcount;
d.bitcount += 8;
}
var sum = 0, cur = 0, len = 0;
var tag = d.tag;
/* get more bits while code value is above sum */
do {
cur = 2 * cur + (tag & 1);
tag >>>= 1;
++len;
sum += t.table[len];
cur -= t.table[len];
} while (cur >= 0);
d.tag = tag;
d.bitcount -= len;
return t.trans[sum + cur];
}
/* given a data stream, decode dynamic trees from it */
function tinf_decode_trees(d: Data, lt: Tree, dt: Tree): void {
var hlit, hdist, hclen;
var i, num, length;
/* get 5 bits HLIT (257-286) */
hlit = tinf_read_bits(d, 5, 257);
/* get 5 bits HDIST (1-32) */
hdist = tinf_read_bits(d, 5, 1);
/* get 4 bits HCLEN (4-19) */
hclen = tinf_read_bits(d, 4, 4);
for (i = 0; i < 19; ++i) lengths[i] = 0;
/* read code lengths for code length alphabet */
for (i = 0; i < hclen; ++i) {
/* get 3 bits code length (0-7) */
var clen = tinf_read_bits(d, 3, 0);
lengths[clcidx[i]] = clen;
}
/* build code length tree */
tinf_build_tree(code_tree, lengths, 0, 19);
/* decode code lengths for the dynamic trees */
for (num = 0; num < hlit + hdist;) {
var sym = tinf_decode_symbol(d, code_tree);
switch (sym) {
case 16:
/* copy previous code length 3-6 times (read 2 bits) */
var prev = lengths[num - 1];
for (length = tinf_read_bits(d, 2, 3); length; --length) {
lengths[num++] = prev;
}
break;
case 17:
/* repeat code length 0 for 3-10 times (read 3 bits) */
for (length = tinf_read_bits(d, 3, 3); length; --length) {
lengths[num++] = 0;
}
break;
case 18:
/* repeat code length 0 for 11-138 times (read 7 bits) */
for (length = tinf_read_bits(d, 7, 11); length; --length) {
lengths[num++] = 0;
}
break;
default:
/* values 0-15 represent the actual code lengths */
lengths[num++] = sym;
break;
}
}
/* build dynamic trees */
tinf_build_tree(lt, lengths, 0, hlit);
tinf_build_tree(dt, lengths, hlit, hdist);
}
/* ----------------------------- *
* -- block inflate functions -- *
* ----------------------------- */
/* given a stream and two trees, inflate a block of data */
function tinf_inflate_block_data(d: Data, lt: Tree, dt: Tree): number {
for (;;) {
var sym = tinf_decode_symbol(d, lt);
/* check for end of block */
if (sym === 256) {
return TINF_OK;
}
if (sym < 256) {
d.dest[d.destLen++] = sym;
} else {
var length, dist, offs;
var i;
sym -= 257;
/* possibly get more bits from length code */
length = tinf_read_bits(d, length_bits[sym], length_base[sym]);
dist = tinf_decode_symbol(d, dt);
/* possibly get more bits from distance code */
offs = d.destLen - tinf_read_bits(d, dist_bits[dist], dist_base[dist]);
/* copy match */
for (i = offs; i < offs + length; ++i) {
d.dest[d.destLen++] = d.dest[i];
}
}
}
}
/* inflate an uncompressed block of data */
function tinf_inflate_uncompressed_block(d: Data) {
var length, invlength;
var i;
/* unread from bitbuffer */
while (d.bitcount > 8) {
d.sourceIndex--;
d.bitcount -= 8;
}
/* get length */
length = d.source[d.sourceIndex + 1];
length = 256 * length + d.source[d.sourceIndex];
/* get one's complement of length */
invlength = d.source[d.sourceIndex + 3];
invlength = 256 * invlength + d.source[d.sourceIndex + 2];
/* check length */
if (length !== (~invlength & 0x0000ffff))
return TINF_DATA_ERROR;
d.sourceIndex += 4;
/* copy block */
for (i = length; i; --i)
d.dest[d.destLen++] = d.source[d.sourceIndex++];
/* make sure we start next block on a byte boundary */
d.bitcount = 0;
return TINF_OK;
}
/* inflate stream from source to dest */
function tinf_uncompress(source: Uint8Array, dest: Uint8Array) {
var d = new Data(source, dest);
var bfinal, btype, res;
do {
/* read final block flag */
bfinal = tinf_getbit(d);
/* read block type (2 bits) */
btype = tinf_read_bits(d, 2, 0);
/* decompress block */
switch (btype) {
case 0:
/* decompress uncompressed block */
res = tinf_inflate_uncompressed_block(d);
break;
case 1:
/* decompress block with fixed huffman trees */
res = tinf_inflate_block_data(d, sltree, sdtree);
break;
case 2:
/* decompress block with dynamic huffman trees */
tinf_decode_trees(d, d.ltree, d.dtree);
res = tinf_inflate_block_data(d, d.ltree, d.dtree);
break;
default:
res = TINF_DATA_ERROR;
}
if (res !== TINF_OK)
throw new Error('Data error');
} while (!bfinal);
if (d.destLen < d.dest.length) {
if (typeof d.dest.slice === 'function')
return d.dest.slice(0, d.destLen);
else
return d.dest.subarray(0, d.destLen);
}
return d.dest;
}
/* -------------------- *
* -- initialization -- *
* -------------------- */
/* build fixed huffman trees */
tinf_build_fixed_trees(sltree, sdtree);
/* build extra bits and base tables */
tinf_build_bits_base(length_bits, length_base, 4, 3);
tinf_build_bits_base(dist_bits, dist_base, 2, 1);
/* fix a special case */
length_bits[28] = 0;
length_base[28] = 258;
export default tinf_uncompress
@@ -0,0 +1,134 @@
import inflate from './tiny-inflate'
// Shift size for getting the index-1 table offset.
const SHIFT_1 = 6 + 5;
// Shift size for getting the index-2 table offset.
const SHIFT_2 = 5;
// Difference between the two shift sizes,
// for getting an index-1 offset from an index-2 offset. 6=11-5
const SHIFT_1_2 = SHIFT_1 - SHIFT_2;
// Number of index-1 entries for the BMP. 32=0x20
// This part of the index-1 table is omitted from the serialized form.
const OMITTED_BMP_INDEX_1_LENGTH = 0x10000 >> SHIFT_1;
// Number of entries in an index-2 block. 64=0x40
const INDEX_2_BLOCK_LENGTH = 1 << SHIFT_1_2;
// Mask for getting the lower bits for the in-index-2-block offset. */
const INDEX_2_MASK = INDEX_2_BLOCK_LENGTH - 1;
// Shift size for shifting left the index array values.
// Increases possible data size with 16-bit index values at the cost
// of compactability.
// This requires data blocks to be aligned by DATA_GRANULARITY.
const INDEX_SHIFT = 2;
// Number of entries in a data block. 32=0x20
const DATA_BLOCK_LENGTH = 1 << SHIFT_2;
// Mask for getting the lower bits for the in-data-block offset.
const DATA_MASK = DATA_BLOCK_LENGTH - 1;
// The part of the index-2 table for U+D800..U+DBFF stores values for
// lead surrogate code _units_ not code _points_.
// Values for lead surrogate code _points_ are indexed with this portion of the table.
// Length=32=0x20=0x400>>SHIFT_2. (There are 1024=0x400 lead surrogates.)
const LSCP_INDEX_2_OFFSET = 0x10000 >> SHIFT_2;
const LSCP_INDEX_2_LENGTH = 0x400 >> SHIFT_2;
// Count the lengths of both BMP pieces. 2080=0x820
const INDEX_2_BMP_LENGTH = LSCP_INDEX_2_OFFSET + LSCP_INDEX_2_LENGTH;
// The 2-byte UTF-8 version of the index-2 table follows at offset 2080=0x820.
// Length 32=0x20 for lead bytes C0..DF, regardless of SHIFT_2.
const UTF8_2B_INDEX_2_OFFSET = INDEX_2_BMP_LENGTH;
const UTF8_2B_INDEX_2_LENGTH = 0x800 >> 6; // U+0800 is the first code point after 2-byte UTF-8
// The index-1 table, only used for supplementary code points, at offset 2112=0x840.
// Variable length, for code points up to highStart, where the last single-value range starts.
// Maximum length 512=0x200=0x100000>>SHIFT_1.
// (For 0x100000 supplementary code points U+10000..U+10ffff.)
//
// The part of the index-2 table for supplementary code points starts
// after this index-1 table.
//
// Both the index-1 table and the following part of the index-2 table
// are omitted completely if there is only BMP data.
const INDEX_1_OFFSET = UTF8_2B_INDEX_2_OFFSET + UTF8_2B_INDEX_2_LENGTH;
// The alignment size of a data block. Also the granularity for compaction.
const DATA_GRANULARITY = 1 << INDEX_SHIFT;
const isBigEndian = (new Uint8Array(new Uint32Array([0x12345678]).buffer)[0] === 0x12);
class UnicodeTrie {
private data: Uint32Array;
private highStart: number;
private errorValue: number;
constructor(data: Uint8Array) {
// read binary format
const view = new DataView(data.buffer);
this.highStart = view.getUint32(0, true);
this.errorValue = view.getUint32(4, true);
let uncompressedLength = view.getUint32(8, true);
data = data.subarray(12);
// double inflate the actual trie data
data = inflate(data, new Uint8Array(uncompressedLength));
data = inflate(data, new Uint8Array(uncompressedLength));
if (isBigEndian) {
// swap bytes from little-endian
const len = data.length;
for (let i = 0; i < len; i += 4) {
// Exchange data[i] and data[i + 3]:
let x = data[i]; data[i] = data[i+3]; data[i+3] = x;
// Exchange data[i + 1] and data[i + 2]:
let y = data[i+1]; data[i+1] = data[i+2]; data[i+2] = y;
}
}
this.data = new Uint32Array(data.buffer);
}
get(codePoint: number): number {
let index;
if ((codePoint < 0) || (codePoint > 0x10ffff)) {
return this.errorValue;
}
if ((codePoint < 0xd800) || ((codePoint > 0xdbff) && (codePoint <= 0xffff))) {
// Ordinary BMP code point, excluding leading surrogates.
// BMP uses a single level lookup. BMP index starts at offset 0 in the index.
// data is stored in the index array itself.
index = (this.data[codePoint >> SHIFT_2] << INDEX_SHIFT) + (codePoint & DATA_MASK);
return this.data[index];
}
if (codePoint <= 0xffff) {
// Lead Surrogate Code Point. A Separate index section is stored for
// lead surrogate code units and code points.
// The main index has the code unit data.
// For this function, we need the code point data.
index = (this.data[LSCP_INDEX_2_OFFSET + ((codePoint - 0xd800) >> SHIFT_2)] << INDEX_SHIFT) + (codePoint & DATA_MASK);
return this.data[index];
}
if (codePoint < this.highStart) {
// Supplemental code point, use two-level lookup.
index = this.data[(INDEX_1_OFFSET - OMITTED_BMP_INDEX_1_LENGTH) + (codePoint >> SHIFT_1)];
index = this.data[index + ((codePoint >> SHIFT_2) & INDEX_2_MASK)];
index = (index << INDEX_SHIFT) + (codePoint & DATA_MASK);
return this.data[index];
}
return this.data[this.data.length - DATA_GRANULARITY];
}
}
export default UnicodeTrie
@@ -0,0 +1,33 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2021",
"lib": [
"dom",
"es2021"
],
"rootDir": ".",
"outDir": "../out",
"sourceMap": true,
"removeComments": true,
"strict": true,
"baseUrl": ".",
"paths": {
"common/*": [
"../../../src/common/*"
]
},
"types": [
"../../../node_modules/@types/mocha"
]
},
"include": [
"./**/*",
"../../../typings/xterm.d.ts"
],
"references": [
{
"path": "../../../src/common"
}
]
}
@@ -0,0 +1,73 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { openTerminal, launchBrowser } from '../../../out-test/api/TestUtils';
import { Browser, Page } from '@playwright/test';
const APP = 'http://127.0.0.1:3001/test';
let browser: Browser;
let page: Page;
const width = 800;
const height = 600;
describe('UnicodeGraphemesAddon', () => {
before(async function(): Promise<any> {
browser = await launchBrowser();
page = await (await browser.newContext()).newPage();
await page.setViewportSize({ width, height });
});
after(async () => {
await browser.close();
});
beforeEach(async function(): Promise<any> {
await page.goto(APP);
await openTerminal(page);
});
async function evalWidth(str: string): Promise<number> {
return page.evaluate(`window.term._core.unicodeService.getStringCellWidth('${str}')`);
}
const ourVersion = '15-graphemes';
it('wcwidth V15 emoji test', async () => {
await page.evaluate(`
window.unicode = new UnicodeGraphemesAddon();
window.term.loadAddon(window.unicode);
`);
// should have loaded '15-graphemes'
assert.deepEqual(await page.evaluate(`window.term.unicode.versions`), ['6', '15', '15-graphemes']);
// switch should not throw
await page.evaluate(`window.term.unicode.activeVersion = '${ourVersion}';`);
assert.equal(await page.evaluate(`window.term.unicode.activeVersion`), ourVersion);
assert.equal(await evalWidth('🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣'), 20,
'10 emoji - width 10 in V6; 20 in V11 or later');
assert.equal(await evalWidth('\u{1F476}\u{1F3FF}\u{1F476}'), 4,
'baby with emoji modifier fitzpatrick type-6; baby');
assert.equal(await evalWidth('\u{1F469}\u200d\u{1f469}\u200d\u{1f466}'), 2,
'woman+zwj+woman+zwj+boy');
assert.equal(await evalWidth('=\u{1F3CB}\u{FE0F}=\u{F3CB}\u{1F3FE}\u200D\u2640='), 7,
'person lifting weights (plain, emoji); woman lighting weights, medium dark');
assert.equal(await evalWidth('\u{1F469}\u{1F469}\u{200D}\u{1F393}\u{1F468}\u{1F3FF}\u{200D}\u{1F393}'), 6,
'woman; woman student; man student dark');
assert.equal(await evalWidth('\u{1f1f3}\u{1f1f4}/'), 3,
'regional indicator symbol letters N and O, cluster');
assert.equal(await evalWidth('\u{1f1f3}/\u{1f1f4}'), 3,
'regional indicator symbol letters N and O, separated');
assert.equal(await evalWidth('\u0061\u0301'), 1,
'letter a with acute accent');
assert.equal(await evalWidth('{\u1100\u1161\u11a8\u1100\u1161}'), 6,
'Korean Jamo');
assert.equal(await evalWidth('\uAC00=\uD685='), 6,
'Hangul syllables (pre-composed)');
assert.equal(await evalWidth('(\u26b0\ufe0e)'), 3,
'coffin with text presentation');
assert.equal(await evalWidth('(\u26b0\ufe0f)'), 4,
'coffin with emoji presentation');
assert.equal(await evalWidth('<E\u0301\ufe0fg\ufe0fa\ufe0fl\ufe0fi\ufe0f\ufe0ft\ufe0fe\u0301\ufe0f>'), 16,
'Égalité (using separate acute) emoij_presentation');
});
});
@@ -0,0 +1,35 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2015",
"lib": [
"dom",
"es2015"
],
"rootDir": ".",
"outDir": "../out-test",
"sourceMap": true,
"removeComments": true,
"strict": true,
"baseUrl": ".",
"paths": {
"common/*": [
"../../../src/common/*"
]
},
"types": [
"../../../node_modules/@types/mocha",
"../../../node_modules/@types/node",
"../../../out-test/api/TestUtils"
]
},
"include": [
"./**/*",
"../../../typings/xterm.d.ts"
],
"references": [
{
"path": "../../../src/common"
}
]
}
@@ -0,0 +1,9 @@
{
"files": [],
"include": [],
"references": [
{ "path": "./src" },
{ "path": "./test" },
{ "path": "./benchmark" }
]
}
@@ -0,0 +1,14 @@
/**
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { Terminal, ITerminalAddon } from 'xterm';
declare module 'xterm-addon-unicode-graphemes' {
export class Unicode11Addon implements ITerminalAddon {
constructor();
public activate(terminal: Terminal): void;
public dispose(): void;
}
}

Some files were not shown because too many files have changed in this diff Show More