@@ -16,6 +16,47 @@ import { SixelImageStorage } from './SixelImageStorage';
|
||||
import { IIPImageStorage } from './IIPImageStorage';
|
||||
import { ITerminalExt, IImageAddonOptions, IResetHandler } from './Types';
|
||||
|
||||
|
||||
/**
|
||||
* Document VT features provided by this addon.
|
||||
*
|
||||
* @vt: #E[Supported via @xterm/addon-image.] DCS SIXEL "SIXEL Graphics" "DCS Ps ; Ps ; Ps ; q Pt ST" "Draw SIXEL image."
|
||||
*
|
||||
* Sixel support is provided by the addon @xterm/addon-image with these limitations:
|
||||
* - immediate coloring (no shared palette, allows high color settings of `img2sixel`)
|
||||
* - max. palette size of 4096 colors
|
||||
* - max. pixel width of 16K
|
||||
* - max. 25 MB per sixel sequence
|
||||
* - VT340 cursor positioning (begin of last sixel data row)
|
||||
*
|
||||
* See [addon readme](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-image) for more details.
|
||||
*
|
||||
*
|
||||
* @vt: #E[Supported via @xterm/addon-image.] OSC 1337 "iTerm2 Commands" "OSC 1337 ; Pt BEL" "Custom iTerm2 commands."
|
||||
*
|
||||
* Only the inline image protocol (IIP) is supported by the addon @xterm/addon-image with
|
||||
* the following limitations:
|
||||
* - sequence:
|
||||
* - format: `OSC 1337 ; File=inline=1 ; size=<unencoded size> ; ... : <base64 payload> BEL`
|
||||
* - size param must be set and payload may not exceed CEIL(size * 4 / 3)
|
||||
* - strict base64 handling as of RFC4648 §4 (standard alphabet, optional padding,
|
||||
* no separator bytes allowed)
|
||||
* - supported params: size, name, width, height, preserveAspectRatio
|
||||
* - image formats: PNG, JPEG and GIF
|
||||
* - no animation support (renders first image of a GIF)
|
||||
* - no multipart support
|
||||
* - VT340 cursor positioning (begin of last sixel data row)
|
||||
*
|
||||
* See [addon readme](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-image)
|
||||
* and [iTerm2 IIP docs](https://iterm2.com/documentation-images.html) for more details.
|
||||
*
|
||||
*
|
||||
* @vt: #E[Supported via @xterm/addon-image.] APC KITTY_GRAPHICS "Kitty Graphics" "APC G Pt ST" "Kitty Graphics Protocol."
|
||||
*
|
||||
* Kitty graphics support is provided by the addon @xterm/addon-image.
|
||||
* Note that while basic image output already works, this is still work in progress.
|
||||
*/
|
||||
|
||||
// default values of addon ctor options
|
||||
const DEFAULT_OPTIONS: IImageAddonOptions = {
|
||||
enableSizeReports: true,
|
||||
@@ -166,7 +207,7 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
|
||||
this._disposeLater(
|
||||
kittyStorage,
|
||||
kittyHandler,
|
||||
terminal._core._inputHandler._parser.registerApcHandler(0x47, kittyHandler)
|
||||
terminal._core._inputHandler._parser.registerApcHandler({ final: 'G' }, kittyHandler)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,9 @@ xterm.js version: {{version}}
|
||||
{{#CSI.length}}
|
||||
- [CSI](#csi)
|
||||
{{/CSI.length}}
|
||||
{{#APC.length}}
|
||||
- [APC](#apc)
|
||||
{{/APC.length}}
|
||||
{{#DCS.length}}
|
||||
- [DCS](#dcs)
|
||||
{{/DCS.length}}
|
||||
@@ -80,8 +83,9 @@ This document lists xterm.js' support of terminal sequences. The sequences are g
|
||||
- CSI - Control Sequence Introducer: sequence starting with \`ESC [\` (7bit) or CSI (\`\\x9B\`, 8bit)
|
||||
- DCS - Device Control String: sequence starting with \`ESC P\` (7bit) or DCS (\`\\x90\`, 8bit)
|
||||
- OSC - Operating System Command: sequence starting with \`ESC ]\` (7bit) or OSC (\`\\x9D\`, 8bit)
|
||||
- APC - Application Program Command: sequence starting with \`ESC _\` (7bit) or OSC (\`\\x9F\`, 8bit)
|
||||
|
||||
Application Program Command (APC), Privacy Message (PM) and Start of String (SOS) are recognized but not supported,
|
||||
Privacy Message (PM) and Start of String (SOS) are recognized but not supported,
|
||||
any sequence of these types will be silently ignored. They are also not hookable by the API.
|
||||
|
||||
Note that the list only marks sequences implemented in xterm.js' core codebase as supported. Missing sequences are either
|
||||
@@ -178,6 +182,33 @@ To denote the sequences the tables use the same abbreviations as xterm does:
|
||||
{{/CSI.length}}
|
||||
|
||||
|
||||
{{#APC.length}}
|
||||
## APC
|
||||
|
||||
| Mnemonic | Name | Sequence | Short Description | Support |
|
||||
| -------- | ---- | -------- | ----------------- | ------- |
|
||||
{{#APC}}
|
||||
| {{mnemonic}} | {{name}} | \`{{sequence}}\` | {{{shortDescription}}} {{#longDescription.length}}_[more](#{{longTarget}}){: .link-details}_{{/longDescription.length}} | {{{status}}} |
|
||||
{{/APC}}
|
||||
|
||||
{{#APC.hasLongDescriptions}}
|
||||
{{#APC}}
|
||||
{{#longDescription.length}}
|
||||
<section class="sequence-details">
|
||||
|
||||
### {{name}}
|
||||
{{#longDescription}}
|
||||
{{{.}}}
|
||||
{{/longDescription}}
|
||||
|
||||
</section>
|
||||
{{/longDescription.length}}
|
||||
{{/APC}}
|
||||
{{/APC.hasLongDescriptions}}
|
||||
|
||||
{{/APC.length}}
|
||||
|
||||
|
||||
{{#DCS.length}}
|
||||
## DCS
|
||||
|
||||
|
||||
Generated
+11
@@ -245,6 +245,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
|
||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
@@ -558,6 +559,7 @@
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -599,6 +601,7 @@
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -1796,6 +1799,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.1.tgz",
|
||||
"integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.50.1",
|
||||
"@typescript-eslint/types": "8.50.1",
|
||||
@@ -2361,6 +2365,7 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -2752,6 +2757,7 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -3747,6 +3753,7 @@
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
|
||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -7016,6 +7023,7 @@
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
@@ -7856,6 +7864,7 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -8022,6 +8031,7 @@
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz",
|
||||
"integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.7",
|
||||
"@types/estree": "^1.0.8",
|
||||
@@ -8070,6 +8080,7 @@
|
||||
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz",
|
||||
"integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@discoveryjs/json-ext": "^0.5.0",
|
||||
"@webpack-cli/configtest": "^1.2.0",
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@
|
||||
"benchmark-baseline": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json --baseline out-test/benchmark/*benchmark.js",
|
||||
"benchmark-eval": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json --eval out-test/benchmark/*benchmark.js",
|
||||
"clean": "rm -rf lib out addons/*/lib addons/*/out",
|
||||
"vtfeatures": "node bin/extract_vtfeatures.js src/**/*.ts src/*.ts",
|
||||
"vtfeatures": "node bin/extract_vtfeatures.js src/*/*.ts src/*/*/*.ts src/*.ts addons/**/src/*.ts",
|
||||
"prepackage": "npm run build",
|
||||
"package": "webpack",
|
||||
"postpackage": "npm run esbuild-package",
|
||||
|
||||
@@ -107,7 +107,7 @@ export class MockTerminal implements ITerminal {
|
||||
public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public registerApcHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
|
||||
public registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
|
||||
|
||||
@@ -243,8 +243,8 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
}
|
||||
|
||||
/** Add handler for APC escape sequence. See xterm.d.ts for details. */
|
||||
public registerApcHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
|
||||
return this._inputHandler.registerApcHandler(ident, callback);
|
||||
public registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
|
||||
return this._inputHandler.registerApcHandler(id, callback);
|
||||
}
|
||||
|
||||
protected _setup(): void {
|
||||
|
||||
@@ -30,28 +30,9 @@ import { XTERM_VERSION } from 'common/Version';
|
||||
*/
|
||||
const GLEVEL: { [key: string]: number } = { '(': 0, ')': 1, '*': 2, '+': 3, '-': 1, '.': 2 };
|
||||
|
||||
/**
|
||||
* VT commands done by the parser - FIXME: move this to the parser?
|
||||
*/
|
||||
// @vt: #Y ESC CSI "Control Sequence Introducer" "ESC [" "Start of a CSI sequence."
|
||||
// @vt: #Y ESC OSC "Operating System Command" "ESC ]" "Start of an OSC sequence."
|
||||
// @vt: #Y ESC DCS "Device Control String" "ESC P" "Start of a DCS sequence."
|
||||
// @vt: #Y ESC ST "String Terminator" "ESC \" "Terminator used for string type sequences."
|
||||
// @vt: #Y ESC PM "Privacy Message" "ESC ^" "Start of a privacy message."
|
||||
// @vt: #Y ESC APC "Application Program Command" "ESC _" "Start of an APC sequence."
|
||||
// @vt: #Y C1 CSI "Control Sequence Introducer" "\x9B" "Start of a CSI sequence."
|
||||
// @vt: #Y C1 OSC "Operating System Command" "\x9D" "Start of an OSC sequence."
|
||||
// @vt: #Y C1 DCS "Device Control String" "\x90" "Start of a DCS sequence."
|
||||
// @vt: #Y C1 ST "String Terminator" "\x9C" "Terminator used for string type sequences."
|
||||
// @vt: #Y C1 PM "Privacy Message" "\x9E" "Start of a privacy message."
|
||||
// @vt: #Y C1 APC "Application Program Command" "\x9F" "Start of an APC sequence."
|
||||
// @vt: #Y C0 NUL "Null" "\0, \x00" "NUL is ignored."
|
||||
// @vt: #Y C0 ESC "Escape" "\e, \x1B" "Start of a sequence. Cancels any other sequence."
|
||||
|
||||
/**
|
||||
* Document xterm VT features here that are currently unsupported
|
||||
*/
|
||||
// @vt: #E[Supported via @xterm/addon-image.] DCS SIXEL "SIXEL Graphics" "DCS Ps ; Ps ; Ps ; q Pt ST" "Draw SIXEL image."
|
||||
// @vt: #N DCS DECUDK "User Defined Keys" "DCS Ps ; Ps \| Pt ST" "Definitions for user-defined keys."
|
||||
// @vt: #N DCS XTGETTCAP "Request Terminfo String" "DCS + q Pt ST" "Request Terminfo String."
|
||||
// @vt: #N DCS XTSETTCAP "Set Terminfo Data" "DCS + p Pt ST" "Set Terminfo Data."
|
||||
@@ -211,6 +192,9 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
}
|
||||
this._logService.debug('Unknown DCS code: ', { identifier: this._parser.identToString(ident), action, payload });
|
||||
});
|
||||
this._parser.setApcHandlerFallback((ident, action, payload) => {
|
||||
this._logService.debug('Unknown APC code: ', { identifier: this._parser.identToString(ident), action, payload });
|
||||
});
|
||||
|
||||
/**
|
||||
* print handler
|
||||
@@ -729,8 +713,8 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
/**
|
||||
* Forward registerApcHandler from parser.
|
||||
*/
|
||||
public registerApcHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
|
||||
return this._parser.registerApcHandler(ident, new ApcHandler(callback));
|
||||
public registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
|
||||
return this._parser.registerApcHandler(id, new ApcHandler(callback));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+2
-2
@@ -22,7 +22,7 @@ export interface ICoreTerminal {
|
||||
registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise<boolean>): IDisposable;
|
||||
registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise<boolean>): IDisposable;
|
||||
registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
|
||||
registerApcHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
|
||||
registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
|
||||
}
|
||||
|
||||
export interface IDisposable {
|
||||
@@ -476,7 +476,7 @@ export interface IInputHandler {
|
||||
registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise<boolean>): IDisposable;
|
||||
registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise<boolean>): IDisposable;
|
||||
registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
|
||||
registerApcHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
|
||||
registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
|
||||
|
||||
/** C0 BEL */ bell(): boolean;
|
||||
/** C0 LF */ lineFeed(): boolean;
|
||||
|
||||
+354
-255
@@ -5,7 +5,7 @@
|
||||
import { assert } from 'chai';
|
||||
import { ApcParser, ApcHandler } from 'common/parser/ApcParser';
|
||||
import { StringToUtf32, utf32ToString } from 'common/input/TextDecoder';
|
||||
import { IApcHandler } from 'common/parser/Types';
|
||||
import { IApcHandler, IFunctionIdentifier } from 'common/parser/Types';
|
||||
|
||||
function toUtf32(s: string): Uint32Array {
|
||||
const utf32 = new Uint32Array(s.length);
|
||||
@@ -14,31 +14,53 @@ function toUtf32(s: string): Uint32Array {
|
||||
return utf32.subarray(0, length);
|
||||
}
|
||||
|
||||
class TestHandler implements IApcHandler {
|
||||
public id: number;
|
||||
public output: [string, number, string, (boolean | string)?][];
|
||||
public msg: string;
|
||||
public returnFalse: boolean;
|
||||
|
||||
constructor(
|
||||
id: number,
|
||||
output: [string, number, string, (boolean | string)?][],
|
||||
msg: string,
|
||||
returnFalse: boolean = false
|
||||
) {
|
||||
this.id = id;
|
||||
this.output = output;
|
||||
this.msg = msg;
|
||||
this.returnFalse = returnFalse;
|
||||
function identifier(id: IFunctionIdentifier): number {
|
||||
let res = 0;
|
||||
if (id.prefix) {
|
||||
if (id.prefix.length > 1) {
|
||||
throw new Error('only one byte as prefix supported');
|
||||
}
|
||||
res = id.prefix.charCodeAt(0);
|
||||
if (res && 0x3c > res || res > 0x3f) {
|
||||
throw new Error('prefix must be in range 0x3c .. 0x3f');
|
||||
}
|
||||
}
|
||||
if (id.intermediates) {
|
||||
if (id.intermediates.length > 2) {
|
||||
throw new Error('only two bytes as intermediates are supported');
|
||||
}
|
||||
for (let i = 0; i < id.intermediates.length; ++i) {
|
||||
const intermediate = id.intermediates.charCodeAt(i);
|
||||
if (0x20 > intermediate || intermediate > 0x2f) {
|
||||
throw new Error('intermediate must be in range 0x20 .. 0x2f');
|
||||
}
|
||||
res <<= 8;
|
||||
res |= intermediate;
|
||||
}
|
||||
}
|
||||
if (id.final.length !== 1) {
|
||||
throw new Error('final must be a single byte');
|
||||
}
|
||||
const finalCode = id.final.charCodeAt(0);
|
||||
if (0x40 > finalCode || finalCode > 0x7e) {
|
||||
throw new Error('final must be in range 0x40 .. 0x7e');
|
||||
}
|
||||
res <<= 8;
|
||||
res |= finalCode;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
class TestHandler implements IApcHandler {
|
||||
constructor(public output: any[], public msg: string, public returnFalse: boolean = false) {}
|
||||
public start(): void {
|
||||
this.output.push([this.msg, this.id, 'START']);
|
||||
this.output.push([this.msg, 'START']);
|
||||
}
|
||||
public put(data: Uint32Array, start: number, end: number): void {
|
||||
this.output.push([this.msg, this.id, 'PUT', utf32ToString(data, start, end)]);
|
||||
this.output.push([this.msg, 'PUT', utf32ToString(data, start, end)]);
|
||||
}
|
||||
public end(success: boolean): boolean {
|
||||
this.output.push([this.msg, this.id, 'END', success]);
|
||||
this.output.push([this.msg, 'END', success]);
|
||||
if (this.returnFalse) {
|
||||
return false;
|
||||
}
|
||||
@@ -48,173 +70,105 @@ class TestHandler implements IApcHandler {
|
||||
|
||||
describe('ApcParser', () => {
|
||||
let parser: ApcParser;
|
||||
let reports: [number, string, (boolean | string | undefined)?][] = [];
|
||||
|
||||
let reports: any[] = [];
|
||||
beforeEach(() => {
|
||||
reports = [];
|
||||
parser = new ApcParser();
|
||||
parser.setHandlerFallback((id: number, action: 'START' | 'PUT' | 'END', data?: string | boolean) => {
|
||||
reports.push([id, action, data]);
|
||||
});
|
||||
parser.setHandlerFallback((id, action, data) => reports.push([id, action, data]));
|
||||
});
|
||||
|
||||
describe('identifier parsing', () => {
|
||||
it('single character identifier', () => {
|
||||
parser.start();
|
||||
const data = toUtf32('Gf=100,a=T;payload');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [
|
||||
[0x47, 'START', undefined], // 0x47 = 'G'
|
||||
[0x47, 'PUT', 'f=100,a=T;payload'],
|
||||
[0x47, 'END', true]
|
||||
]);
|
||||
});
|
||||
|
||||
it('identifier with no payload', () => {
|
||||
parser.start();
|
||||
const data = toUtf32('G');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [
|
||||
[0x47, 'START', undefined],
|
||||
[0x47, 'END', true]
|
||||
]);
|
||||
});
|
||||
|
||||
it('identifier with chunked payload', () => {
|
||||
parser.start();
|
||||
let data = toUtf32('Gf=100');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(',a=T');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(';payload');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [
|
||||
[0x47, 'START', undefined],
|
||||
[0x47, 'PUT', 'f=100'],
|
||||
[0x47, 'PUT', ',a=T'],
|
||||
[0x47, 'PUT', ';payload'],
|
||||
[0x47, 'END', true]
|
||||
]);
|
||||
});
|
||||
|
||||
it('empty APC sequence', () => {
|
||||
parser.start();
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, []);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handler registration', () => {
|
||||
let handlerReports: [string, number, string, (boolean | string)?][];
|
||||
|
||||
beforeEach(() => {
|
||||
handlerReports = [];
|
||||
});
|
||||
|
||||
it('registerHandler for specific identifier', () => {
|
||||
const G_CODE = 0x47; // 'G'
|
||||
parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'kitty'));
|
||||
parser.start();
|
||||
const data = toUtf32('Gf=100,a=T;imagedata');
|
||||
it('setApcHandler', () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th'));
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(handlerReports, [
|
||||
['kitty', G_CODE, 'START'],
|
||||
['kitty', G_CODE, 'PUT', 'f=100,a=T;imagedata'],
|
||||
['kitty', G_CODE, 'END', true]
|
||||
]);
|
||||
assert.deepEqual(reports, []);
|
||||
});
|
||||
|
||||
it('unregistered identifier falls back', () => {
|
||||
const G_CODE = 0x47; // 'G'
|
||||
const X_CODE = 0x58; // 'X'
|
||||
parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'kitty'));
|
||||
parser.start();
|
||||
const data = toUtf32('Xsome data');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(handlerReports, []);
|
||||
assert.deepEqual(reports, [
|
||||
[X_CODE, 'START', undefined],
|
||||
[X_CODE, 'PUT', 'some data'],
|
||||
[X_CODE, 'END', true]
|
||||
// messages from TestHandler
|
||||
['th', 'START'],
|
||||
['th', 'PUT', 'Here comes'],
|
||||
['th', 'PUT', 'the mouse!'],
|
||||
['th', 'END', true]
|
||||
]);
|
||||
});
|
||||
|
||||
it('clearHandler removes handler', () => {
|
||||
const G_CODE = 0x47;
|
||||
parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'kitty'));
|
||||
parser.clearHandler(G_CODE);
|
||||
parser.start();
|
||||
const data = toUtf32('Gf=100');
|
||||
it('clearApcHandler', () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th'));
|
||||
parser.clearHandler(identifier({intermediates: '+', final: 'p'}));
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(handlerReports, []);
|
||||
assert.deepEqual(reports, [
|
||||
[G_CODE, 'START', undefined],
|
||||
[G_CODE, 'PUT', 'f=100'],
|
||||
[G_CODE, 'END', true]
|
||||
// messages from fallback handler
|
||||
[identifier({intermediates: '+', final: 'p'}), 'START', undefined],
|
||||
[identifier({intermediates: '+', final: 'p'}), 'PUT', 'Here comes'],
|
||||
[identifier({intermediates: '+', final: 'p'}), 'PUT', 'the mouse!'],
|
||||
[identifier({intermediates: '+', final: 'p'}), 'END', true]
|
||||
]);
|
||||
});
|
||||
|
||||
it('multiple handlers for same identifier', () => {
|
||||
const G_CODE = 0x47;
|
||||
parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'handler1'));
|
||||
parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'handler2'));
|
||||
parser.start();
|
||||
const data = toUtf32('Gdata');
|
||||
it('addApcHandler', () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1'));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2'));
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(handlerReports, [
|
||||
['handler2', G_CODE, 'START'],
|
||||
['handler1', G_CODE, 'START'],
|
||||
['handler2', G_CODE, 'PUT', 'data'],
|
||||
['handler1', G_CODE, 'PUT', 'data'],
|
||||
['handler2', G_CODE, 'END', true],
|
||||
['handler1', G_CODE, 'END', false]
|
||||
assert.deepEqual(reports, [
|
||||
['th2', 'START'],
|
||||
['th1', 'START'],
|
||||
['th2', 'PUT', 'Here comes'],
|
||||
['th1', 'PUT', 'Here comes'],
|
||||
['th2', 'PUT', 'the mouse!'],
|
||||
['th1', 'PUT', 'the mouse!'],
|
||||
['th2', 'END', true],
|
||||
['th1', 'END', false] // false due being already handled by th2!
|
||||
]);
|
||||
});
|
||||
|
||||
it('handler returning false allows fallthrough', () => {
|
||||
const G_CODE = 0x47;
|
||||
parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'handler1'));
|
||||
parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'handler2', true));
|
||||
parser.start();
|
||||
const data = toUtf32('Gdata');
|
||||
it('addApcHandler with return false', () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1'));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2', true));
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(handlerReports, [
|
||||
['handler2', G_CODE, 'START'],
|
||||
['handler1', G_CODE, 'START'],
|
||||
['handler2', G_CODE, 'PUT', 'data'],
|
||||
['handler1', G_CODE, 'PUT', 'data'],
|
||||
['handler2', G_CODE, 'END', true],
|
||||
['handler1', G_CODE, 'END', true]
|
||||
assert.deepEqual(reports, [
|
||||
['th2', 'START'],
|
||||
['th1', 'START'],
|
||||
['th2', 'PUT', 'Here comes'],
|
||||
['th1', 'PUT', 'Here comes'],
|
||||
['th2', 'PUT', 'the mouse!'],
|
||||
['th1', 'PUT', 'the mouse!'],
|
||||
['th2', 'END', true],
|
||||
['th1', 'END', true] // true since th2 indicated to keep bubbling
|
||||
]);
|
||||
});
|
||||
|
||||
it('dispose removes handler', () => {
|
||||
const G_CODE = 0x47;
|
||||
parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'handler1'));
|
||||
const disposable = parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'handler2'));
|
||||
disposable.dispose();
|
||||
parser.start();
|
||||
const data = toUtf32('Gdata');
|
||||
it('dispose handlers', () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1'));
|
||||
const dispo = parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2', true));
|
||||
dispo.dispose();
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(handlerReports, [
|
||||
['handler1', G_CODE, 'START'],
|
||||
['handler1', G_CODE, 'PUT', 'data'],
|
||||
['handler1', G_CODE, 'END', true]
|
||||
assert.deepEqual(reports, [
|
||||
['th1', 'START'],
|
||||
['th1', 'PUT', 'Here comes'],
|
||||
['th1', 'PUT', 'the mouse!'],
|
||||
['th1', 'END', true]
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ApcHandler convenience class', () => {
|
||||
describe('ApcHandlerFactory', () => {
|
||||
const TEST_PAYLOAD_LIMIT = 100;
|
||||
const CHUNK_SIZE = 10;
|
||||
let originalPayloadLimit: number;
|
||||
@@ -231,133 +185,278 @@ describe('ApcParser', () => {
|
||||
});
|
||||
|
||||
it('should be called once on end(true)', () => {
|
||||
const G_CODE = 0x47;
|
||||
const results: [number, string][] = [];
|
||||
parser.registerHandler(G_CODE, new ApcHandler((data: string) => {
|
||||
results.push([G_CODE, data]);
|
||||
return true;
|
||||
}));
|
||||
parser.start();
|
||||
let data = toUtf32('Gf=100');
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(data => { reports.push(data); return true; }));
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(',a=T;payload');
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(results, [[G_CODE, 'f=100,a=T;payload']]);
|
||||
assert.deepEqual(reports, ['Here comes the mouse!']);
|
||||
});
|
||||
|
||||
it('should not be called on end(false)', () => {
|
||||
const G_CODE = 0x47;
|
||||
const results: [number, string][] = [];
|
||||
parser.registerHandler(G_CODE, new ApcHandler((data: string) => {
|
||||
results.push([G_CODE, data]);
|
||||
return true;
|
||||
}));
|
||||
parser.start();
|
||||
const data = toUtf32('Gf=100,a=T;payload');
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(data => { reports.push(data); return true; }));
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(false);
|
||||
assert.deepEqual(results, []);
|
||||
assert.deepEqual(reports, []);
|
||||
});
|
||||
|
||||
it('should handle payload up to limit', function(): void {
|
||||
this.timeout(30000);
|
||||
const G_CODE = 0x47;
|
||||
const results: [number, string][] = [];
|
||||
parser.registerHandler(G_CODE, new ApcHandler((data: string) => {
|
||||
results.push([G_CODE, data]);
|
||||
return true;
|
||||
}));
|
||||
parser.start();
|
||||
let data = toUtf32('G');
|
||||
it('should be disposable', () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(data => { reports.push(['one', data]); return true; }));
|
||||
const dispo = parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(data => { reports.push(['two', data]); return true; }));
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('A'.repeat(CHUNK_SIZE));
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [['two', 'Here comes the mouse!']]);
|
||||
dispo.dispose();
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
data = toUtf32('some other');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' data');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'some other data']]);
|
||||
});
|
||||
it('should respect return false', () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(data => { reports.push(['one', data]); return true; }));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(data => { reports.push(['two', data]); return false; }));
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'Here comes the mouse!']]);
|
||||
});
|
||||
it('should work up to payload limit', function(): void {
|
||||
this.timeout(30000);
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(data => { reports.push(data); return true; }));
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
const data = toUtf32('A'.repeat(CHUNK_SIZE));
|
||||
for (let i = 0; i < TEST_PAYLOAD_LIMIT; i += CHUNK_SIZE) {
|
||||
parser.put(data, 0, data.length);
|
||||
}
|
||||
parser.end(true);
|
||||
assert.deepEqual(results, [[G_CODE, 'A'.repeat(TEST_PAYLOAD_LIMIT)]]);
|
||||
assert.deepEqual(reports, ['A'.repeat(TEST_PAYLOAD_LIMIT)]);
|
||||
});
|
||||
|
||||
it('should abort for payload over limit', function(): void {
|
||||
it('should abort for payload limit +1', function(): void {
|
||||
this.timeout(30000);
|
||||
const G_CODE = 0x47;
|
||||
const results: [number, string][] = [];
|
||||
parser.registerHandler(G_CODE, new ApcHandler((data: string) => {
|
||||
results.push([G_CODE, data]);
|
||||
return true;
|
||||
}));
|
||||
parser.start();
|
||||
let data = toUtf32('G');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('A'.repeat(CHUNK_SIZE));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(data => { reports.push(data); return true; }));
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('A'.repeat(CHUNK_SIZE));
|
||||
for (let i = 0; i < TEST_PAYLOAD_LIMIT; i += CHUNK_SIZE) {
|
||||
parser.put(data, 0, data.length);
|
||||
}
|
||||
data = toUtf32('A');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(results, []);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reset behavior', () => {
|
||||
let handlerReports: [string, number, string, (boolean | string)?][];
|
||||
|
||||
beforeEach(() => {
|
||||
handlerReports = [];
|
||||
});
|
||||
|
||||
it('reset during payload cleans up handlers', () => {
|
||||
const G_CODE = 0x47;
|
||||
parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'kitty'));
|
||||
parser.start();
|
||||
const data = toUtf32('Gf=100');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.reset();
|
||||
assert.deepEqual(handlerReports, [
|
||||
['kitty', G_CODE, 'START'],
|
||||
['kitty', G_CODE, 'PUT', 'f=100'],
|
||||
['kitty', G_CODE, 'END', false]
|
||||
]);
|
||||
assert.deepEqual(reports, []);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
class TestHandlerAsync implements IApcHandler {
|
||||
constructor(public output: any[], public msg: string, public returnFalse: boolean = false) {}
|
||||
public start(): void {
|
||||
this.output.push([this.msg, 'START']);
|
||||
}
|
||||
public put(data: Uint32Array, start: number, end: number): void {
|
||||
this.output.push([this.msg, 'PUT', utf32ToString(data, start, end)]);
|
||||
}
|
||||
public async end(success: boolean): Promise<boolean> {
|
||||
// simple sleep to check in tests whether ordering gets messed up
|
||||
await Promise.resolve();
|
||||
this.output.push([this.msg, 'END', success]);
|
||||
if (this.returnFalse) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
async function unhookP(parser: ApcParser, success: boolean): Promise<void> {
|
||||
let result: void | Promise<boolean>;
|
||||
let prev: boolean | undefined;
|
||||
while (result = parser.end(success, prev)) {
|
||||
prev = await result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
describe('ApcParser - async tests', () => {
|
||||
let parser: ApcParser;
|
||||
let reports: [number, string, (boolean | string | undefined)?][] = [];
|
||||
|
||||
let reports: any[] = [];
|
||||
beforeEach(() => {
|
||||
reports = [];
|
||||
parser = new ApcParser();
|
||||
parser.setHandlerFallback((id: number, action: 'START' | 'PUT' | 'END', data?: string | boolean) => {
|
||||
reports.push([id, action, data]);
|
||||
});
|
||||
parser.setHandlerFallback((id, action, data) => reports.push([id, action, data]));
|
||||
});
|
||||
|
||||
async function endP(parser: ApcParser, success: boolean): Promise<void> {
|
||||
let result: void | Promise<boolean>;
|
||||
let prev: boolean | undefined;
|
||||
while (result = parser.end(success, prev)) {
|
||||
prev = await result;
|
||||
}
|
||||
}
|
||||
|
||||
describe('async ApcHandler', () => {
|
||||
it('should handle async handler', async () => {
|
||||
const G_CODE = 0x47;
|
||||
const results: [number, string][] = [];
|
||||
parser.registerHandler(G_CODE, new ApcHandler(async (data: string) => {
|
||||
await new Promise(res => setTimeout(res, 10));
|
||||
results.push([G_CODE, data]);
|
||||
return true;
|
||||
}));
|
||||
parser.start();
|
||||
const data = toUtf32('Gf=100,a=T');
|
||||
parser.put(data, 0, data.length);
|
||||
await endP(parser, true);
|
||||
assert.deepEqual(results, [[G_CODE, 'f=100,a=T']]);
|
||||
describe('sync and async mixed', () => {
|
||||
describe('sync | async | sync', () => {
|
||||
it('first should run, cleanup action for others', async () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', false));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', false));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's2', false));
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await unhookP(parser, true);
|
||||
assert.deepEqual(reports, [
|
||||
// messages from TestHandler
|
||||
['s2', 'START'],
|
||||
['a1', 'START'],
|
||||
['s1', 'START'],
|
||||
['s2', 'PUT', 'Here comes'],
|
||||
['a1', 'PUT', 'Here comes'],
|
||||
['s1', 'PUT', 'Here comes'],
|
||||
['s2', 'PUT', 'the mouse!'],
|
||||
['a1', 'PUT', 'the mouse!'],
|
||||
['s1', 'PUT', 'the mouse!'],
|
||||
['s2', 'END', true],
|
||||
['a1', 'END', false], // important: a1 before s1
|
||||
['s1', 'END', false]
|
||||
]);
|
||||
});
|
||||
it('all should run', async () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', true));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', true));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's2', true));
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await unhookP(parser, true);
|
||||
assert.deepEqual(reports, [
|
||||
// messages from TestHandler
|
||||
['s2', 'START'],
|
||||
['a1', 'START'],
|
||||
['s1', 'START'],
|
||||
['s2', 'PUT', 'Here comes'],
|
||||
['a1', 'PUT', 'Here comes'],
|
||||
['s1', 'PUT', 'Here comes'],
|
||||
['s2', 'PUT', 'the mouse!'],
|
||||
['a1', 'PUT', 'the mouse!'],
|
||||
['s1', 'PUT', 'the mouse!'],
|
||||
['s2', 'END', true],
|
||||
['a1', 'END', true], // important: a1 before s1
|
||||
['s1', 'END', true]
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('async | sync | async', () => {
|
||||
it('first should run, cleanup action for others', async () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', false));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', false));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a2', false));
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await unhookP(parser, true);
|
||||
assert.deepEqual(reports, [
|
||||
// messages from TestHandler
|
||||
['a2', 'START'],
|
||||
['s1', 'START'],
|
||||
['a1', 'START'],
|
||||
['a2', 'PUT', 'Here comes'],
|
||||
['s1', 'PUT', 'Here comes'],
|
||||
['a1', 'PUT', 'Here comes'],
|
||||
['a2', 'PUT', 'the mouse!'],
|
||||
['s1', 'PUT', 'the mouse!'],
|
||||
['a1', 'PUT', 'the mouse!'],
|
||||
['a2', 'END', true],
|
||||
['s1', 'END', false], // important: s1 between a2 .. a1
|
||||
['a1', 'END', false]
|
||||
]);
|
||||
});
|
||||
it('all should run', async () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', true));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', true));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a2', true));
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await unhookP(parser, true);
|
||||
assert.deepEqual(reports, [
|
||||
// messages from TestHandler
|
||||
['a2', 'START'],
|
||||
['s1', 'START'],
|
||||
['a1', 'START'],
|
||||
['a2', 'PUT', 'Here comes'],
|
||||
['s1', 'PUT', 'Here comes'],
|
||||
['a1', 'PUT', 'Here comes'],
|
||||
['a2', 'PUT', 'the mouse!'],
|
||||
['s1', 'PUT', 'the mouse!'],
|
||||
['a1', 'PUT', 'the mouse!'],
|
||||
['a2', 'END', true],
|
||||
['s1', 'END', true], // important: s1 between a2 .. a1
|
||||
['a1', 'END', true]
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('ApcHandlerFactory', () => {
|
||||
it('should be called once on end(true)', async () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(async data => { reports.push(data); return true; }));
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await unhookP(parser, true);
|
||||
assert.deepEqual(reports, ['Here comes the mouse!']);
|
||||
});
|
||||
it('should not be called on end(false)', async () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(async data => { reports.push(data); return true; }));
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await unhookP(parser, false);
|
||||
assert.deepEqual(reports, []);
|
||||
});
|
||||
it('should be disposable', async () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(async data => { reports.push(['one', data]); return true; }));
|
||||
const dispo = parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(async data => { reports.push(['two', data]); return true; }));
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await unhookP(parser, true);
|
||||
assert.deepEqual(reports, [['two', 'Here comes the mouse!']]);
|
||||
dispo.dispose();
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
data = toUtf32('some other');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' data');
|
||||
parser.put(data, 0, data.length);
|
||||
await unhookP(parser, true);
|
||||
assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'some other data']]);
|
||||
});
|
||||
it('should respect return false', async () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(async data => { reports.push(['one', data]); return true; }));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(async data => { reports.push(['two', data]); return false; }));
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await unhookP(parser, true);
|
||||
assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'Here comes the mouse!']]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { IApcHandler, IHandlerCollection, ApcFallbackHandlerType, IApcParser, ISubParserStackState } from 'common/parser/Types';
|
||||
import { ApcState, ParserConstants } from 'common/parser/Constants';
|
||||
import { ParserConstants } from 'common/parser/Constants';
|
||||
import { utf32ToString } from 'common/input/TextDecoder';
|
||||
import { IDisposable } from 'common/Types';
|
||||
|
||||
@@ -19,10 +19,9 @@ const EMPTY_HANDLERS: IApcHandler[] = [];
|
||||
* The identifier is the character code of the first byte after ESC _.
|
||||
*/
|
||||
export class ApcParser implements IApcParser {
|
||||
private _state = ApcState.START;
|
||||
private _active = EMPTY_HANDLERS;
|
||||
private _id = -1;
|
||||
private _handlers: IHandlerCollection<IApcHandler> = Object.create(null);
|
||||
private _active = EMPTY_HANDLERS;
|
||||
private _ident: number = 0;
|
||||
private _handlerFb: ApcFallbackHandlerType = () => { };
|
||||
private _stack: ISubParserStackState = {
|
||||
paused: false,
|
||||
@@ -64,22 +63,24 @@ export class ApcParser implements IApcParser {
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
// force cleanup handlers if payload was already sent
|
||||
if (this._state === ApcState.PAYLOAD) {
|
||||
// force cleanup handlers
|
||||
if (this._active.length) {
|
||||
for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) {
|
||||
this._active[j].end(false);
|
||||
}
|
||||
}
|
||||
this._stack.paused = false;
|
||||
this._active = EMPTY_HANDLERS;
|
||||
this._id = -1;
|
||||
this._state = ApcState.START;
|
||||
this._ident = 0;
|
||||
}
|
||||
|
||||
private _start(): void {
|
||||
this._active = this._handlers[this._id] || EMPTY_HANDLERS;
|
||||
public start(ident: number): void {
|
||||
// always reset leftover handlers
|
||||
this.reset();
|
||||
this._ident = ident;
|
||||
this._active = this._handlers[ident] || EMPTY_HANDLERS;
|
||||
if (!this._active.length) {
|
||||
this._handlerFb(this._id, 'START');
|
||||
this._handlerFb(this._ident, 'START');
|
||||
} else {
|
||||
for (let j = this._active.length - 1; j >= 0; j--) {
|
||||
this._active[j].start();
|
||||
@@ -87,9 +88,9 @@ export class ApcParser implements IApcParser {
|
||||
}
|
||||
}
|
||||
|
||||
private _put(data: Uint32Array, start: number, end: number): void {
|
||||
public put(data: Uint32Array, start: number, end: number): void {
|
||||
if (!this._active.length) {
|
||||
this._handlerFb(this._id, 'PUT', utf32ToString(data, start, end));
|
||||
this._handlerFb(this._ident, 'PUT', utf32ToString(data, start, end));
|
||||
} else {
|
||||
for (let j = this._active.length - 1; j >= 0; j--) {
|
||||
this._active[j].put(data, start, end);
|
||||
@@ -97,100 +98,51 @@ export class ApcParser implements IApcParser {
|
||||
}
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
// always reset leftover handlers
|
||||
this.reset();
|
||||
this._state = ApcState.ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Put data to current APC command.
|
||||
* For APC, the first character is used as the identifier.
|
||||
* Format: ESC _ <identifier><payload> ESC \
|
||||
* Example: ESC _ G f=100,a=T;... ESC \ (Kitty graphics, identifier='G')
|
||||
*/
|
||||
public put(data: Uint32Array, start: number, end: number): void {
|
||||
if (this._state === ApcState.ABORT) {
|
||||
return;
|
||||
}
|
||||
if (this._state === ApcState.ID) {
|
||||
// The first character is the identifier
|
||||
if (start < end) {
|
||||
this._id = data[start++];
|
||||
this._state = ApcState.PAYLOAD;
|
||||
this._start();
|
||||
}
|
||||
}
|
||||
if (this._state === ApcState.PAYLOAD && end - start > 0) {
|
||||
this._put(data, start, end);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates end of an APC command.
|
||||
* Whether the APC got aborted or finished normally
|
||||
* is indicated by `success`.
|
||||
*/
|
||||
public end(success: boolean, promiseResult: boolean = true): void | Promise<boolean> {
|
||||
if (this._state === ApcState.START) {
|
||||
return;
|
||||
}
|
||||
// do nothing if command was faulty
|
||||
if (this._state !== ApcState.ABORT) {
|
||||
// if we are still in ID state and get an early end
|
||||
// means we got an empty APC sequence with no identifier,
|
||||
// which is invalid - just reset and return
|
||||
if (this._state === ApcState.ID) {
|
||||
this._active = EMPTY_HANDLERS;
|
||||
this._id = -1;
|
||||
this._state = ApcState.START;
|
||||
return;
|
||||
if (!this._active.length) {
|
||||
this._handlerFb(this._ident, 'END', success);
|
||||
} else {
|
||||
let handlerResult: boolean | Promise<boolean> = false;
|
||||
let j = this._active.length - 1;
|
||||
let fallThrough = false;
|
||||
if (this._stack.paused) {
|
||||
j = this._stack.loopPosition - 1;
|
||||
handlerResult = promiseResult;
|
||||
fallThrough = this._stack.fallThrough;
|
||||
this._stack.paused = false;
|
||||
}
|
||||
|
||||
if (!this._active.length) {
|
||||
this._handlerFb(this._id, 'END', success);
|
||||
} else {
|
||||
let handlerResult: boolean | Promise<boolean> = false;
|
||||
let j = this._active.length - 1;
|
||||
let fallThrough = false;
|
||||
if (this._stack.paused) {
|
||||
j = this._stack.loopPosition - 1;
|
||||
handlerResult = promiseResult;
|
||||
fallThrough = this._stack.fallThrough;
|
||||
this._stack.paused = false;
|
||||
}
|
||||
if (!fallThrough && handlerResult === false) {
|
||||
for (; j >= 0; j--) {
|
||||
handlerResult = this._active[j].end(success);
|
||||
if (handlerResult === true) {
|
||||
break;
|
||||
} else if (handlerResult instanceof Promise) {
|
||||
this._stack.paused = true;
|
||||
this._stack.loopPosition = j;
|
||||
this._stack.fallThrough = false;
|
||||
return handlerResult;
|
||||
}
|
||||
}
|
||||
j--;
|
||||
}
|
||||
// cleanup left over handlers
|
||||
// we always have to call .end for proper cleanup,
|
||||
// here we use `success` to indicate whether a handler should execute
|
||||
if (!fallThrough && handlerResult === false) {
|
||||
for (; j >= 0; j--) {
|
||||
handlerResult = this._active[j].end(false);
|
||||
if (handlerResult instanceof Promise) {
|
||||
handlerResult = this._active[j].end(success);
|
||||
if (handlerResult === true) {
|
||||
break;
|
||||
} else if (handlerResult instanceof Promise) {
|
||||
this._stack.paused = true;
|
||||
this._stack.loopPosition = j;
|
||||
this._stack.fallThrough = true;
|
||||
this._stack.fallThrough = false;
|
||||
return handlerResult;
|
||||
}
|
||||
}
|
||||
j--;
|
||||
}
|
||||
// cleanup left over handlers (fallThrough for async)
|
||||
for (; j >= 0; j--) {
|
||||
handlerResult = this._active[j].end(false);
|
||||
if (handlerResult instanceof Promise) {
|
||||
this._stack.paused = true;
|
||||
this._stack.loopPosition = j;
|
||||
this._stack.fallThrough = true;
|
||||
return handlerResult;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
this._active = EMPTY_HANDLERS;
|
||||
this._id = -1;
|
||||
this._state = ApcState.START;
|
||||
this._ident = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,9 +21,11 @@ export const enum ParserState {
|
||||
DCS_IGNORE = 11,
|
||||
DCS_INTERMEDIATE = 12,
|
||||
DCS_PASSTHROUGH = 13,
|
||||
APC_STRING = 14,
|
||||
APC_ENTRY = 14,
|
||||
APC_INTERMEDIATE = 15,
|
||||
APC_PASSTHROUGH = 16,
|
||||
// Number of states, meaning LAST_STATE + 1.
|
||||
STATE_LENGTH = 15
|
||||
STATE_LENGTH = 17
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,16 +62,6 @@ export const enum OscState {
|
||||
ABORT = 3
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal states of ApcParser.
|
||||
*/
|
||||
export const enum ApcState {
|
||||
START = 0,
|
||||
ID = 1,
|
||||
PAYLOAD = 2,
|
||||
ABORT = 3
|
||||
}
|
||||
|
||||
// payload limit for OSC and DCS
|
||||
export const enum ParserConstants {
|
||||
PAYLOAD_LIMIT = 10000000
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IParsingState, IParams, ParamsArray, IOscParser, IOscHandler, OscFallbackHandlerType, IFunctionIdentifier, IParserStackState, ParserStackType, ResumableHandlersType } from 'common/parser/Types';
|
||||
import { IParsingState, IParams, ParamsArray, IOscParser, IOscHandler, OscFallbackHandlerType, IFunctionIdentifier, IParserStackState, ParserStackType } from 'common/parser/Types';
|
||||
import { EscapeSequenceParser, TransitionTable, VT500_TRANSITION_TABLE } from 'common/parser/EscapeSequenceParser';
|
||||
import { assert } from 'chai';
|
||||
import { StringToUtf32, stringFromCodePoint, utf32ToString } from 'common/input/TextDecoder';
|
||||
@@ -12,6 +12,7 @@ import { Params } from 'common/parser/Params';
|
||||
import { OscHandler } from 'common/parser/OscParser';
|
||||
import { IDisposable } from 'common/Types';
|
||||
import { DcsHandler } from 'common/parser/DcsParser';
|
||||
import { ApcHandler } from 'common/parser/ApcParser';
|
||||
|
||||
|
||||
function r(a: number, b: number): string[] {
|
||||
@@ -145,6 +146,15 @@ const testTerminal: any = {
|
||||
},
|
||||
actionDCSUnhook(success: boolean): void {
|
||||
this.calls.push(['dcs unhook', success]);
|
||||
},
|
||||
actionAPCStart(): void {
|
||||
this.calls.push(['apc start']);
|
||||
},
|
||||
actionAPCPut(s: string): void {
|
||||
this.calls.push(['apc put', s]);
|
||||
},
|
||||
actionAPCEnd(success: boolean): void {
|
||||
this.calls.push(['apc end', success]);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -163,7 +173,9 @@ const states: number[] = [
|
||||
ParserState.DCS_IGNORE,
|
||||
ParserState.DCS_INTERMEDIATE,
|
||||
ParserState.DCS_PASSTHROUGH,
|
||||
ParserState.APC_STRING
|
||||
ParserState.APC_ENTRY,
|
||||
ParserState.APC_INTERMEDIATE,
|
||||
ParserState.APC_PASSTHROUGH
|
||||
];
|
||||
let state: any;
|
||||
|
||||
@@ -198,6 +210,18 @@ testParser.setDcsHandlerFallback((collectAndFlag, action, payload) => {
|
||||
testTerminal.actionDCSUnhook(payload);
|
||||
}
|
||||
});
|
||||
testParser.setApcHandlerFallback((collectAndFlag, action, payload) => {
|
||||
switch (action) {
|
||||
case 'START':
|
||||
testTerminal.actionAPCStart(payload);
|
||||
break;
|
||||
case 'PUT':
|
||||
testTerminal.actionAPCPut(payload);
|
||||
break;
|
||||
case 'END':
|
||||
testTerminal.actionAPCEnd(payload);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// translate string based parse calls into typed array based
|
||||
@@ -278,7 +302,7 @@ describe('EscapeSequenceParser', () => {
|
||||
const exceptions: { [key: number]: { [key: string]: any[] } } = {
|
||||
8: { '\x18': [], '\x1a': [] }, // abort OSC_STRING
|
||||
13: { '\x18': [['dcs unhook', false]], '\x1a': [['dcs unhook', false]] }, // abort DCS_PASSTHROUGH
|
||||
14: { '\x18': [], '\x1a': [] } // abort APC_STRING
|
||||
16: { '\x18': [['apc end', false]], '\x1a': [['apc end', false]] } // abort APC_PASSTHROUGH
|
||||
};
|
||||
parser.reset();
|
||||
testTerminal.clear();
|
||||
@@ -725,20 +749,6 @@ describe('EscapeSequenceParser', () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
it('trans ANYWHERE/ESCAPE --> APC_STRING', () => {
|
||||
parser.reset();
|
||||
// C0 (ESC _)
|
||||
parse(parser, '\x1b_');
|
||||
assert.equal(parser.currentState, ParserState.APC_STRING);
|
||||
parser.reset();
|
||||
// C1
|
||||
for (state in states) {
|
||||
parser.currentState = state;
|
||||
parse(parser, '\x9f');
|
||||
assert.equal(parser.currentState, ParserState.APC_STRING);
|
||||
parser.reset();
|
||||
}
|
||||
});
|
||||
it('state SOS_PM_STRING ignore rules', () => {
|
||||
parser.reset();
|
||||
let ignored = r(0x00, 0x18);
|
||||
@@ -1035,6 +1045,134 @@ describe('EscapeSequenceParser', () => {
|
||||
parser.reset();
|
||||
testTerminal.clear();
|
||||
});
|
||||
it('state APC_ENTRY', () => {
|
||||
parser.reset();
|
||||
// C0
|
||||
parse(parser, '\x1b_');
|
||||
assert.equal(parser.currentState, ParserState.APC_ENTRY);
|
||||
parser.reset();
|
||||
// C1
|
||||
for (state in states) {
|
||||
parser.currentState = state;
|
||||
parse(parser, '\x9f');
|
||||
assert.equal(parser.currentState, ParserState.APC_ENTRY);
|
||||
parser.reset();
|
||||
}
|
||||
});
|
||||
it('state APC_ENTRY ignore rules', () => {
|
||||
parser.reset();
|
||||
const ignored = [
|
||||
'\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08',
|
||||
'\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11',
|
||||
'\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f'];
|
||||
for (let i = 0; i < ignored.length; ++i) {
|
||||
parser.currentState = ParserState.APC_ENTRY;
|
||||
parse(parser, ignored[i]);
|
||||
assert.equal(parser.currentState, ParserState.APC_ENTRY);
|
||||
parser.reset();
|
||||
}
|
||||
});
|
||||
it('trans APC_ENTRY --> APC_INTERMEDIATE with collect action', () => {
|
||||
parser.reset();
|
||||
const collect = r(0x20, 0x30);
|
||||
for (let i = 0; i < collect.length; ++i) {
|
||||
parser.currentState = ParserState.APC_ENTRY;
|
||||
parse(parser, collect[i]);
|
||||
assert.equal(parser.currentState, ParserState.APC_INTERMEDIATE);
|
||||
assert.equal(parser.collect, collect[i]);
|
||||
parser.reset();
|
||||
}
|
||||
});
|
||||
it('trans APC_ENTRY --> APC_PASSTHROUGH with start', () => {
|
||||
parser.reset();
|
||||
testTerminal.clear();
|
||||
const collect = r(0x30, 0x7f);
|
||||
for (let i = 0; i < collect.length; ++i) {
|
||||
parser.currentState = ParserState.APC_ENTRY;
|
||||
parse(parser, collect[i]);
|
||||
assert.equal(parser.currentState, ParserState.APC_PASSTHROUGH);
|
||||
testTerminal.compare([['apc start']]);
|
||||
parser.reset();
|
||||
testTerminal.clear();
|
||||
}
|
||||
});
|
||||
it('trans APC_INTERMEDIATE --> APC_PASSTHROUGH with start', () => {
|
||||
parser.reset();
|
||||
testTerminal.clear();
|
||||
const collect = r(0x30, 0x7f);
|
||||
for (let i = 0; i < collect.length; ++i) {
|
||||
parser.currentState = ParserState.APC_INTERMEDIATE;
|
||||
parse(parser, collect[i]);
|
||||
assert.equal(parser.currentState, ParserState.APC_PASSTHROUGH);
|
||||
testTerminal.compare([['apc start']]);
|
||||
parser.reset();
|
||||
testTerminal.clear();
|
||||
}
|
||||
});
|
||||
it('state APC_INTERMEDIATE ignore rules', () => {
|
||||
parser.reset();
|
||||
const ignored = [
|
||||
'\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08',
|
||||
'\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11',
|
||||
'\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f'];
|
||||
for (let i = 0; i < ignored.length; ++i) {
|
||||
parser.currentState = ParserState.APC_INTERMEDIATE;
|
||||
parse(parser, ignored[i]);
|
||||
assert.equal(parser.currentState, ParserState.APC_INTERMEDIATE);
|
||||
parser.reset();
|
||||
}
|
||||
});
|
||||
it('state APC_PASSTHROUGH put action', () => {
|
||||
parser.reset();
|
||||
testTerminal.clear();
|
||||
let puts = r(0x08, 0x0e);
|
||||
puts = puts.concat(r(0x20, 0x7f));
|
||||
for (let i = 0; i < puts.length; ++i) {
|
||||
parser.currentState = ParserState.APC_PASSTHROUGH;
|
||||
parse(parser, puts[i]);
|
||||
assert.equal(parser.currentState, ParserState.APC_PASSTHROUGH);
|
||||
testTerminal.compare([['apc put', puts[i]]]);
|
||||
parser.reset();
|
||||
testTerminal.clear();
|
||||
}
|
||||
});
|
||||
it('state APC_PASSTHROUGH ignore rules', () => {
|
||||
parser.reset();
|
||||
testTerminal.clear();
|
||||
let puts = r(0x00, 0x08);
|
||||
puts = puts.concat(r(0x0e, 0x18));
|
||||
puts.push('\x19');
|
||||
puts = puts.concat(r(0x1c, 0x20));
|
||||
puts.push('\x7f');
|
||||
for (let i = 0; i < puts.length; ++i) {
|
||||
parser.currentState = ParserState.APC_PASSTHROUGH;
|
||||
parse(parser, puts[i]);
|
||||
assert.equal(parser.currentState, ParserState.APC_PASSTHROUGH);
|
||||
testTerminal.compare([]);
|
||||
parser.reset();
|
||||
testTerminal.clear();
|
||||
}
|
||||
});
|
||||
it('trans APC_PASSTHROUGH --> GROUND|ESCAPE with end action', () => {
|
||||
parser.reset();
|
||||
testTerminal.clear();
|
||||
const collect = ['\x9c', '\x18', '\x1a']; // ST - true, CAN & SUB - false
|
||||
for (let i = 0; i < collect.length; ++i) {
|
||||
parser.currentState = ParserState.APC_PASSTHROUGH;
|
||||
parse(parser, collect[i]);
|
||||
assert.equal(parser.currentState, ParserState.GROUND);
|
||||
testTerminal.compare([['apc end', collect[i] === '\x9c']]);
|
||||
parser.reset();
|
||||
testTerminal.clear();
|
||||
}
|
||||
// ESC end
|
||||
parser.currentState = ParserState.APC_PASSTHROUGH;
|
||||
parse(parser, '\x1b');
|
||||
assert.equal(parser.currentState, ParserState.ESCAPE);
|
||||
testTerminal.compare([['apc end', true]]);
|
||||
parser.reset();
|
||||
testTerminal.clear();
|
||||
});
|
||||
});
|
||||
|
||||
function test(s: string, value: any, noReset: any): void {
|
||||
@@ -1100,6 +1238,42 @@ describe('EscapeSequenceParser', () => {
|
||||
['print', 'defg']
|
||||
], null);
|
||||
});
|
||||
it('single APC', () => {
|
||||
test('\x1b_X3+$aäbc;däe\x9c', [
|
||||
['apc start'],
|
||||
['apc put', '3+$aäbc;däe'],
|
||||
['apc end', true]
|
||||
], null);
|
||||
});
|
||||
it('multi APC', () => {
|
||||
test('\x1b_Xabc;de', [
|
||||
['apc start'],
|
||||
['apc put', 'abc;de']
|
||||
], null);
|
||||
testTerminal.clear();
|
||||
test('abc\x9c', [
|
||||
['apc put', 'abc'],
|
||||
['apc end', true]
|
||||
], true);
|
||||
});
|
||||
it('print + DCS(C1) + print', () => {
|
||||
test('abc\x9fAbc;de\x9cxyz', [
|
||||
['print', 'abc'],
|
||||
['apc start'],
|
||||
['apc put', 'bc;de'],
|
||||
['apc end', true],
|
||||
['print', 'xyz']
|
||||
], null);
|
||||
});
|
||||
it('print + DCS(C0) + print', () => {
|
||||
test('abc\x1b_Abc;de\x1b\\xyz', [
|
||||
['print', 'abc'],
|
||||
['apc start'],
|
||||
['apc put', 'bc;de'],
|
||||
['apc end', true],
|
||||
['print', 'xyz']
|
||||
], null);
|
||||
});
|
||||
it('error recovery', () => {
|
||||
test('\x1b[1€abcdefg\x9b<;c', [
|
||||
['print', 'abcdefg'],
|
||||
@@ -1146,6 +1320,22 @@ describe('EscapeSequenceParser', () => {
|
||||
['dcs unhook', false] // false for abort
|
||||
], null);
|
||||
});
|
||||
it('CAN should abort APC', () => {
|
||||
test('abc\x9fXbc;de\x18', [
|
||||
['print', 'abc'],
|
||||
['apc start'],
|
||||
['apc put', 'bc;de'],
|
||||
['apc end', false] // false for abort
|
||||
], null);
|
||||
});
|
||||
it('SUB should abort APC', () => {
|
||||
test('abc\x9fXbc;de\x1a', [
|
||||
['print', 'abc'],
|
||||
['apc start'],
|
||||
['apc put', 'bc;de'],
|
||||
['apc end', false] // false for abort
|
||||
], null);
|
||||
});
|
||||
it('CAN should abort OSC', () => {
|
||||
test('\x1b]0;abc123€öäü\x18', [
|
||||
['osc', '0;abc123€öäü, success: false']
|
||||
@@ -1210,6 +1400,7 @@ describe('EscapeSequenceParser', () => {
|
||||
const exe: string[] = [];
|
||||
const osc: [number, string][] = [];
|
||||
const dcs: ([string] | [string, string] | [string, string, ParamsArray, number])[] = [];
|
||||
const apc: [string, string][] = [];
|
||||
function clearAccu(): void {
|
||||
print = '';
|
||||
esc.length = 0;
|
||||
@@ -1217,6 +1408,7 @@ describe('EscapeSequenceParser', () => {
|
||||
exe.length = 0;
|
||||
osc.length = 0;
|
||||
dcs.length = 0;
|
||||
apc.length = 0;
|
||||
}
|
||||
beforeEach(() => {
|
||||
parser2 = new TestEscapeSequenceParser();
|
||||
@@ -1574,6 +1766,95 @@ describe('EscapeSequenceParser', () => {
|
||||
assert.deepEqual(dcsCustom, [['A', [1, 2, 3], 'abc']]);
|
||||
});
|
||||
});
|
||||
it('APC handler', () => {
|
||||
parser2.registerApcHandler({ intermediates: '+', final: 'p' }, {
|
||||
start: function (): void {
|
||||
apc.push(['start', '']);
|
||||
},
|
||||
put: function (data: Uint32Array, start: number, end: number): void {
|
||||
let s = '';
|
||||
for (let i = start; i < end; ++i) {
|
||||
s += stringFromCodePoint(data[i]);
|
||||
}
|
||||
apc.push(['put', s]);
|
||||
},
|
||||
end: function (success): boolean {
|
||||
apc.push(['end', success ? '1' : '0']);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
parse(parser2, '\x1b_+pabc');
|
||||
parse(parser2, ';de\x9c');
|
||||
assert.deepEqual(apc, [
|
||||
['start', ''],
|
||||
['put', 'abc'], ['put', ';de'],
|
||||
['end', '1']
|
||||
]);
|
||||
parser2.clearApcHandler({ intermediates: '+', final: 'p' });
|
||||
parser2.clearApcHandler({ intermediates: '+', final: 'p' }); // should not throw
|
||||
clearAccu();
|
||||
parse(parser2, '\x1b_+pabc');
|
||||
parse(parser2, ';de\x9c');
|
||||
assert.deepEqual(apc, []);
|
||||
});
|
||||
describe('APC custom handlers', () => {
|
||||
const APC_INPUT = '\x1b_+pabc\x1b\\';
|
||||
it('Prevent fallback', () => {
|
||||
const apcCustom: [string, string][] = [];
|
||||
parser2.registerApcHandler({ intermediates: '+', final: 'p' }, new ApcHandler(data => { apcCustom.push(['A', data]); return true; }));
|
||||
parser2.registerApcHandler({ intermediates: '+', final: 'p' }, new ApcHandler(data => { apcCustom.push(['B', data]); return true; }));
|
||||
parse(parser2, APC_INPUT);
|
||||
assert.deepEqual(apcCustom, [['B', 'abc']]);
|
||||
});
|
||||
it('Allow fallback', () => {
|
||||
const apcCustom: [string, string][] = [];
|
||||
parser2.registerApcHandler({ intermediates: '+', final: 'p' }, new ApcHandler(data => { apcCustom.push(['A', data]); return true; }));
|
||||
parser2.registerApcHandler({ intermediates: '+', final: 'p' }, new ApcHandler(data => { apcCustom.push(['B', data]); return false; }));
|
||||
parse(parser2, APC_INPUT);
|
||||
assert.deepEqual(apcCustom, [['B', 'abc'], ['A', 'abc']]);
|
||||
});
|
||||
it('Multiple custom handlers fallback once', () => {
|
||||
const apcCustom: [string, string][] = [];
|
||||
parser2.registerApcHandler({ intermediates: '+', final: 'p' }, new ApcHandler(data => { apcCustom.push(['A', data]); return true; }));
|
||||
parser2.registerApcHandler({ intermediates: '+', final: 'p' }, new ApcHandler(data => { apcCustom.push(['B', data]); return true; }));
|
||||
parser2.registerApcHandler({ intermediates: '+', final: 'p' }, new ApcHandler(data => { apcCustom.push(['C', data]); return false; }));
|
||||
parse(parser2, APC_INPUT);
|
||||
assert.deepEqual(apcCustom, [['C', 'abc'], ['B', 'abc']]);
|
||||
});
|
||||
it('Multiple custom handlers no fallback', () => {
|
||||
const apcCustom: [string, string][] = [];
|
||||
parser2.registerApcHandler({ intermediates: '+', final: 'p' }, new ApcHandler(data => { apcCustom.push(['A', data]); return true; }));
|
||||
parser2.registerApcHandler({ intermediates: '+', final: 'p' }, new ApcHandler(data => { apcCustom.push(['B', data]); return true; }));
|
||||
parser2.registerApcHandler({ intermediates: '+', final: 'p' }, new ApcHandler(data => { apcCustom.push(['C', data]); return true; }));
|
||||
parse(parser2, APC_INPUT);
|
||||
assert.deepEqual(apcCustom, [['C', 'abc']]);
|
||||
});
|
||||
it('Execution order should go from latest handler down to the original', () => {
|
||||
const order: number[] = [];
|
||||
parser2.registerApcHandler({ intermediates: '+', final: 'p' }, new ApcHandler(() => { order.push(1); return true; }));
|
||||
parser2.registerApcHandler({ intermediates: '+', final: 'p' }, new ApcHandler(() => { order.push(2); return false; }));
|
||||
parser2.registerApcHandler({ intermediates: '+', final: 'p' }, new ApcHandler(() => { order.push(3); return false; }));
|
||||
parse(parser2, APC_INPUT);
|
||||
assert.deepEqual(order, [3, 2, 1]);
|
||||
});
|
||||
it('Dispose should work', () => {
|
||||
const apcCustom: [string, string][] = [];
|
||||
parser2.registerApcHandler({ intermediates: '+', final: 'p' }, new ApcHandler(data => { apcCustom.push(['A', data]); return true; }));
|
||||
const dispo = parser2.registerApcHandler({ intermediates: '+', final: 'p' }, new ApcHandler(data => { apcCustom.push(['B', data]); return true; }));
|
||||
dispo.dispose();
|
||||
parse(parser2, APC_INPUT);
|
||||
assert.deepEqual(apcCustom, [['A', 'abc']]);
|
||||
});
|
||||
it('Should not corrupt the parser when dispose is called twice', () => {
|
||||
const apcCustom: [string, string][] = [];
|
||||
parser2.registerApcHandler({ intermediates: '+', final: 'p' }, new ApcHandler(data => { apcCustom.push(['A', data]); return true; }));
|
||||
const dispo = parser2.registerApcHandler({ intermediates: '+', final: 'p' }, new ApcHandler(data => { apcCustom.push(['B', data]); return true; }));
|
||||
dispo.dispose();
|
||||
dispo.dispose();
|
||||
parse(parser2, APC_INPUT);
|
||||
assert.deepEqual(apcCustom, [['A', 'abc']]);
|
||||
});
|
||||
});
|
||||
it('ERROR handler', () => {
|
||||
let errorState: IParsingState | null = null;
|
||||
parser2.setErrorHandler(function (state: IParsingState): IParsingState {
|
||||
@@ -1625,15 +1906,22 @@ describe('EscapeSequenceParser', () => {
|
||||
assert.throws(() => { parser.identifier({ final: '\x7f' }); }, 'final must be in range 64 .. 126');
|
||||
assert.throws(() => { parser.identifier({ final: 'zz' }); }, 'final must be a single byte');
|
||||
});
|
||||
it('final ESC range 0x30 .. 0x7e, one byte', () => {
|
||||
it('final ESC + APC range 0x30 .. 0x7e, one byte', () => {
|
||||
for (let i = 0x30; i <= 0x7e; ++i) {
|
||||
const final = String.fromCharCode(i);
|
||||
let handler: IDisposable | undefined;
|
||||
assert.doesNotThrow(() => { handler = parser.registerEscHandler({ final }, () => true); }, 'final must be in range 48 .. 126');
|
||||
if (handler) handler.dispose();
|
||||
assert.doesNotThrow(
|
||||
() => { handler = parser.registerApcHandler({ final }, {start: () => {}, put: ()=>{}, end: ()=>true}); },
|
||||
'final must be in range 48 .. 126'
|
||||
);
|
||||
if (handler) handler.dispose();
|
||||
}
|
||||
assert.throws(() => { parser.registerEscHandler({ final: '\x2f' }, () => true); }, 'final must be in range 48 .. 126');
|
||||
assert.throws(() => { parser.registerEscHandler({ final: '\x7f' }, () => true); }, 'final must be in range 48 .. 126');
|
||||
assert.throws(() => { parser.registerApcHandler({ final: '\x2f' }, {start: () => {}, put: ()=>{}, end: ()=>true}); }, 'final must be in range 48 .. 126');
|
||||
assert.throws(() => { parser.registerApcHandler({ final: '\x7f' }, {start: () => {}, put: ()=>{}, end: ()=>true}); }, 'final must be in range 48 .. 126');
|
||||
});
|
||||
it('id calculation - should stacking prefix -> intermediate -> final', () => {
|
||||
assert.equal(parser.identToString(parser.identifier({ final: 'z' })), 'z');
|
||||
@@ -1705,6 +1993,25 @@ describe('EscapeSequenceParser', () => {
|
||||
]
|
||||
);
|
||||
});
|
||||
it('APC', () => {
|
||||
const callstack: any[] = [];
|
||||
const h1 = parser.registerApcHandler({ final: 'z' }, new ApcHandler(data => { callstack.push(['z', data]); return true; }));
|
||||
const h2 = parser.registerApcHandler({ intermediates: '!', final: 'z' }, new ApcHandler(data => { callstack.push(['!z', data]); return true; }));
|
||||
const h3 = parser.registerApcHandler({ intermediates: '!!', final: 'z' }, new ApcHandler(data => { callstack.push(['!!z', data]); return true; }));
|
||||
parse(parser, '\x1b_zAB\x1b\\\x1b_!zAB\x1b\\\x1b_!!zAB\x1b\\');
|
||||
h1.dispose();
|
||||
h2.dispose();
|
||||
h3.dispose();
|
||||
parse(parser, '\x1b_zAB\x1b\\\x1b_!zAB\x1b\\\x1b_!!zAB\x1b\\');
|
||||
assert.deepEqual(
|
||||
callstack,
|
||||
[
|
||||
['z', 'AB'],
|
||||
['!z', 'AB'],
|
||||
['!!z', 'AB'],
|
||||
]
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
// TODO: error conditions and error recovery (not implemented yet in parser)
|
||||
@@ -1758,9 +2065,9 @@ async function throwsAsync(fn: () => Promise<any>, message?: string | undefined)
|
||||
}
|
||||
|
||||
describe('EscapeSequenceParser - async', () => {
|
||||
// sequences: SGR 1;31 | hello SP | ESC %G | wor | ESC E | ld! | SGR 0 | EXE \r\n | $> | DCS 1;2 a [xyz] ST | OSC 1;foo=bar ST | FIN
|
||||
// needed handlers: CSI m, PRINT, ESC %G, ESC E, EXE \r, EXE \n, OSC 1
|
||||
const INPUT = '\x1b[1;31mhello \x1b%Gwor\x1bEld!\x1b[0m\r\n$>\x1bP1;2axyz\x1b\\\x1b]1;foo=bar\x1b\\FIN';
|
||||
// sequences: SGR 1;31 | hello SP | ESC %G | wor | ESC E | ld! | SGR 0 | EXE \r\n | $> | DCS 1;2 a [xyz] ST | OSC 1;foo=bar ST | APC X abc ST | FIN
|
||||
// needed handlers: CSI m, PRINT, ESC %G, ESC E, EXE \r, EXE \n, OSC 1, APC X
|
||||
const INPUT = '\x1b[1;31mhello \x1b%Gwor\x1bEld!\x1b[0m\r\n$>\x1bP1;2axyz\x1b\\\x1b]1;foo=bar\x1b\\\x1b_Xabc\x1b\\FIN';
|
||||
let RESULT: any[];
|
||||
let parser: TestEscapeSequenceParser;
|
||||
const callstack: any[] = [];
|
||||
@@ -1782,6 +2089,7 @@ describe('EscapeSequenceParser - async', () => {
|
||||
['PRINT', '$>'],
|
||||
['DCS a', ['xyz', [1, 2]]],
|
||||
['OSC 1', 'foo=bar'],
|
||||
['APC X', 'abc'],
|
||||
['PRINT', 'FIN']
|
||||
];
|
||||
parser = new TestEscapeSequenceParser();
|
||||
@@ -1805,6 +2113,7 @@ describe('EscapeSequenceParser - async', () => {
|
||||
parser.setExecuteHandler('\n', () => { callstack.push(['EXE \n']); return true; });
|
||||
parser.registerOscHandler(1, new OscHandler(data => { callstack.push(['OSC 1', data]); return true; }));
|
||||
parser.registerDcsHandler({ final: 'a' }, new DcsHandler((data, params) => { callstack.push(['DCS a', [data, params.toArray()]]); return true; }));
|
||||
parser.registerApcHandler({ final: 'X' }, new ApcHandler(data => { callstack.push(['APC X', data]); return true; }));
|
||||
});
|
||||
|
||||
it('sync handlers keep being parsed in sync mode', () => {
|
||||
@@ -1840,6 +2149,7 @@ describe('EscapeSequenceParser - async', () => {
|
||||
parser.setExecuteHandler('\n', () => { callstack.push(['EXE \n']); return true; });
|
||||
parser.registerOscHandler(1, new OscHandler(async data => { callstack.push(['OSC 1', data]); return true; }));
|
||||
parser.registerDcsHandler({ final: 'a' }, new DcsHandler(async (data, params) => { callstack.push(['DCS a', [data, params.toArray()]]); return true; }));
|
||||
parser.registerApcHandler({ final: 'X' }, new ApcHandler(async data => { callstack.push(['APC X', data]); return true; }));
|
||||
});
|
||||
|
||||
it('sync parse call does not work anymore', () => {
|
||||
@@ -1876,7 +2186,8 @@ describe('EscapeSequenceParser - async', () => {
|
||||
[20, ParserStackType.ESC, 0],
|
||||
[27, ParserStackType.CSI, 0],
|
||||
[41, ParserStackType.DCS, 0],
|
||||
[54, ParserStackType.OSC, 0]
|
||||
[54, ParserStackType.OSC, 0],
|
||||
[62, ParserStackType.APC, 0],
|
||||
]);
|
||||
});
|
||||
it('correct result on chunked awaited parse calls', async () => {
|
||||
@@ -1903,6 +2214,7 @@ describe('EscapeSequenceParser - async', () => {
|
||||
['PRINT', '>'],
|
||||
['DCS a', ['xyz', [1, 2]]],
|
||||
['OSC 1', 'foo=bar'],
|
||||
['APC X', 'abc'],
|
||||
['PRINT', 'F'],
|
||||
['PRINT', 'I'],
|
||||
['PRINT', 'N']
|
||||
@@ -1921,7 +2233,8 @@ describe('EscapeSequenceParser - async', () => {
|
||||
[0, ParserStackType.ESC, 0],
|
||||
[0, ParserStackType.CSI, 0],
|
||||
[0, ParserStackType.DCS, 0],
|
||||
[0, ParserStackType.OSC, 0]
|
||||
[0, ParserStackType.OSC, 0],
|
||||
[0, ParserStackType.APC, 0],
|
||||
]);
|
||||
});
|
||||
it('multiple async SGR handlers', async () => {
|
||||
@@ -1941,7 +2254,8 @@ describe('EscapeSequenceParser - async', () => {
|
||||
[27, ParserStackType.CSI, 1],
|
||||
[27, ParserStackType.CSI, 0],
|
||||
[41, ParserStackType.DCS, 0],
|
||||
[54, ParserStackType.OSC, 0]
|
||||
[54, ParserStackType.OSC, 0],
|
||||
[62, ParserStackType.APC, 0],
|
||||
]);
|
||||
clearAccu();
|
||||
// after dispose we should be back to RESULT
|
||||
@@ -1954,7 +2268,8 @@ describe('EscapeSequenceParser - async', () => {
|
||||
[20, ParserStackType.ESC, 0],
|
||||
[27, ParserStackType.CSI, 0],
|
||||
[41, ParserStackType.DCS, 0],
|
||||
[54, ParserStackType.OSC, 0]
|
||||
[54, ParserStackType.OSC, 0],
|
||||
[62, ParserStackType.APC, 0],
|
||||
]);
|
||||
clearAccu();
|
||||
|
||||
@@ -1972,7 +2287,8 @@ describe('EscapeSequenceParser - async', () => {
|
||||
[20, ParserStackType.ESC, 0],
|
||||
[27, ParserStackType.CSI, 1],
|
||||
[41, ParserStackType.DCS, 0],
|
||||
[54, ParserStackType.OSC, 0]
|
||||
[54, ParserStackType.OSC, 0],
|
||||
[62, ParserStackType.APC, 0],
|
||||
]);
|
||||
clearAccu();
|
||||
// after dispose we should be back to RESULT
|
||||
@@ -1985,7 +2301,8 @@ describe('EscapeSequenceParser - async', () => {
|
||||
[20, ParserStackType.ESC, 0],
|
||||
[27, ParserStackType.CSI, 0],
|
||||
[41, ParserStackType.DCS, 0],
|
||||
[54, ParserStackType.OSC, 0]
|
||||
[54, ParserStackType.OSC, 0],
|
||||
[62, ParserStackType.APC, 0],
|
||||
]);
|
||||
});
|
||||
it('multiple async ESC handlers', async () => {
|
||||
@@ -2003,7 +2320,8 @@ describe('EscapeSequenceParser - async', () => {
|
||||
[20, ParserStackType.ESC, 0],
|
||||
[27, ParserStackType.CSI, 0],
|
||||
[41, ParserStackType.DCS, 0],
|
||||
[54, ParserStackType.OSC, 0]
|
||||
[54, ParserStackType.OSC, 0],
|
||||
[62, ParserStackType.APC, 0],
|
||||
]);
|
||||
clearAccu();
|
||||
// after dispose we should be back to RESULT
|
||||
@@ -2016,7 +2334,8 @@ describe('EscapeSequenceParser - async', () => {
|
||||
[20, ParserStackType.ESC, 0],
|
||||
[27, ParserStackType.CSI, 0],
|
||||
[41, ParserStackType.DCS, 0],
|
||||
[54, ParserStackType.OSC, 0]
|
||||
[54, ParserStackType.OSC, 0],
|
||||
[62, ParserStackType.APC, 0],
|
||||
]);
|
||||
clearAccu();
|
||||
|
||||
@@ -2033,7 +2352,8 @@ describe('EscapeSequenceParser - async', () => {
|
||||
[20, ParserStackType.ESC, 1],
|
||||
[27, ParserStackType.CSI, 0],
|
||||
[41, ParserStackType.DCS, 0],
|
||||
[54, ParserStackType.OSC, 0]
|
||||
[54, ParserStackType.OSC, 0],
|
||||
[62, ParserStackType.APC, 0],
|
||||
]);
|
||||
clearAccu();
|
||||
// after dispose we should be back to RESULT
|
||||
@@ -2046,7 +2366,8 @@ describe('EscapeSequenceParser - async', () => {
|
||||
[20, ParserStackType.ESC, 0],
|
||||
[27, ParserStackType.CSI, 0],
|
||||
[41, ParserStackType.DCS, 0],
|
||||
[54, ParserStackType.OSC, 0]
|
||||
[54, ParserStackType.OSC, 0],
|
||||
[62, ParserStackType.APC, 0],
|
||||
]);
|
||||
});
|
||||
it('sync/async SGR mixed', async () => {
|
||||
@@ -2071,7 +2392,8 @@ describe('EscapeSequenceParser - async', () => {
|
||||
[27, ParserStackType.CSI, 2],
|
||||
[27, ParserStackType.CSI, 0],
|
||||
[41, ParserStackType.DCS, 0],
|
||||
[54, ParserStackType.OSC, 0]
|
||||
[54, ParserStackType.OSC, 0],
|
||||
[62, ParserStackType.APC, 0],
|
||||
]);
|
||||
clearAccu();
|
||||
// dispose SGR2 (sync one)
|
||||
@@ -2092,7 +2414,8 @@ describe('EscapeSequenceParser - async', () => {
|
||||
[27, ParserStackType.CSI, 1],
|
||||
[27, ParserStackType.CSI, 0],
|
||||
[41, ParserStackType.DCS, 0],
|
||||
[54, ParserStackType.OSC, 0]
|
||||
[54, ParserStackType.OSC, 0],
|
||||
[62, ParserStackType.APC, 0],
|
||||
]);
|
||||
clearAccu();
|
||||
// dispose SGR3 (async one)
|
||||
@@ -2105,7 +2428,8 @@ describe('EscapeSequenceParser - async', () => {
|
||||
[20, ParserStackType.ESC, 0],
|
||||
[27, ParserStackType.CSI, 0],
|
||||
[41, ParserStackType.DCS, 0],
|
||||
[54, ParserStackType.OSC, 0]
|
||||
[54, ParserStackType.OSC, 0],
|
||||
[62, ParserStackType.APC, 0],
|
||||
]);
|
||||
});
|
||||
it('multiple async OSC handlers', async () => {
|
||||
@@ -2123,7 +2447,8 @@ describe('EscapeSequenceParser - async', () => {
|
||||
[27, ParserStackType.CSI, 0],
|
||||
[41, ParserStackType.DCS, 0],
|
||||
[54, ParserStackType.OSC, 0],
|
||||
[54, ParserStackType.OSC, 0]
|
||||
[54, ParserStackType.OSC, 0],
|
||||
[62, ParserStackType.APC, 0],
|
||||
]);
|
||||
clearAccu();
|
||||
// after dispose we should be back to RESULT
|
||||
@@ -2136,7 +2461,8 @@ describe('EscapeSequenceParser - async', () => {
|
||||
[20, ParserStackType.ESC, 0],
|
||||
[27, ParserStackType.CSI, 0],
|
||||
[41, ParserStackType.DCS, 0],
|
||||
[54, ParserStackType.OSC, 0]
|
||||
[54, ParserStackType.OSC, 0],
|
||||
[62, ParserStackType.APC, 0],
|
||||
]);
|
||||
clearAccu();
|
||||
|
||||
@@ -2153,7 +2479,8 @@ describe('EscapeSequenceParser - async', () => {
|
||||
[20, ParserStackType.ESC, 0],
|
||||
[27, ParserStackType.CSI, 0],
|
||||
[41, ParserStackType.DCS, 0],
|
||||
[54, ParserStackType.OSC, 0]
|
||||
[54, ParserStackType.OSC, 0],
|
||||
[62, ParserStackType.APC, 0],
|
||||
]);
|
||||
clearAccu();
|
||||
// after dispose we should be back to RESULT
|
||||
@@ -2166,7 +2493,8 @@ describe('EscapeSequenceParser - async', () => {
|
||||
[20, ParserStackType.ESC, 0],
|
||||
[27, ParserStackType.CSI, 0],
|
||||
[41, ParserStackType.DCS, 0],
|
||||
[54, ParserStackType.OSC, 0]
|
||||
[54, ParserStackType.OSC, 0],
|
||||
[62, ParserStackType.APC, 0],
|
||||
]);
|
||||
clearAccu();
|
||||
});
|
||||
@@ -2185,7 +2513,8 @@ describe('EscapeSequenceParser - async', () => {
|
||||
[27, ParserStackType.CSI, 0],
|
||||
[41, ParserStackType.DCS, 0],
|
||||
[41, ParserStackType.DCS, 0],
|
||||
[54, ParserStackType.OSC, 0]
|
||||
[54, ParserStackType.OSC, 0],
|
||||
[62, ParserStackType.APC, 0],
|
||||
]);
|
||||
clearAccu();
|
||||
// after dispose we should be back to RESULT
|
||||
@@ -2198,7 +2527,8 @@ describe('EscapeSequenceParser - async', () => {
|
||||
[20, ParserStackType.ESC, 0],
|
||||
[27, ParserStackType.CSI, 0],
|
||||
[41, ParserStackType.DCS, 0],
|
||||
[54, ParserStackType.OSC, 0]
|
||||
[54, ParserStackType.OSC, 0],
|
||||
[62, ParserStackType.APC, 0],
|
||||
]);
|
||||
clearAccu();
|
||||
|
||||
@@ -2215,7 +2545,8 @@ describe('EscapeSequenceParser - async', () => {
|
||||
[20, ParserStackType.ESC, 0],
|
||||
[27, ParserStackType.CSI, 0],
|
||||
[41, ParserStackType.DCS, 0],
|
||||
[54, ParserStackType.OSC, 0]
|
||||
[54, ParserStackType.OSC, 0],
|
||||
[62, ParserStackType.APC, 0],
|
||||
]);
|
||||
clearAccu();
|
||||
// after dispose we should be back to RESULT
|
||||
@@ -2228,7 +2559,74 @@ describe('EscapeSequenceParser - async', () => {
|
||||
[20, ParserStackType.ESC, 0],
|
||||
[27, ParserStackType.CSI, 0],
|
||||
[41, ParserStackType.DCS, 0],
|
||||
[54, ParserStackType.OSC, 0]
|
||||
[54, ParserStackType.OSC, 0],
|
||||
[62, ParserStackType.APC, 0],
|
||||
]);
|
||||
clearAccu();
|
||||
});
|
||||
it('multiple async APC handlers', async () => {
|
||||
// register with fallback
|
||||
const APC2 = parser.registerApcHandler({ final: 'X' }, new ApcHandler(async data => { callstack.push(['#2 APC X', data]); return false; }));
|
||||
await parseP(parser, INPUT);
|
||||
for (let i = 0; i < callstack.length; ++i) {
|
||||
const entry = callstack[i];
|
||||
if (entry[0] === '2# APC X') assert.equal(callstack[i + 1][0], 'APC a', 'Should fallback to original handler');
|
||||
}
|
||||
evalStackSaves(parser.trackedStack, [
|
||||
[6, ParserStackType.CSI, 0],
|
||||
[15, ParserStackType.ESC, 0],
|
||||
[20, ParserStackType.ESC, 0],
|
||||
[27, ParserStackType.CSI, 0],
|
||||
[41, ParserStackType.DCS, 0],
|
||||
[54, ParserStackType.OSC, 0],
|
||||
[62, ParserStackType.APC, 0],
|
||||
[62, ParserStackType.APC, 0],
|
||||
]);
|
||||
clearAccu();
|
||||
// after dispose we should be back to RESULT
|
||||
APC2.dispose();
|
||||
await parseP(parser, INPUT);
|
||||
assert.deepEqual(callstack, RESULT, 'Should not call custom handler');
|
||||
evalStackSaves(parser.trackedStack, [
|
||||
[6, ParserStackType.CSI, 0],
|
||||
[15, ParserStackType.ESC, 0],
|
||||
[20, ParserStackType.ESC, 0],
|
||||
[27, ParserStackType.CSI, 0],
|
||||
[41, ParserStackType.DCS, 0],
|
||||
[54, ParserStackType.OSC, 0],
|
||||
[62, ParserStackType.APC, 0],
|
||||
]);
|
||||
clearAccu();
|
||||
|
||||
// register without fallback
|
||||
const APC22 = parser.registerApcHandler({ final: 'X' }, new ApcHandler(async data => { callstack.push(['#2 APC X', data]); return true; }));
|
||||
await parseP(parser, INPUT);
|
||||
for (let i = 0; i < callstack.length; ++i) {
|
||||
const entry = callstack[i];
|
||||
if (entry[0] === '2# APC X') assert.notEqual(callstack[i + 1][0], 'APC X', 'Should fallback to original handler');
|
||||
}
|
||||
evalStackSaves(parser.trackedStack, [
|
||||
[6, ParserStackType.CSI, 0],
|
||||
[15, ParserStackType.ESC, 0],
|
||||
[20, ParserStackType.ESC, 0],
|
||||
[27, ParserStackType.CSI, 0],
|
||||
[41, ParserStackType.DCS, 0],
|
||||
[54, ParserStackType.OSC, 0],
|
||||
[62, ParserStackType.APC, 0],
|
||||
]);
|
||||
clearAccu();
|
||||
// after dispose we should be back to RESULT
|
||||
APC22.dispose();
|
||||
await parseP(parser, INPUT);
|
||||
assert.deepEqual(callstack, RESULT, 'Should not call custom handler');
|
||||
evalStackSaves(parser.trackedStack, [
|
||||
[6, ParserStackType.CSI, 0],
|
||||
[15, ParserStackType.ESC, 0],
|
||||
[20, ParserStackType.ESC, 0],
|
||||
[27, ParserStackType.CSI, 0],
|
||||
[41, ParserStackType.DCS, 0],
|
||||
[54, ParserStackType.OSC, 0],
|
||||
[62, ParserStackType.APC, 0],
|
||||
]);
|
||||
clearAccu();
|
||||
});
|
||||
|
||||
@@ -12,6 +12,24 @@ import { OscParser } from 'common/parser/OscParser';
|
||||
import { DcsParser } from 'common/parser/DcsParser';
|
||||
import { ApcParser } from 'common/parser/ApcParser';
|
||||
|
||||
/**
|
||||
* VT commands done by the parser
|
||||
*/
|
||||
// @vt: #Y ESC CSI "Control Sequence Introducer" "ESC [" "Start of a CSI sequence."
|
||||
// @vt: #Y ESC OSC "Operating System Command" "ESC ]" "Start of an OSC sequence."
|
||||
// @vt: #Y ESC DCS "Device Control String" "ESC P" "Start of a DCS sequence."
|
||||
// @vt: #Y ESC ST "String Terminator" "ESC \" "Terminator used for string type sequences."
|
||||
// @vt: #Y ESC PM "Privacy Message" "ESC ^" "Start of a privacy message."
|
||||
// @vt: #Y ESC APC "Application Program Command" "ESC _" "Start of an APC sequence."
|
||||
// @vt: #Y C1 CSI "Control Sequence Introducer" "\x9B" "Start of a CSI sequence."
|
||||
// @vt: #Y C1 OSC "Operating System Command" "\x9D" "Start of an OSC sequence."
|
||||
// @vt: #Y C1 DCS "Device Control String" "\x90" "Start of a DCS sequence."
|
||||
// @vt: #Y C1 ST "String Terminator" "\x9C" "Terminator used for string type sequences."
|
||||
// @vt: #Y C1 PM "Privacy Message" "\x9E" "Start of a privacy message."
|
||||
// @vt: #Y C1 APC "Application Program Command" "\x9F" "Start of an APC sequence."
|
||||
// @vt: #Y C0 NUL "Null" "\0, \x00" "NUL is ignored."
|
||||
// @vt: #Y C0 ESC "Escape" "\e, \x1B" "Start of a sequence. Cancels any other sequence."
|
||||
|
||||
/**
|
||||
* Table values are generated like this:
|
||||
* index: currentState << TableValue.INDEX_STATE_SHIFT | charCode
|
||||
@@ -77,7 +95,9 @@ const NON_ASCII_PRINTABLE = 0xA0;
|
||||
* Taken from https://vt100.net/emu/dec_ansi_parser.
|
||||
*/
|
||||
export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
|
||||
const table: TransitionTable = new TransitionTable(4095);
|
||||
// table size:
|
||||
// (ParserState.STATE_LENGTH - 1) << TableAccess.INDEX_STATE_SHIFT | NON_ASCII_PRINTABLE + 1
|
||||
const table: TransitionTable = new TransitionTable(4257);
|
||||
|
||||
// range macro for byte
|
||||
const BYTE_VALUES = 256;
|
||||
@@ -105,7 +125,7 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
|
||||
table.add(0x1b, state, ParserAction.CLEAR, ParserState.ESCAPE); // ESC
|
||||
table.add(0x9d, state, ParserAction.OSC_START, ParserState.OSC_STRING); // OSC
|
||||
table.addMany([0x98, 0x9e], state, ParserAction.IGNORE, ParserState.SOS_PM_STRING); // SOS, PM
|
||||
table.add(0x9f, state, ParserAction.APC_START, ParserState.APC_STRING); // APC
|
||||
table.add(0x9f, state, ParserAction.CLEAR, ParserState.APC_ENTRY); // APC
|
||||
table.add(0x9b, state, ParserAction.CLEAR, ParserState.CSI_ENTRY); // CSI
|
||||
table.add(0x90, state, ParserAction.CLEAR, ParserState.DCS_ENTRY); // DCS
|
||||
}
|
||||
@@ -136,11 +156,20 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
|
||||
table.add(0x9c, ParserState.SOS_PM_STRING, ParserAction.IGNORE, ParserState.GROUND);
|
||||
table.add(0x7f, ParserState.SOS_PM_STRING, ParserAction.IGNORE, ParserState.SOS_PM_STRING);
|
||||
// apc
|
||||
table.add(0x5f, ParserState.ESCAPE, ParserAction.APC_START, ParserState.APC_STRING);
|
||||
table.addMany(PRINTABLES, ParserState.APC_STRING, ParserAction.APC_PUT, ParserState.APC_STRING);
|
||||
table.addMany(EXECUTABLES, ParserState.APC_STRING, ParserAction.IGNORE, ParserState.APC_STRING);
|
||||
table.add(0x7f, ParserState.APC_STRING, ParserAction.IGNORE, ParserState.APC_STRING);
|
||||
table.addMany([0x1b, 0x9c, 0x18, 0x1a], ParserState.APC_STRING, ParserAction.APC_END, ParserState.GROUND);
|
||||
table.add(0x5f, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.APC_ENTRY);
|
||||
table.addMany(EXECUTABLES, ParserState.APC_ENTRY, ParserAction.IGNORE, ParserState.APC_ENTRY);
|
||||
table.add(0x7f, ParserState.APC_ENTRY, ParserAction.IGNORE, ParserState.APC_ENTRY);
|
||||
table.addMany(r(0x20, 0x30), ParserState.APC_ENTRY, ParserAction.COLLECT, ParserState.APC_INTERMEDIATE);
|
||||
table.addMany(r(0x30, 0x7f), ParserState.APC_ENTRY, ParserAction.APC_START, ParserState.APC_PASSTHROUGH);
|
||||
table.addMany(r(0x30, 0x7f), ParserState.APC_INTERMEDIATE, ParserAction.APC_START, ParserState.APC_PASSTHROUGH);
|
||||
table.addMany(EXECUTABLES, ParserState.APC_INTERMEDIATE, ParserAction.IGNORE, ParserState.APC_INTERMEDIATE);
|
||||
table.addMany(r(0x20, 0x30), ParserState.APC_INTERMEDIATE, ParserAction.COLLECT, ParserState.APC_INTERMEDIATE);
|
||||
table.add(0x7f, ParserState.APC_INTERMEDIATE, ParserAction.IGNORE, ParserState.APC_INTERMEDIATE);
|
||||
table.addMany(PRINTABLES, ParserState.APC_PASSTHROUGH, ParserAction.APC_PUT, ParserState.APC_PASSTHROUGH);
|
||||
table.addMany(EXECUTABLES, ParserState.APC_PASSTHROUGH, ParserAction.IGNORE, ParserState.APC_PASSTHROUGH);
|
||||
table.addMany(r(0x08, 0x0e), ParserState.APC_PASSTHROUGH, ParserAction.APC_PUT, ParserState.APC_PASSTHROUGH);
|
||||
table.add(0x7f, ParserState.APC_PASSTHROUGH, ParserAction.IGNORE, ParserState.APC_PASSTHROUGH);
|
||||
table.addMany([0x1b, 0x9c, 0x18, 0x1a], ParserState.APC_PASSTHROUGH, ParserAction.APC_END, ParserState.GROUND);
|
||||
// csi entries
|
||||
table.add(0x5b, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.CSI_ENTRY);
|
||||
table.addMany(r(0x40, 0x7f), ParserState.CSI_ENTRY, ParserAction.CSI_DISPATCH, ParserState.GROUND);
|
||||
@@ -169,22 +198,18 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
|
||||
table.add(0x50, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.DCS_ENTRY);
|
||||
table.addMany(EXECUTABLES, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY);
|
||||
table.add(0x7f, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY);
|
||||
table.addMany(r(0x1c, 0x20), ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY);
|
||||
table.addMany(r(0x20, 0x30), ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE);
|
||||
table.addMany(r(0x30, 0x3c), ParserState.DCS_ENTRY, ParserAction.PARAM, ParserState.DCS_PARAM);
|
||||
table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_PARAM);
|
||||
table.addMany(EXECUTABLES, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);
|
||||
table.addMany(r(0x20, 0x80), ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);
|
||||
table.addMany(r(0x1c, 0x20), ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);
|
||||
table.addMany(EXECUTABLES, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM);
|
||||
table.add(0x7f, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM);
|
||||
table.addMany(r(0x1c, 0x20), ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM);
|
||||
table.addMany(r(0x30, 0x3c), ParserState.DCS_PARAM, ParserAction.PARAM, ParserState.DCS_PARAM);
|
||||
table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_IGNORE);
|
||||
table.addMany(r(0x20, 0x30), ParserState.DCS_PARAM, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE);
|
||||
table.addMany(EXECUTABLES, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE);
|
||||
table.add(0x7f, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE);
|
||||
table.addMany(r(0x1c, 0x20), ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE);
|
||||
table.addMany(r(0x20, 0x30), ParserState.DCS_INTERMEDIATE, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE);
|
||||
table.addMany(r(0x30, 0x40), ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_IGNORE);
|
||||
table.addMany(r(0x40, 0x7f), ParserState.DCS_INTERMEDIATE, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH);
|
||||
@@ -200,7 +225,7 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
|
||||
table.add(NON_ASCII_PRINTABLE, ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE);
|
||||
table.add(NON_ASCII_PRINTABLE, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);
|
||||
table.add(NON_ASCII_PRINTABLE, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH);
|
||||
table.add(NON_ASCII_PRINTABLE, ParserState.APC_STRING, ParserAction.APC_PUT, ParserState.APC_STRING);
|
||||
table.add(NON_ASCII_PRINTABLE, ParserState.APC_PASSTHROUGH, ParserAction.APC_PUT, ParserState.APC_PASSTHROUGH);
|
||||
return table;
|
||||
})();
|
||||
|
||||
@@ -439,11 +464,13 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
this._oscParser.setHandlerFallback(handler);
|
||||
}
|
||||
|
||||
public registerApcHandler(ident: number, handler: IApcHandler): IDisposable {
|
||||
return this._apcParser.registerHandler(ident, handler);
|
||||
public registerApcHandler(id: IFunctionIdentifier, handler: IApcHandler): IDisposable {
|
||||
id.prefix = undefined; // APC does not support prefix byte
|
||||
return this._apcParser.registerHandler(this._identifier(id, [0x30, 0x7e]), handler);
|
||||
}
|
||||
public clearApcHandler(ident: number): void {
|
||||
this._apcParser.clearHandler(ident);
|
||||
public clearApcHandler(id: IFunctionIdentifier): void {
|
||||
id.prefix = undefined; // APC does not support prefix byte
|
||||
this._apcParser.clearHandler(this._identifier(id, [0x30, 0x7e]));
|
||||
}
|
||||
public setApcHandlerFallback(handler: ApcFallbackHandlerType): void {
|
||||
this._apcParser.setHandlerFallback(handler);
|
||||
@@ -719,7 +746,10 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
}
|
||||
|
||||
// normal transition & action lookup
|
||||
transition = this._transitions.table[this.currentState << TableAccess.INDEX_STATE_SHIFT | (code < 0xa0 ? code : NON_ASCII_PRINTABLE)];
|
||||
transition = this._transitions.table[
|
||||
this.currentState << TableAccess.INDEX_STATE_SHIFT |
|
||||
(code < NON_ASCII_PRINTABLE ? code : NON_ASCII_PRINTABLE)
|
||||
];
|
||||
switch (transition >> TableAccess.TRANSITION_ACTION_SHIFT) {
|
||||
case ParserAction.PRINT:
|
||||
// Note: 0x20 (SP) is included, 0x7F (DEL) is excluded
|
||||
@@ -872,16 +902,18 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
this.precedingJoinState = 0;
|
||||
break;
|
||||
case ParserAction.APC_START:
|
||||
this._apcParser.start();
|
||||
this._apcParser.start(this._collect << 8 | code);
|
||||
break;
|
||||
case ParserAction.APC_PUT:
|
||||
// inner loop - exit APC_PUT: 0x18, 0x1a, 0x1b, 0x9c
|
||||
// allowed: 00/08 .. 00/13, 02/00 .. 07/14 + NON_ASCII_PRINTABLE
|
||||
for (let j = i + 1; ; ++j) {
|
||||
if (j >= length || (code = data[j]) === 0x18 || code === 0x1a || code === 0x1b || code === 0x9c || (code > 0x7f && code < NON_ASCII_PRINTABLE)) {
|
||||
this._apcParser.put(data, i, j);
|
||||
i = j - 1;
|
||||
break;
|
||||
}
|
||||
if (j < length && (
|
||||
(data[j] >= 0x20 && data[j] < 0x7f) || (data[j] >= 0x08 && data[j] < 0x0e) || data[j] >= NON_ASCII_PRINTABLE
|
||||
)) continue;
|
||||
this._apcParser.put(data, i, j);
|
||||
i = j - 1;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case ParserAction.APC_END:
|
||||
|
||||
@@ -220,8 +220,8 @@ export interface IEscapeSequenceParser extends IDisposable {
|
||||
clearOscHandler(ident: number): void;
|
||||
setOscHandlerFallback(handler: OscFallbackHandlerType): void;
|
||||
|
||||
registerApcHandler(ident: number, handler: IApcHandler): IDisposable;
|
||||
clearApcHandler(ident: number): void;
|
||||
registerApcHandler(id: IFunctionIdentifier, handler: IApcHandler): IDisposable;
|
||||
clearApcHandler(id: IFunctionIdentifier): void;
|
||||
setApcHandlerFallback(handler: ApcFallbackHandlerType): void;
|
||||
|
||||
setErrorHandler(handler: (state: IParsingState) => IParsingState): void;
|
||||
@@ -252,7 +252,7 @@ export interface IDcsParser extends ISubParser<IDcsHandler, DcsFallbackHandlerTy
|
||||
}
|
||||
|
||||
export interface IApcParser extends ISubParser<IApcHandler, ApcFallbackHandlerType> {
|
||||
start(): void;
|
||||
start(ident: number): void;
|
||||
end(success: boolean, promiseResult?: boolean): void | Promise<boolean>;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ export class ParserApi implements IParser {
|
||||
public addOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
|
||||
return this.registerOscHandler(ident, callback);
|
||||
}
|
||||
public registerApcHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
|
||||
return this._core.registerApcHandler(ident, callback);
|
||||
public registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
|
||||
return this._core.registerApcHandler(id, callback);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@ import { perfContext, before, beforeEach, ThroughputRuntimeCase } from 'xterm-be
|
||||
|
||||
import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser';
|
||||
import { C0, C1 } from 'common/data/EscapeSequences';
|
||||
import { IDcsHandler, IOscHandler, IParams } from 'common/parser/Types';
|
||||
import { IDcsHandler, IOscHandler, IApcHandler, IParams } from 'common/parser/Types';
|
||||
import { OscHandler } from 'common/parser/OscParser';
|
||||
import { DcsHandler } from '../../out/common/parser/DcsParser';
|
||||
import { DcsHandler } from 'common/parser/DcsParser';
|
||||
import { ApcHandler } from 'common/parser/ApcParser';
|
||||
|
||||
const SIZE = 5000000;
|
||||
|
||||
@@ -32,6 +33,12 @@ class FastOscHandler implements IOscHandler {
|
||||
public end(success: boolean): boolean { return true; }
|
||||
}
|
||||
|
||||
class FastApcHandler implements IApcHandler {
|
||||
public start(): void {}
|
||||
public put(data: Uint32Array, start: number, end: number): void {}
|
||||
public end(success: boolean): boolean { return true; }
|
||||
}
|
||||
|
||||
|
||||
perfContext('Parser throughput - 50MB data', () => {
|
||||
let parsed: Uint32Array;
|
||||
@@ -108,6 +115,8 @@ perfContext('Parser throughput - 50MB data', () => {
|
||||
parser.registerEscHandler({ intermediates: '%', final: 'G' }, () => true);
|
||||
parser.registerDcsHandler({ final: 'p' }, new DcsHandler(data => true));
|
||||
parser.registerDcsHandler({ final: 'q' }, new FastDcsHandler());
|
||||
parser.registerApcHandler({ final: 'X' }, new ApcHandler(data => true));
|
||||
parser.registerApcHandler({ final: 'Y' }, new FastApcHandler());
|
||||
});
|
||||
|
||||
perfContext('PRINT - a', () => {
|
||||
@@ -349,4 +358,64 @@ perfContext('Parser throughput - 50MB data', () => {
|
||||
return { payloadSize: parsed.length };
|
||||
}, { fork: true }).showAverageThroughput();
|
||||
});
|
||||
|
||||
perfContext('APC string interface (short seq)', () => {
|
||||
before(() => {
|
||||
const data = '\x1b_Xhi\x1b\\\x1b_Xhi\x1b\\\x1b_Xhi\x1b\\\x1b_Xhi\x1b\\\x1b_Xhi\x1b\\';
|
||||
let content = '';
|
||||
while (content.length < SIZE) {
|
||||
content += data;
|
||||
}
|
||||
parsed = toUtf32(content);
|
||||
});
|
||||
new ThroughputRuntimeCase('', async () => {
|
||||
parser.parse(parsed, parsed.length);
|
||||
return { payloadSize: parsed.length };
|
||||
}, { fork: true }).showAverageThroughput();
|
||||
});
|
||||
|
||||
perfContext('APC string interface (long seq)', () => {
|
||||
before(() => {
|
||||
const data = '\x1b_XLorem ipsum dolor sit amet, consetetur sadipscing elitr.\x1b\\';
|
||||
let content = '';
|
||||
while (content.length < SIZE) {
|
||||
content += data;
|
||||
}
|
||||
parsed = toUtf32(content);
|
||||
});
|
||||
new ThroughputRuntimeCase('', async () => {
|
||||
parser.parse(parsed, parsed.length);
|
||||
return { payloadSize: parsed.length };
|
||||
}, { fork: true }).showAverageThroughput();
|
||||
});
|
||||
|
||||
perfContext('APC class interface (short seq)', () => {
|
||||
before(() => {
|
||||
const data = '\x1b_Yhi\x1b\\\x1b_Yhi\x1b\\\x1b_Yhi\x1b\\\x1b_Yhi\x1b\\\x1b_Yhi\x1b\\';
|
||||
let content = '';
|
||||
while (content.length < SIZE) {
|
||||
content += data;
|
||||
}
|
||||
parsed = toUtf32(content);
|
||||
});
|
||||
new ThroughputRuntimeCase('', async () => {
|
||||
parser.parse(parsed, parsed.length);
|
||||
return { payloadSize: parsed.length };
|
||||
}, { fork: true }).showAverageThroughput();
|
||||
});
|
||||
|
||||
perfContext('APC class interface (long seq)', () => {
|
||||
before(() => {
|
||||
const data = '\x1b_YLorem ipsum dolor sit amet, consetetur sadipscing elitr.\x1b\\';
|
||||
let content = '';
|
||||
while (content.length < SIZE) {
|
||||
content += data;
|
||||
}
|
||||
parsed = toUtf32(content);
|
||||
});
|
||||
new ThroughputRuntimeCase('', async () => {
|
||||
parser.parse(parsed, parsed.length);
|
||||
return { payloadSize: parsed.length };
|
||||
}, { fork: true }).showAverageThroughput();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -248,7 +248,7 @@ test.describe('Parser Integration Tests', () => {
|
||||
await ctx.proxy.evaluate(([term]) => {
|
||||
window.customApcHandlerCallStack = [];
|
||||
// APC uses first character as identifier (e.g., 0x41 = 'A')
|
||||
window.disposable = term.parser.registerApcHandler(0x41, data => {
|
||||
window.disposable = term.parser.registerApcHandler({ final: 'A' }, data => {
|
||||
window.customApcHandlerCallStack!.push(['handler', data]);
|
||||
return true;
|
||||
});
|
||||
@@ -263,7 +263,7 @@ test.describe('Parser Integration Tests', () => {
|
||||
test('should handle short data', async () => {
|
||||
await ctx.proxy.evaluate(([term]) => {
|
||||
window.customApcHandlerCallStack = [];
|
||||
window.disposable = term.parser.registerApcHandler(0x42, data => {
|
||||
window.disposable = term.parser.registerApcHandler({ final: 'B' }, data => {
|
||||
window.customApcHandlerCallStack!.push(['handler', data]);
|
||||
return true;
|
||||
});
|
||||
@@ -278,15 +278,15 @@ test.describe('Parser Integration Tests', () => {
|
||||
await ctx.proxy.evaluate(([term]) => {
|
||||
window.customApcHandlerCallStack = [];
|
||||
window.disposables = [
|
||||
term.parser.registerApcHandler(0x43, data => {
|
||||
term.parser.registerApcHandler({ final: 'C' }, data => {
|
||||
window.customApcHandlerCallStack!.push(['A', data]);
|
||||
return false;
|
||||
}),
|
||||
term.parser.registerApcHandler(0x43, data => {
|
||||
term.parser.registerApcHandler({ final: 'C' }, data => {
|
||||
window.customApcHandlerCallStack!.push(['B', data]);
|
||||
return true;
|
||||
}),
|
||||
term.parser.registerApcHandler(0x43, data => {
|
||||
term.parser.registerApcHandler({ final: 'C' }, data => {
|
||||
window.customApcHandlerCallStack!.push(['C', data]);
|
||||
return false;
|
||||
})
|
||||
@@ -302,17 +302,17 @@ test.describe('Parser Integration Tests', () => {
|
||||
await ctx.proxy.evaluate(([term]) => {
|
||||
window.customApcHandlerCallStack = [];
|
||||
window.disposables = [
|
||||
term.parser.registerApcHandler(0x44, data => {
|
||||
term.parser.registerApcHandler({ final: 'D' }, data => {
|
||||
window.customApcHandlerCallStack!.push(['A', data]);
|
||||
return false;
|
||||
}),
|
||||
term.parser.registerApcHandler(0x44, data => {
|
||||
term.parser.registerApcHandler({ final: 'D' }, data => {
|
||||
return new Promise(res => setTimeout(res, 50)).then(() => {
|
||||
window.customApcHandlerCallStack!.push(['B', data]);
|
||||
return false;
|
||||
});
|
||||
}),
|
||||
term.parser.registerApcHandler(0x44, data => {
|
||||
term.parser.registerApcHandler({ final: 'D' }, data => {
|
||||
window.customApcHandlerCallStack!.push(['C', data]);
|
||||
return false;
|
||||
})
|
||||
@@ -330,11 +330,11 @@ test.describe('Parser Integration Tests', () => {
|
||||
await ctx.proxy.evaluate(([term]) => {
|
||||
window.customApcHandlerCallStack = [];
|
||||
window.disposables = [
|
||||
term.parser.registerApcHandler(0x46, data => { // 'F'
|
||||
term.parser.registerApcHandler({ final: 'F' }, data => {
|
||||
window.customApcHandlerCallStack!.push(['F', data]);
|
||||
return true;
|
||||
}),
|
||||
term.parser.registerApcHandler(0x58, data => { // 'X'
|
||||
term.parser.registerApcHandler({ final: 'X' }, data => {
|
||||
window.customApcHandlerCallStack!.push(['X', data]);
|
||||
return true;
|
||||
})
|
||||
|
||||
Vendored
+5
-5
@@ -1274,12 +1274,12 @@ declare module '@xterm/headless' {
|
||||
prefix?: string;
|
||||
/**
|
||||
* Optional intermediate bytes, must be in range \x20 .. \x2f.
|
||||
* Usable in CSI, DCS and ESC.
|
||||
* Usable in CSI, DCS, ESC and APC.
|
||||
*/
|
||||
intermediates?: string;
|
||||
/**
|
||||
* Final byte, must be in range \x40 .. \x7e for CSI and DCS,
|
||||
* \x30 .. \x7e for ESC.
|
||||
* \x30 .. \x7e for ESC and APC.
|
||||
*/
|
||||
final: string;
|
||||
}
|
||||
@@ -1354,8 +1354,8 @@ declare module '@xterm/headless' {
|
||||
|
||||
/**
|
||||
* Adds a handler for APC escape sequences.
|
||||
* @param ident The identifier (first character) of the sequence as a
|
||||
* character code, e.g. 71 for 'G' (Kitty graphics protocol).
|
||||
* @param id Specifies the function identifier under which the callback
|
||||
* gets registered, e.g. {final: 'G'} for Kitty graphics protocol.
|
||||
* @param callback The function to handle the sequence. Note that the
|
||||
* function will only be called once if the sequence finished successfully.
|
||||
* There is currently no way to intercept smaller data chunks, data chunks
|
||||
@@ -1368,7 +1368,7 @@ declare module '@xterm/headless' {
|
||||
* added handler is tried first.
|
||||
* @returns An IDisposable you can call to remove this handler.
|
||||
*/
|
||||
registerApcHandler(ident: number, callback: (data: string) => boolean): IDisposable;
|
||||
registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean): IDisposable;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Vendored
+5
-5
@@ -1909,12 +1909,12 @@ declare module '@xterm/xterm' {
|
||||
prefix?: string;
|
||||
/**
|
||||
* Optional intermediate bytes, must be in range \x20 .. \x2f.
|
||||
* Usable in CSI, DCS and ESC.
|
||||
* Usable in CSI, DCS, ESC and APC.
|
||||
*/
|
||||
intermediates?: string;
|
||||
/**
|
||||
* Final byte, must be in range \x40 .. \x7e for CSI and DCS,
|
||||
* \x30 .. \x7e for ESC.
|
||||
* \x30 .. \x7e for ESC and APC.
|
||||
*/
|
||||
final: string;
|
||||
}
|
||||
@@ -2002,8 +2002,8 @@ declare module '@xterm/xterm' {
|
||||
|
||||
/**
|
||||
* Adds a handler for APC escape sequences.
|
||||
* @param ident The identifier (first character) of the sequence as a
|
||||
* character code, e.g. 71 for 'G' (Kitty graphics protocol).
|
||||
* @param id Specifies the function identifier under which the callback
|
||||
* gets registered, e.g. {final: 'G'} for Kitty graphics protocol.
|
||||
* @param callback The function to handle the sequence. Note that the
|
||||
* function will only be called once if the sequence finished successfully.
|
||||
* There is currently no way to intercept smaller data chunks, data chunks
|
||||
@@ -2016,7 +2016,7 @@ declare module '@xterm/xterm' {
|
||||
* handler. The most recently added handler is tried first.
|
||||
* @returns An IDisposable you can call to remove this handler.
|
||||
*/
|
||||
registerApcHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
|
||||
registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user