Move common primitives to src/common/primitives/
Phase 1 of #5963: reorganize building blocks (buffer, parser, input, data, and shared utilities) under common/primitives/. Runtime code (services, public API, CoreTerminal, InputHandler) stays in place. Update import paths across browser, headless, addons, and tests. Add TypeScript path mappings so common/* aliases resolve primitives first, then the common root. Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
This commit is contained in:
co-authored by
Daniel Imms
parent
3cdbc77386
commit
701c0affd5
@@ -0,0 +1,487 @@
|
||||
/**
|
||||
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
import { assert } from 'chai';
|
||||
import { ApcParser, ApcHandler } from './ApcParser';
|
||||
import { StringToUtf32, utf32ToString } from '../input/TextDecoder';
|
||||
import { IApcHandler, IFunctionIdentifier } from './Types';
|
||||
|
||||
function toUtf32(s: string): Uint32Array {
|
||||
const utf32 = new Uint32Array(s.length);
|
||||
const decoder = new StringToUtf32();
|
||||
const length = decoder.decode(s, utf32);
|
||||
return utf32.subarray(0, length);
|
||||
}
|
||||
|
||||
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 > 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, 'START']);
|
||||
}
|
||||
public put(data: Uint32Array, start: number, end: number): void {
|
||||
this.output.push([this.msg, 'PUT', utf32ToString(data, start, end)]);
|
||||
}
|
||||
public end(success: boolean): boolean {
|
||||
this.output.push([this.msg, 'END', success]);
|
||||
if (this.returnFalse) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
describe('ApcParser', () => {
|
||||
let parser: ApcParser;
|
||||
let reports: any[] = [];
|
||||
beforeEach(() => {
|
||||
reports = [];
|
||||
parser = new ApcParser();
|
||||
parser.setHandlerFallback((id, action, data) => reports.push([id, action, data]));
|
||||
});
|
||||
describe('handler registration', () => {
|
||||
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(reports, [
|
||||
// messages from TestHandler
|
||||
['th', 'START'],
|
||||
['th', 'PUT', 'Here comes'],
|
||||
['th', 'PUT', 'the mouse!'],
|
||||
['th', 'END', true]
|
||||
]);
|
||||
});
|
||||
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(reports, [
|
||||
// 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('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(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('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(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 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(reports, [
|
||||
['th1', 'START'],
|
||||
['th1', 'PUT', 'Here comes'],
|
||||
['th1', 'PUT', 'the mouse!'],
|
||||
['th1', 'END', true]
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('ApcHandlerFactory', () => {
|
||||
const TEST_PAYLOAD_LIMIT = 100;
|
||||
const CHUNK_SIZE = 10;
|
||||
let originalPayloadLimit: number;
|
||||
|
||||
beforeEach(() => {
|
||||
const handlerConstructor = ApcHandler as unknown as { _payloadLimit: number };
|
||||
originalPayloadLimit = handlerConstructor._payloadLimit;
|
||||
handlerConstructor._payloadLimit = TEST_PAYLOAD_LIMIT;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
const handlerConstructor = ApcHandler as unknown as { _payloadLimit: number };
|
||||
handlerConstructor._payloadLimit = originalPayloadLimit;
|
||||
});
|
||||
|
||||
it('should be called once on end(true)', () => {
|
||||
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(true);
|
||||
assert.deepEqual(reports, ['Here comes the mouse!']);
|
||||
});
|
||||
it('should not be called on end(false)', () => {
|
||||
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(reports, []);
|
||||
});
|
||||
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(' 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(reports, ['A'.repeat(TEST_PAYLOAD_LIMIT)]);
|
||||
});
|
||||
it('should abort for payload limit +1', 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'}));
|
||||
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(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: any[] = [];
|
||||
beforeEach(() => {
|
||||
reports = [];
|
||||
parser = new ApcParser();
|
||||
parser.setHandlerFallback((id, action, data) => reports.push([id, action, data]));
|
||||
});
|
||||
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!']]);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('reset', () => {
|
||||
it('should abort active handlers with end(false) when reset during payload', () => {
|
||||
const ident = identifier({ intermediates: '+', final: 'p' });
|
||||
parser.registerHandler(ident, new TestHandler(reports, 'th'));
|
||||
parser.start(ident);
|
||||
let data = toUtf32('partial');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.reset();
|
||||
assert.deepEqual(reports, [
|
||||
['th', 'START'],
|
||||
['th', 'PUT', 'partial'],
|
||||
['th', 'END', false]
|
||||
]);
|
||||
reports.length = 0;
|
||||
parser.start(ident);
|
||||
data = toUtf32('complete');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [
|
||||
['th', 'START'],
|
||||
['th', 'PUT', 'complete'],
|
||||
['th', 'END', true]
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IApcHandler, IHandlerCollection, ApcFallbackHandlerType, IApcParser, ISubParserStackState } from './Types';
|
||||
import { ParserConstants } from './Constants';
|
||||
import { utf32ToString } from '../input/TextDecoder';
|
||||
import { IDisposable } from '../../Types';
|
||||
import { LimitedStringBuilder } from '../StringBuilder';
|
||||
|
||||
const EMPTY_HANDLERS: IApcHandler[] = [];
|
||||
|
||||
/**
|
||||
* APC Parser for handling Application Program Command sequences.
|
||||
* APC sequences use the format: ESC _ <identifier><data> ESC \
|
||||
*
|
||||
* Unlike OSC which uses numeric identifiers (e.g., OSC 1337),
|
||||
* APC uses the first character as the identifier (e.g., 'G' for Kitty graphics).
|
||||
* The identifier is the character code of the first byte after ESC _.
|
||||
*/
|
||||
export class ApcParser implements IApcParser {
|
||||
private _handlers: IHandlerCollection<IApcHandler> = Object.create(null);
|
||||
private _active = EMPTY_HANDLERS;
|
||||
private _ident: number = 0;
|
||||
private _handlerFb: ApcFallbackHandlerType = () => { };
|
||||
private _stack: ISubParserStackState = {
|
||||
paused: false,
|
||||
loopPosition: 0,
|
||||
fallThrough: false
|
||||
};
|
||||
|
||||
/**
|
||||
* Register an APC handler for a specific identifier.
|
||||
* @param ident The character code of the first byte (e.g., 0x47 for 'G')
|
||||
* @param handler The handler to register
|
||||
*/
|
||||
public registerHandler(ident: number, handler: IApcHandler): IDisposable {
|
||||
this._handlers[ident] ??= [];
|
||||
const handlerList = this._handlers[ident];
|
||||
handlerList.push(handler);
|
||||
return {
|
||||
dispose: () => {
|
||||
const handlerIndex = handlerList.indexOf(handler);
|
||||
if (handlerIndex !== -1) {
|
||||
handlerList.splice(handlerIndex, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public clearHandler(ident: number): void {
|
||||
if (this._handlers[ident]) delete this._handlers[ident];
|
||||
}
|
||||
|
||||
public setHandlerFallback(handler: ApcFallbackHandlerType): void {
|
||||
this._handlerFb = handler;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._handlers = Object.create(null);
|
||||
this._handlerFb = () => { };
|
||||
this._active = EMPTY_HANDLERS;
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
// 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._ident = 0;
|
||||
}
|
||||
|
||||
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._ident, 'START');
|
||||
} else {
|
||||
for (let j = this._active.length - 1; j >= 0; j--) {
|
||||
this._active[j].start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public put(data: Uint32Array, start: number, end: number): void {
|
||||
if (!this._active.length) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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._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 (!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 (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._ident = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient class to allow attaching string based handler functions
|
||||
* as APC handlers.
|
||||
*/
|
||||
export class ApcHandler implements IApcHandler {
|
||||
private static _payloadLimit = ParserConstants.PAYLOAD_LIMIT;
|
||||
|
||||
private _data = new LimitedStringBuilder(ApcHandler._payloadLimit);
|
||||
private _hitLimit: boolean = false;
|
||||
|
||||
constructor(private _handler: (data: string) => boolean | Promise<boolean>) { }
|
||||
|
||||
public start(): void {
|
||||
this._data.reset();
|
||||
this._hitLimit = false;
|
||||
}
|
||||
|
||||
public put(data: Uint32Array, start: number, end: number): void {
|
||||
if (this._hitLimit) {
|
||||
return;
|
||||
}
|
||||
if (this._data.append(utf32ToString(data, start, end))) {
|
||||
this._hitLimit = true;
|
||||
}
|
||||
}
|
||||
|
||||
public end(success: boolean): boolean | Promise<boolean> {
|
||||
let ret: boolean | Promise<boolean> = false;
|
||||
if (this._hitLimit) {
|
||||
ret = false;
|
||||
} else if (success) {
|
||||
ret = this._handler(this._data.toString());
|
||||
if (ret instanceof Promise) {
|
||||
// need to hold data until `ret` got resolved
|
||||
// dont care for errors, data will be freed anyway on next start
|
||||
return ret.then(res => {
|
||||
this._data.reset();
|
||||
this._hitLimit = false;
|
||||
return res;
|
||||
});
|
||||
}
|
||||
}
|
||||
this._data.reset();
|
||||
this._hitLimit = false;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* Internal states of EscapeSequenceParser.
|
||||
*/
|
||||
export const enum ParserState {
|
||||
GROUND = 0,
|
||||
ESCAPE = 1,
|
||||
ESCAPE_INTERMEDIATE = 2,
|
||||
CSI_ENTRY = 3,
|
||||
CSI_PARAM = 4,
|
||||
CSI_INTERMEDIATE = 5,
|
||||
CSI_IGNORE = 6,
|
||||
SOS_PM_STRING = 7,
|
||||
OSC_STRING = 8,
|
||||
DCS_ENTRY = 9,
|
||||
DCS_PARAM = 10,
|
||||
DCS_IGNORE = 11,
|
||||
DCS_INTERMEDIATE = 12,
|
||||
DCS_PASSTHROUGH = 13,
|
||||
APC_ENTRY = 14,
|
||||
APC_INTERMEDIATE = 15,
|
||||
APC_PASSTHROUGH = 16,
|
||||
// Number of states, meaning LAST_STATE + 1.
|
||||
STATE_LENGTH = 17
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal actions of EscapeSequenceParser.
|
||||
*/
|
||||
export const enum ParserAction {
|
||||
IGNORE = 0,
|
||||
ERROR = 1,
|
||||
PRINT = 2,
|
||||
EXECUTE = 3,
|
||||
OSC_START = 4,
|
||||
OSC_PUT = 5,
|
||||
OSC_END = 6,
|
||||
CSI_DISPATCH = 7,
|
||||
PARAM = 8,
|
||||
COLLECT = 9,
|
||||
ESC_DISPATCH = 10,
|
||||
CLEAR = 11,
|
||||
DCS_HOOK = 12,
|
||||
DCS_PUT = 13,
|
||||
DCS_UNHOOK = 14,
|
||||
APC_START = 15,
|
||||
APC_PUT = 16,
|
||||
APC_END = 17
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal states of OscParser.
|
||||
*/
|
||||
export const enum OscState {
|
||||
START = 0,
|
||||
ID = 1,
|
||||
PAYLOAD = 2,
|
||||
ABORT = 3
|
||||
}
|
||||
|
||||
// payload limit for OSC and DCS
|
||||
export const enum ParserConstants {
|
||||
PAYLOAD_LIMIT = 10000000
|
||||
}
|
||||
@@ -0,0 +1,499 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
import { assert } from 'chai';
|
||||
import { DcsParser, DcsHandler } from './DcsParser';
|
||||
import { IDcsHandler, IParams, IFunctionIdentifier } from './Types';
|
||||
import { utf32ToString, StringToUtf32 } from '../input/TextDecoder';
|
||||
import { Params } from './Params';
|
||||
|
||||
function toUtf32(s: string): Uint32Array {
|
||||
const utf32 = new Uint32Array(s.length);
|
||||
const decoder = new StringToUtf32();
|
||||
const length = decoder.decode(s, utf32);
|
||||
return utf32.subarray(0, length);
|
||||
}
|
||||
|
||||
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 > 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 IDcsHandler {
|
||||
constructor(public output: any[], public msg: string, public returnFalse: boolean = false) {}
|
||||
public hook(params: IParams): void {
|
||||
this.output.push([this.msg, 'HOOK', params.toArray()]);
|
||||
}
|
||||
public put(data: Uint32Array, start: number, end: number): void {
|
||||
this.output.push([this.msg, 'PUT', utf32ToString(data, start, end)]);
|
||||
}
|
||||
public unhook(success: boolean): boolean {
|
||||
this.output.push([this.msg, 'UNHOOK', success]);
|
||||
if (this.returnFalse) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
describe('DcsParser', () => {
|
||||
let parser: DcsParser;
|
||||
let reports: any[] = [];
|
||||
beforeEach(() => {
|
||||
reports = [];
|
||||
parser = new DcsParser();
|
||||
parser.setHandlerFallback((id, action, data) => {
|
||||
if (action === 'HOOK') {
|
||||
data = data.toArray();
|
||||
}
|
||||
reports.push([id, action, data]);
|
||||
});
|
||||
});
|
||||
describe('handler registration', () => {
|
||||
it('setDcsHandler', () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th'));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.unhook(true);
|
||||
assert.deepEqual(reports, [
|
||||
// messages from TestHandler
|
||||
['th', 'HOOK', [1, 2, 3]],
|
||||
['th', 'PUT', 'Here comes'],
|
||||
['th', 'PUT', 'the mouse!'],
|
||||
['th', 'UNHOOK', true]
|
||||
]);
|
||||
});
|
||||
it('clearDcsHandler', () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th'));
|
||||
parser.clearHandler(identifier({intermediates: '+', final: 'p'}));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.unhook(true);
|
||||
assert.deepEqual(reports, [
|
||||
// messages from fallback handler
|
||||
[identifier({intermediates: '+', final: 'p'}), 'HOOK', [1, 2, 3]],
|
||||
[identifier({intermediates: '+', final: 'p'}), 'PUT', 'Here comes'],
|
||||
[identifier({intermediates: '+', final: 'p'}), 'PUT', 'the mouse!'],
|
||||
[identifier({intermediates: '+', final: 'p'}), 'UNHOOK', true]
|
||||
]);
|
||||
});
|
||||
it('addDcsHandler', () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1'));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2'));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.unhook(true);
|
||||
assert.deepEqual(reports, [
|
||||
['th2', 'HOOK', [1, 2, 3]],
|
||||
['th1', 'HOOK', [1, 2, 3]],
|
||||
['th2', 'PUT', 'Here comes'],
|
||||
['th1', 'PUT', 'Here comes'],
|
||||
['th2', 'PUT', 'the mouse!'],
|
||||
['th1', 'PUT', 'the mouse!'],
|
||||
['th2', 'UNHOOK', true],
|
||||
['th1', 'UNHOOK', false] // false due being already handled by th2!
|
||||
]);
|
||||
});
|
||||
it('addDcsHandler 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.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.unhook(true);
|
||||
assert.deepEqual(reports, [
|
||||
['th2', 'HOOK', [1, 2, 3]],
|
||||
['th1', 'HOOK', [1, 2, 3]],
|
||||
['th2', 'PUT', 'Here comes'],
|
||||
['th1', 'PUT', 'Here comes'],
|
||||
['th2', 'PUT', 'the mouse!'],
|
||||
['th1', 'PUT', 'the mouse!'],
|
||||
['th2', 'UNHOOK', true],
|
||||
['th1', 'UNHOOK', true] // true since th2 indicated to keep bubbling
|
||||
]);
|
||||
});
|
||||
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.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.unhook(true);
|
||||
assert.deepEqual(reports, [
|
||||
['th1', 'HOOK', [1, 2, 3]],
|
||||
['th1', 'PUT', 'Here comes'],
|
||||
['th1', 'PUT', 'the mouse!'],
|
||||
['th1', 'UNHOOK', true]
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('DcsHandlerFactory', () => {
|
||||
const TEST_PAYLOAD_LIMIT = 100;
|
||||
const CHUNK_SIZE = 10;
|
||||
let originalPayloadLimit: number;
|
||||
|
||||
beforeEach(() => {
|
||||
const handlerConstructor = DcsHandler as unknown as { _payloadLimit: number };
|
||||
originalPayloadLimit = handlerConstructor._payloadLimit;
|
||||
handlerConstructor._payloadLimit = TEST_PAYLOAD_LIMIT;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
const handlerConstructor = DcsHandler as unknown as { _payloadLimit: number };
|
||||
handlerConstructor._payloadLimit = originalPayloadLimit;
|
||||
});
|
||||
|
||||
it('should be called once on end(true)', () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push([params.toArray(), data]); return true; }));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.unhook(true);
|
||||
assert.deepEqual(reports, [[[1, 2, 3], 'Here comes the mouse!']]);
|
||||
});
|
||||
it('should not be called on end(false)', () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push([params.toArray(), data]); return true; }));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.unhook(false);
|
||||
assert.deepEqual(reports, []);
|
||||
});
|
||||
it('should be disposable', () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push(['one', params.toArray(), data]); return true; }));
|
||||
const dispo = parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push(['two', params.toArray(), data]); return true; }));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.unhook(true);
|
||||
assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!']]);
|
||||
dispo.dispose();
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
data = toUtf32('some other');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' data');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.unhook(true);
|
||||
assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'some other data']]);
|
||||
});
|
||||
it('should respect return false', () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push(['one', params.toArray(), data]); return true; }));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push(['two', params.toArray(), data]); return false; }));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.unhook(true);
|
||||
assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'Here comes the mouse!']]);
|
||||
});
|
||||
it('should work up to payload limit', function(): void {
|
||||
this.timeout(30000);
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push([params.toArray(), data]); return true; }));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
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.unhook(true);
|
||||
assert.deepEqual(reports, [[[1, 2, 3], 'A'.repeat(TEST_PAYLOAD_LIMIT)]]);
|
||||
});
|
||||
it('should abort for payload limit +1', function(): void {
|
||||
this.timeout(30000);
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push([params.toArray(), data]); return true; }));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
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.unhook(true);
|
||||
assert.deepEqual(reports, []);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
class TestHandlerAsync implements IDcsHandler {
|
||||
constructor(public output: any[], public msg: string, public returnFalse: boolean = false) {}
|
||||
public hook(params: IParams): void {
|
||||
this.output.push([this.msg, 'HOOK', params.toArray()]);
|
||||
}
|
||||
public put(data: Uint32Array, start: number, end: number): void {
|
||||
this.output.push([this.msg, 'PUT', utf32ToString(data, start, end)]);
|
||||
}
|
||||
public async unhook(success: boolean): Promise<boolean> {
|
||||
// simple sleep to check in tests whether ordering gets messed up
|
||||
await Promise.resolve();
|
||||
this.output.push([this.msg, 'UNHOOK', success]);
|
||||
if (this.returnFalse) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
async function unhookP(parser: DcsParser, success: boolean): Promise<void> {
|
||||
let result: void | Promise<boolean>;
|
||||
let prev: boolean | undefined;
|
||||
while (result = parser.unhook(success, prev)) {
|
||||
prev = await result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
describe('DcsParser - async tests', () => {
|
||||
let parser: DcsParser;
|
||||
let reports: any[] = [];
|
||||
beforeEach(() => {
|
||||
reports = [];
|
||||
parser = new DcsParser();
|
||||
parser.setHandlerFallback((id, action, data) => {
|
||||
if (action === 'HOOK') {
|
||||
data = data.toArray();
|
||||
}
|
||||
reports.push([id, action, data]);
|
||||
});
|
||||
});
|
||||
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.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
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', 'HOOK', [1, 2, 3]],
|
||||
['a1', 'HOOK', [1, 2, 3]],
|
||||
['s1', 'HOOK', [1, 2, 3]],
|
||||
['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', 'UNHOOK', true],
|
||||
['a1', 'UNHOOK', false], // important: a1 before s1
|
||||
['s1', 'UNHOOK', 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.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
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', 'HOOK', [1, 2, 3]],
|
||||
['a1', 'HOOK', [1, 2, 3]],
|
||||
['s1', 'HOOK', [1, 2, 3]],
|
||||
['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', 'UNHOOK', true],
|
||||
['a1', 'UNHOOK', true], // important: a1 before s1
|
||||
['s1', 'UNHOOK', 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.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
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', 'HOOK', [1, 2, 3]],
|
||||
['s1', 'HOOK', [1, 2, 3]],
|
||||
['a1', 'HOOK', [1, 2, 3]],
|
||||
['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', 'UNHOOK', true],
|
||||
['s1', 'UNHOOK', false], // important: s1 between a2 .. a1
|
||||
['a1', 'UNHOOK', 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.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
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', 'HOOK', [1, 2, 3]],
|
||||
['s1', 'HOOK', [1, 2, 3]],
|
||||
['a1', 'HOOK', [1, 2, 3]],
|
||||
['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', 'UNHOOK', true],
|
||||
['s1', 'UNHOOK', true], // important: s1 between a2 .. a1
|
||||
['a1', 'UNHOOK', true]
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('DcsHandlerFactory', () => {
|
||||
it('should be called once on end(true)', async () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push([params.toArray(), data]); return true; }));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
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, [[[1, 2, 3], 'Here comes the mouse!']]);
|
||||
});
|
||||
it('should not be called on end(false)', async () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push([params.toArray(), data]); return true; }));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
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 DcsHandler(async (data, params) => { reports.push(['one', params.toArray(), data]); return true; }));
|
||||
const dispo = parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['two', params.toArray(), data]); return true; }));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
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', [1, 2, 3], 'Here comes the mouse!']]);
|
||||
dispo.dispose();
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
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', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'some other data']]);
|
||||
});
|
||||
it('should respect return false', async () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['one', params.toArray(), data]); return true; }));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['two', params.toArray(), data]); return false; }));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
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', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'Here comes the mouse!']]);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('reset', () => {
|
||||
it('should abort active handlers with unhook(false) when reset during payload', () => {
|
||||
const ident = identifier({ intermediates: '+', final: 'p' });
|
||||
const params = Params.fromArray([1, 2, 3]);
|
||||
parser.registerHandler(ident, new TestHandler(reports, 'th'));
|
||||
parser.hook(ident, params);
|
||||
let data = toUtf32('partial');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.reset();
|
||||
assert.deepEqual(reports, [
|
||||
['th', 'HOOK', [1, 2, 3]],
|
||||
['th', 'PUT', 'partial'],
|
||||
['th', 'UNHOOK', false]
|
||||
]);
|
||||
reports.length = 0;
|
||||
parser.hook(ident, params);
|
||||
data = toUtf32('complete');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.unhook(true);
|
||||
assert.deepEqual(reports, [
|
||||
['th', 'HOOK', [1, 2, 3]],
|
||||
['th', 'PUT', 'complete'],
|
||||
['th', 'UNHOOK', true]
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable } from '../../Types';
|
||||
import { IDcsHandler, IParams, IHandlerCollection, IDcsParser, DcsFallbackHandlerType, ISubParserStackState } from './Types';
|
||||
import { utf32ToString } from '../input/TextDecoder';
|
||||
import { Params } from './Params';
|
||||
import { ParserConstants } from './Constants';
|
||||
import { LimitedStringBuilder } from '../StringBuilder';
|
||||
|
||||
const EMPTY_HANDLERS: IDcsHandler[] = [];
|
||||
|
||||
export class DcsParser implements IDcsParser {
|
||||
private _handlers: IHandlerCollection<IDcsHandler> = Object.create(null);
|
||||
private _active: IDcsHandler[] = EMPTY_HANDLERS;
|
||||
private _ident: number = 0;
|
||||
private _handlerFb: DcsFallbackHandlerType = () => { };
|
||||
private _stack: ISubParserStackState = {
|
||||
paused: false,
|
||||
loopPosition: 0,
|
||||
fallThrough: false
|
||||
};
|
||||
|
||||
public dispose(): void {
|
||||
this._handlers = Object.create(null);
|
||||
this._handlerFb = () => { };
|
||||
this._active = EMPTY_HANDLERS;
|
||||
}
|
||||
|
||||
public registerHandler(ident: number, handler: IDcsHandler): IDisposable {
|
||||
this._handlers[ident] ??= [];
|
||||
const handlerList = this._handlers[ident];
|
||||
handlerList.push(handler);
|
||||
return {
|
||||
dispose: () => {
|
||||
const handlerIndex = handlerList.indexOf(handler);
|
||||
if (handlerIndex !== -1) {
|
||||
handlerList.splice(handlerIndex, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public clearHandler(ident: number): void {
|
||||
if (this._handlers[ident]) delete this._handlers[ident];
|
||||
}
|
||||
|
||||
public setHandlerFallback(handler: DcsFallbackHandlerType): void {
|
||||
this._handlerFb = handler;
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
// force cleanup leftover 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].unhook(false);
|
||||
}
|
||||
}
|
||||
this._stack.paused = false;
|
||||
this._active = EMPTY_HANDLERS;
|
||||
this._ident = 0;
|
||||
}
|
||||
|
||||
public hook(ident: number, params: IParams): void {
|
||||
// always reset leftover handlers
|
||||
this.reset();
|
||||
this._ident = ident;
|
||||
this._active = this._handlers[ident] || EMPTY_HANDLERS;
|
||||
if (!this._active.length) {
|
||||
this._handlerFb(this._ident, 'HOOK', params);
|
||||
} else {
|
||||
for (let j = this._active.length - 1; j >= 0; j--) {
|
||||
this._active[j].hook(params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public put(data: Uint32Array, start: number, end: number): void {
|
||||
if (!this._active.length) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public unhook(success: boolean, promiseResult: boolean = true): void | Promise<boolean> {
|
||||
if (!this._active.length) {
|
||||
this._handlerFb(this._ident, 'UNHOOK', 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].unhook(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 (fallThrough for async)
|
||||
for (; j >= 0; j--) {
|
||||
handlerResult = this._active[j].unhook(false);
|
||||
if (handlerResult instanceof Promise) {
|
||||
this._stack.paused = true;
|
||||
this._stack.loopPosition = j;
|
||||
this._stack.fallThrough = true;
|
||||
return handlerResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
this._active = EMPTY_HANDLERS;
|
||||
this._ident = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// predefine empty params as [0] (ZDM)
|
||||
const EMPTY_PARAMS = new Params();
|
||||
EMPTY_PARAMS.addParam(0);
|
||||
|
||||
/**
|
||||
* Convenient class to create a DCS handler from a single callback function.
|
||||
* Note: The payload is currently limited to 50 MB (hardcoded).
|
||||
*/
|
||||
export class DcsHandler implements IDcsHandler {
|
||||
private static _payloadLimit = ParserConstants.PAYLOAD_LIMIT;
|
||||
|
||||
private _data = new LimitedStringBuilder(DcsHandler._payloadLimit);
|
||||
private _params: IParams = EMPTY_PARAMS;
|
||||
private _hitLimit: boolean = false;
|
||||
|
||||
constructor(private _handler: (data: string, params: IParams) => boolean | Promise<boolean>) { }
|
||||
|
||||
public hook(params: IParams): void {
|
||||
// since we need to preserve params until `unhook`, we have to clone it
|
||||
// (only borrowed from parser and spans multiple parser states)
|
||||
// perf optimization:
|
||||
// clone only, if we have non empty params, otherwise stick with default
|
||||
this._params = (params.length > 1 || params.params[0]) ? params.clone() : EMPTY_PARAMS;
|
||||
this._data.reset();
|
||||
this._hitLimit = false;
|
||||
}
|
||||
|
||||
public put(data: Uint32Array, start: number, end: number): void {
|
||||
if (this._hitLimit) {
|
||||
return;
|
||||
}
|
||||
if (this._data.append(utf32ToString(data, start, end))) {
|
||||
this._hitLimit = true;
|
||||
}
|
||||
}
|
||||
|
||||
public unhook(success: boolean): boolean | Promise<boolean> {
|
||||
let ret: boolean | Promise<boolean> = false;
|
||||
if (this._hitLimit) {
|
||||
ret = false;
|
||||
} else if (success) {
|
||||
ret = this._handler(this._data.toString(), this._params);
|
||||
if (ret instanceof Promise) {
|
||||
// need to hold data and params until `ret` got resolved
|
||||
// dont care for errors, data will be freed anyway on next start
|
||||
return ret.then(res => {
|
||||
this._params = EMPTY_PARAMS;
|
||||
this._data.reset();
|
||||
this._hitLimit = false;
|
||||
return res;
|
||||
});
|
||||
}
|
||||
}
|
||||
this._params = EMPTY_PARAMS;
|
||||
this._data.reset();
|
||||
this._hitLimit = false;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,934 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandlerType, OscFallbackHandlerType, IOscParser, EscHandlerType, IDcsParser, DcsFallbackHandlerType, IFunctionIdentifier, ExecuteFallbackHandlerType, CsiFallbackHandlerType, EscFallbackHandlerType, PrintHandlerType, PrintFallbackHandlerType, ExecuteHandlerType, IParserStackState, ParserStackType, ResumableHandlersType, IApcHandler, IApcParser, ApcFallbackHandlerType } from './Types';
|
||||
import { ParserState, ParserAction } from './Constants';
|
||||
import { Disposable, toDisposable } from '../Lifecycle';
|
||||
import { IDisposable } from '../../Types';
|
||||
import { Params } from './Params';
|
||||
import { OscParser } from './OscParser';
|
||||
import { DcsParser } from './DcsParser';
|
||||
import { ApcParser } from './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
|
||||
* value: action << TableValue.TRANSITION_ACTION_SHIFT | nextState
|
||||
*/
|
||||
const enum TableAccess {
|
||||
TRANSITION_ACTION_SHIFT = 8,
|
||||
TRANSITION_STATE_MASK = 255,
|
||||
INDEX_STATE_SHIFT = 8
|
||||
}
|
||||
|
||||
/**
|
||||
* Transition table for EscapeSequenceParser.
|
||||
*/
|
||||
export class TransitionTable {
|
||||
public table: Uint16Array;
|
||||
|
||||
constructor(length: number) {
|
||||
this.table = new Uint16Array(length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default transition.
|
||||
* @param action default action
|
||||
* @param next default next state
|
||||
*/
|
||||
public setDefault(action: ParserAction, next: ParserState): void {
|
||||
this.table.fill(action << TableAccess.TRANSITION_ACTION_SHIFT | next);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a transition to the transition table.
|
||||
* @param code input character code
|
||||
* @param state current parser state
|
||||
* @param action parser action to be done
|
||||
* @param next next parser state
|
||||
*/
|
||||
public add(code: number, state: ParserState, action: ParserAction, next: ParserState): void {
|
||||
this.table[state << TableAccess.INDEX_STATE_SHIFT | code] = action << TableAccess.TRANSITION_ACTION_SHIFT | next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add transitions for multiple input character codes.
|
||||
* @param codes input character code array
|
||||
* @param state current parser state
|
||||
* @param action parser action to be done
|
||||
* @param next next parser state
|
||||
*/
|
||||
public addMany(codes: number[], state: ParserState, action: ParserAction, next: ParserState): void {
|
||||
for (let i = 0; i < codes.length; i++) {
|
||||
this.table[state << TableAccess.INDEX_STATE_SHIFT | codes[i]] = action << TableAccess.TRANSITION_ACTION_SHIFT | next;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Pseudo-character placeholder for printable non-ascii characters (unicode).
|
||||
const NON_ASCII_PRINTABLE = 0xA0;
|
||||
|
||||
|
||||
/**
|
||||
* VT500 compatible transition table.
|
||||
* Taken from https://vt100.net/emu/dec_ansi_parser.
|
||||
*/
|
||||
export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
|
||||
// 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;
|
||||
const blueprint = Array.apply(null, Array(BYTE_VALUES)).map((unused: any, i: number) => i);
|
||||
const r = (start: number, end: number): number[] => blueprint.slice(start, end);
|
||||
|
||||
// Default definitions.
|
||||
const PRINTABLES = r(0x20, 0x7f); // 0x20 (SP) included, 0x7F (DEL) excluded
|
||||
const EXECUTABLES = r(0x00, 0x18);
|
||||
EXECUTABLES.push(0x19);
|
||||
EXECUTABLES.push.apply(EXECUTABLES, r(0x1c, 0x20));
|
||||
|
||||
const states: number[] = r(ParserState.GROUND, ParserState.STATE_LENGTH);
|
||||
|
||||
// set default transition
|
||||
table.setDefault(ParserAction.ERROR, ParserState.GROUND);
|
||||
// printables
|
||||
table.addMany(PRINTABLES, ParserState.GROUND, ParserAction.PRINT, ParserState.GROUND);
|
||||
// global anywhere rules
|
||||
for (const state of states) {
|
||||
table.addMany([0x18, 0x1a, 0x99, 0x9a], state, ParserAction.EXECUTE, ParserState.GROUND);
|
||||
table.addMany(r(0x80, 0x90), state, ParserAction.EXECUTE, ParserState.GROUND);
|
||||
table.addMany(r(0x90, 0x98), state, ParserAction.EXECUTE, ParserState.GROUND);
|
||||
table.add(0x9c, state, ParserAction.IGNORE, ParserState.GROUND); // ST as terminator
|
||||
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.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
|
||||
}
|
||||
// rules for executables and 7f
|
||||
table.addMany(EXECUTABLES, ParserState.GROUND, ParserAction.EXECUTE, ParserState.GROUND);
|
||||
table.addMany(EXECUTABLES, ParserState.ESCAPE, ParserAction.EXECUTE, ParserState.ESCAPE);
|
||||
table.add(0x7f, ParserState.ESCAPE, ParserAction.IGNORE, ParserState.ESCAPE);
|
||||
table.addMany(EXECUTABLES, ParserState.OSC_STRING, ParserAction.IGNORE, ParserState.OSC_STRING);
|
||||
table.addMany(EXECUTABLES, ParserState.CSI_ENTRY, ParserAction.EXECUTE, ParserState.CSI_ENTRY);
|
||||
table.add(0x7f, ParserState.CSI_ENTRY, ParserAction.IGNORE, ParserState.CSI_ENTRY);
|
||||
table.addMany(EXECUTABLES, ParserState.CSI_PARAM, ParserAction.EXECUTE, ParserState.CSI_PARAM);
|
||||
table.add(0x7f, ParserState.CSI_PARAM, ParserAction.IGNORE, ParserState.CSI_PARAM);
|
||||
table.addMany(EXECUTABLES, ParserState.CSI_IGNORE, ParserAction.EXECUTE, ParserState.CSI_IGNORE);
|
||||
table.addMany(EXECUTABLES, ParserState.CSI_INTERMEDIATE, ParserAction.EXECUTE, ParserState.CSI_INTERMEDIATE);
|
||||
table.add(0x7f, ParserState.CSI_INTERMEDIATE, ParserAction.IGNORE, ParserState.CSI_INTERMEDIATE);
|
||||
table.addMany(EXECUTABLES, ParserState.ESCAPE_INTERMEDIATE, ParserAction.EXECUTE, ParserState.ESCAPE_INTERMEDIATE);
|
||||
table.add(0x7f, ParserState.ESCAPE_INTERMEDIATE, ParserAction.IGNORE, ParserState.ESCAPE_INTERMEDIATE);
|
||||
// osc
|
||||
table.add(0x5d, ParserState.ESCAPE, ParserAction.OSC_START, ParserState.OSC_STRING);
|
||||
table.addMany(PRINTABLES, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING);
|
||||
table.add(0x7f, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING);
|
||||
table.addMany([0x9c, 0x1b, 0x18, 0x1a, 0x07], ParserState.OSC_STRING, ParserAction.OSC_END, ParserState.GROUND);
|
||||
table.addMany(r(0x1c, 0x20), ParserState.OSC_STRING, ParserAction.IGNORE, ParserState.OSC_STRING);
|
||||
// sos/pm
|
||||
table.addMany([0x58, 0x5e], ParserState.ESCAPE, ParserAction.IGNORE, ParserState.SOS_PM_STRING);
|
||||
table.addMany(PRINTABLES, ParserState.SOS_PM_STRING, ParserAction.IGNORE, ParserState.SOS_PM_STRING);
|
||||
table.addMany(EXECUTABLES, ParserState.SOS_PM_STRING, ParserAction.IGNORE, ParserState.SOS_PM_STRING);
|
||||
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.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);
|
||||
table.addMany(r(0x30, 0x3c), ParserState.CSI_ENTRY, ParserAction.PARAM, ParserState.CSI_PARAM);
|
||||
table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.CSI_ENTRY, ParserAction.COLLECT, ParserState.CSI_PARAM);
|
||||
table.addMany(r(0x30, 0x3c), ParserState.CSI_PARAM, ParserAction.PARAM, ParserState.CSI_PARAM);
|
||||
table.addMany(r(0x40, 0x7f), ParserState.CSI_PARAM, ParserAction.CSI_DISPATCH, ParserState.GROUND);
|
||||
table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.CSI_PARAM, ParserAction.IGNORE, ParserState.CSI_IGNORE);
|
||||
table.addMany(r(0x20, 0x40), ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE);
|
||||
table.add(0x7f, ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE);
|
||||
table.addMany(r(0x40, 0x7f), ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.GROUND);
|
||||
table.addMany(r(0x20, 0x30), ParserState.CSI_ENTRY, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE);
|
||||
table.addMany(r(0x20, 0x30), ParserState.CSI_INTERMEDIATE, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE);
|
||||
table.addMany(r(0x30, 0x40), ParserState.CSI_INTERMEDIATE, ParserAction.IGNORE, ParserState.CSI_IGNORE);
|
||||
table.addMany(r(0x40, 0x7f), ParserState.CSI_INTERMEDIATE, ParserAction.CSI_DISPATCH, ParserState.GROUND);
|
||||
table.addMany(r(0x20, 0x30), ParserState.CSI_PARAM, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE);
|
||||
// esc_intermediate
|
||||
table.addMany(r(0x20, 0x30), ParserState.ESCAPE, ParserAction.COLLECT, ParserState.ESCAPE_INTERMEDIATE);
|
||||
table.addMany(r(0x20, 0x30), ParserState.ESCAPE_INTERMEDIATE, ParserAction.COLLECT, ParserState.ESCAPE_INTERMEDIATE);
|
||||
table.addMany(r(0x30, 0x7f), ParserState.ESCAPE_INTERMEDIATE, ParserAction.ESC_DISPATCH, ParserState.GROUND);
|
||||
table.addMany(r(0x30, 0x50), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);
|
||||
table.addMany(r(0x51, 0x58), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);
|
||||
table.addMany([0x59, 0x5a, 0x5c], ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);
|
||||
table.addMany(r(0x60, 0x7f), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);
|
||||
// dcs entry
|
||||
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(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(EXECUTABLES, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM);
|
||||
table.add(0x7f, 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(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);
|
||||
table.addMany(r(0x40, 0x7f), ParserState.DCS_PARAM, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH);
|
||||
table.addMany(r(0x40, 0x7f), ParserState.DCS_ENTRY, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH);
|
||||
table.addMany(EXECUTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH);
|
||||
table.addMany(PRINTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH);
|
||||
table.add(0x7f, ParserState.DCS_PASSTHROUGH, ParserAction.IGNORE, ParserState.DCS_PASSTHROUGH);
|
||||
table.addMany([0x1b, 0x9c, 0x18, 0x1a], ParserState.DCS_PASSTHROUGH, ParserAction.DCS_UNHOOK, ParserState.GROUND);
|
||||
// special handling of unicode chars
|
||||
table.add(NON_ASCII_PRINTABLE, ParserState.GROUND, ParserAction.PRINT, ParserState.GROUND);
|
||||
table.add(NON_ASCII_PRINTABLE, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING);
|
||||
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_PASSTHROUGH, ParserAction.APC_PUT, ParserState.APC_PASSTHROUGH);
|
||||
return table;
|
||||
})();
|
||||
|
||||
|
||||
/**
|
||||
* EscapeSequenceParser.
|
||||
* This class implements the ANSI/DEC compatible parser described by
|
||||
* Paul Williams (https://vt100.net/emu/dec_ansi_parser).
|
||||
*
|
||||
* To implement custom ANSI compliant escape sequences it is not needed to
|
||||
* alter this parser, instead consider registering a custom handler.
|
||||
* For non ANSI compliant sequences change the transition table with
|
||||
* the optional `transitions` constructor argument and
|
||||
* reimplement the `parse` method.
|
||||
*
|
||||
* This parser is currently hardcoded to operate in ZDM (Zero Default Mode)
|
||||
* as suggested by the original parser, thus empty parameters are set to 0.
|
||||
* This is not in line with the latest ECMA-48 specification
|
||||
* (ZDM was part of the early specs and got completely removed later on).
|
||||
*
|
||||
* Other than the original parser from vt100.net this parser supports
|
||||
* sub parameters in digital parameters separated by colons. Empty sub parameters
|
||||
* are set to -1 (no ZDM for sub parameters).
|
||||
*
|
||||
* About prefix and intermediate bytes:
|
||||
* This parser follows the assumptions of the vt100.net parser with these restrictions:
|
||||
* - only one prefix byte is allowed as first parameter byte, byte range 0x3c .. 0x3f
|
||||
* - max. two intermediates are respected, byte range 0x20 .. 0x2f
|
||||
* Note that this is not in line with ECMA-48 which does not limit either of those.
|
||||
* Furthermore ECMA-48 allows the prefix byte range at any param byte position. Currently
|
||||
* there are no known sequences that follow the broader definition of the specification.
|
||||
*
|
||||
* TODO: implement error recovery hook via error handler return values
|
||||
*/
|
||||
export class EscapeSequenceParser extends Disposable implements IEscapeSequenceParser {
|
||||
public initialState: number;
|
||||
public currentState: number;
|
||||
public precedingJoinState: number; // UnicodeJoinProperties
|
||||
|
||||
// buffers over several parse calls
|
||||
protected _params: Params;
|
||||
protected _collect: number;
|
||||
|
||||
// handler lookup containers
|
||||
protected _printHandler: PrintHandlerType;
|
||||
protected _executeHandlers: { [flag: number]: ExecuteHandlerType };
|
||||
// fast path for EXE bytes < 0x18
|
||||
protected _executeHandlersArr: (ExecuteHandlerType | undefined)[];
|
||||
protected _csiHandlers: IHandlerCollection<CsiHandlerType>;
|
||||
protected _escHandlers: IHandlerCollection<EscHandlerType>;
|
||||
protected readonly _oscParser: IOscParser;
|
||||
protected readonly _dcsParser: IDcsParser;
|
||||
protected readonly _apcParser: IApcParser;
|
||||
protected _errorHandler: (state: IParsingState) => IParsingState;
|
||||
|
||||
// fallback handlers
|
||||
protected _printHandlerFb: PrintFallbackHandlerType;
|
||||
protected _executeHandlerFb: ExecuteFallbackHandlerType;
|
||||
protected _csiHandlerFb: CsiFallbackHandlerType;
|
||||
protected _escHandlerFb: EscFallbackHandlerType;
|
||||
protected _errorHandlerFb: (state: IParsingState) => IParsingState;
|
||||
|
||||
// parser stack save for async handler support
|
||||
protected _parseStack: IParserStackState = {
|
||||
state: ParserStackType.NONE,
|
||||
handlers: [],
|
||||
handlerPos: 0,
|
||||
transition: 0,
|
||||
chunkPos: 0
|
||||
};
|
||||
|
||||
constructor(
|
||||
protected readonly _transitions: TransitionTable = VT500_TRANSITION_TABLE
|
||||
) {
|
||||
super();
|
||||
|
||||
this.initialState = ParserState.GROUND;
|
||||
this.currentState = this.initialState;
|
||||
this._params = new Params(); // defaults to 32 storable params/subparams
|
||||
this._params.addParam(0); // ZDM
|
||||
this._collect = 0;
|
||||
this.precedingJoinState = 0;
|
||||
|
||||
// set default fallback handlers and handler lookup containers
|
||||
this._printHandlerFb = (data, start, end): void => { };
|
||||
this._executeHandlerFb = (code: number): void => { };
|
||||
this._csiHandlerFb = (ident: number, params: IParams): void => { };
|
||||
this._escHandlerFb = (ident: number): void => { };
|
||||
this._errorHandlerFb = (state: IParsingState): IParsingState => state;
|
||||
this._printHandler = this._printHandlerFb;
|
||||
this._executeHandlers = Object.create(null);
|
||||
this._executeHandlersArr = new Array(0x18).fill(undefined);
|
||||
this._csiHandlers = Object.create(null);
|
||||
this._escHandlers = Object.create(null);
|
||||
this._register(toDisposable(() => {
|
||||
this._csiHandlers = Object.create(null);
|
||||
this._executeHandlers = Object.create(null);
|
||||
this._executeHandlersArr = new Array(0x18).fill(undefined);
|
||||
this._escHandlers = Object.create(null);
|
||||
}));
|
||||
this._oscParser = this._register(new OscParser());
|
||||
this._dcsParser = this._register(new DcsParser());
|
||||
this._apcParser = this._register(new ApcParser());
|
||||
this._errorHandler = this._errorHandlerFb;
|
||||
|
||||
// swallow 7bit ST (ESC+\)
|
||||
this.registerEscHandler({ final: '\\' }, () => true);
|
||||
}
|
||||
|
||||
protected _identifier(id: IFunctionIdentifier, finalRange: number[] = [0x40, 0x7e]): 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 > 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 (finalRange[0] > finalCode || finalCode > finalRange[1]) {
|
||||
throw new Error(`final must be in range ${finalRange[0]} .. ${finalRange[1]}`);
|
||||
}
|
||||
res <<= 8;
|
||||
res |= finalCode;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
public identToString(ident: number): string {
|
||||
const res: string[] = [];
|
||||
while (ident) {
|
||||
res.push(String.fromCharCode(ident & 0xFF));
|
||||
ident >>= 8;
|
||||
}
|
||||
return res.reverse().join('');
|
||||
}
|
||||
|
||||
public setPrintHandler(handler: PrintHandlerType): void {
|
||||
this._printHandler = handler;
|
||||
}
|
||||
public clearPrintHandler(): void {
|
||||
this._printHandler = this._printHandlerFb;
|
||||
}
|
||||
|
||||
public registerEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): IDisposable {
|
||||
const ident = this._identifier(id, [0x30, 0x7e]);
|
||||
this._escHandlers[ident] ??= [];
|
||||
const handlerList = this._escHandlers[ident];
|
||||
handlerList.push(handler);
|
||||
return {
|
||||
dispose: () => {
|
||||
const handlerIndex = handlerList.indexOf(handler);
|
||||
if (handlerIndex !== -1) {
|
||||
handlerList.splice(handlerIndex, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
public clearEscHandler(id: IFunctionIdentifier): void {
|
||||
if (this._escHandlers[this._identifier(id, [0x30, 0x7e])]) delete this._escHandlers[this._identifier(id, [0x30, 0x7e])];
|
||||
}
|
||||
public setEscHandlerFallback(handler: EscFallbackHandlerType): void {
|
||||
this._escHandlerFb = handler;
|
||||
}
|
||||
|
||||
public setExecuteHandler(flag: string, handler: ExecuteHandlerType): void {
|
||||
const code = flag.charCodeAt(0);
|
||||
this._executeHandlers[code] = handler;
|
||||
if (code < 0x18) this._executeHandlersArr[code] = handler;
|
||||
}
|
||||
public clearExecuteHandler(flag: string): void {
|
||||
const code = flag.charCodeAt(0);
|
||||
if (this._executeHandlers[code]) delete this._executeHandlers[code];
|
||||
if (code < 0x18) this._executeHandlersArr[code] = undefined;
|
||||
}
|
||||
public setExecuteHandlerFallback(handler: ExecuteFallbackHandlerType): void {
|
||||
this._executeHandlerFb = handler;
|
||||
}
|
||||
|
||||
public registerCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): IDisposable {
|
||||
const ident = this._identifier(id);
|
||||
this._csiHandlers[ident] ??= [];
|
||||
const handlerList = this._csiHandlers[ident];
|
||||
handlerList.push(handler);
|
||||
return {
|
||||
dispose: () => {
|
||||
const handlerIndex = handlerList.indexOf(handler);
|
||||
if (handlerIndex !== -1) {
|
||||
handlerList.splice(handlerIndex, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
public clearCsiHandler(id: IFunctionIdentifier): void {
|
||||
if (this._csiHandlers[this._identifier(id)]) delete this._csiHandlers[this._identifier(id)];
|
||||
}
|
||||
public setCsiHandlerFallback(callback: (ident: number, params: IParams) => void): void {
|
||||
this._csiHandlerFb = callback;
|
||||
}
|
||||
|
||||
public registerDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable {
|
||||
return this._dcsParser.registerHandler(this._identifier(id), handler);
|
||||
}
|
||||
public clearDcsHandler(id: IFunctionIdentifier): void {
|
||||
this._dcsParser.clearHandler(this._identifier(id));
|
||||
}
|
||||
public setDcsHandlerFallback(handler: DcsFallbackHandlerType): void {
|
||||
this._dcsParser.setHandlerFallback(handler);
|
||||
}
|
||||
|
||||
public registerOscHandler(ident: number, handler: IOscHandler): IDisposable {
|
||||
return this._oscParser.registerHandler(ident, handler);
|
||||
}
|
||||
public clearOscHandler(ident: number): void {
|
||||
this._oscParser.clearHandler(ident);
|
||||
}
|
||||
public setOscHandlerFallback(handler: OscFallbackHandlerType): void {
|
||||
this._oscParser.setHandlerFallback(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(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);
|
||||
}
|
||||
|
||||
public setErrorHandler(callback: (state: IParsingState) => IParsingState): void {
|
||||
this._errorHandler = callback;
|
||||
}
|
||||
public clearErrorHandler(): void {
|
||||
this._errorHandler = this._errorHandlerFb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset parser to initial values.
|
||||
*
|
||||
* This can also be used to lift the improper continuation error condition
|
||||
* when dealing with async handlers. Use this only as a last resort to silence
|
||||
* that error when the terminal has no pending data to be processed. Note that
|
||||
* the interrupted async handler might continue its work in the future messing
|
||||
* up the terminal state even further.
|
||||
*/
|
||||
public reset(): void {
|
||||
this.currentState = this.initialState;
|
||||
this._oscParser.reset();
|
||||
this._dcsParser.reset();
|
||||
this._apcParser.reset();
|
||||
this._params.resetZdm();
|
||||
this._collect = 0;
|
||||
this.precedingJoinState = 0;
|
||||
// abort pending continuation from async handler
|
||||
// Here the RESET type indicates, that the next parse call will
|
||||
// ignore any saved stack, instead continues sync with next codepoint from GROUND
|
||||
if (this._parseStack.state !== ParserStackType.NONE) {
|
||||
this._parseStack.state = ParserStackType.RESET;
|
||||
this._parseStack.handlers = []; // also release handlers ref
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Async parse support.
|
||||
*/
|
||||
protected _preserveStack(
|
||||
state: ParserStackType,
|
||||
handlers: ResumableHandlersType,
|
||||
handlerPos: number,
|
||||
transition: number,
|
||||
chunkPos: number
|
||||
): void {
|
||||
this._parseStack.state = state;
|
||||
this._parseStack.handlers = handlers;
|
||||
this._parseStack.handlerPos = handlerPos;
|
||||
this._parseStack.transition = transition;
|
||||
this._parseStack.chunkPos = chunkPos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse UTF32 codepoints in `data` up to `length`.
|
||||
*
|
||||
* Note: For several actions with high data load the parsing is optimized
|
||||
* by using local read ahead loops with hardcoded conditions to
|
||||
* avoid costly table lookups. Make sure that any change of table values
|
||||
* will be reflected in the loop conditions as well and vice versa.
|
||||
* Affected states/actions:
|
||||
* - GROUND:PRINT
|
||||
* - CSI_PARAM:PARAM
|
||||
* - DCS_PARAM:PARAM
|
||||
* - OSC_STRING:OSC_PUT
|
||||
* - DCS_PASSTHROUGH:DCS_PUT
|
||||
*
|
||||
* Additionally the following fast paths exist before the table lookup:
|
||||
* - EXE bytes < 0x18 in non-payload states (avoids table lookup entirely)
|
||||
* - 7-bit CSI sequences without intermediates (ESC [ params final)
|
||||
*
|
||||
* Note on asynchronous handler support:
|
||||
* Any handler returning a promise will be treated as asynchronous.
|
||||
* To keep the in-band blocking working for async handlers, `parse` pauses execution,
|
||||
* creates a stack save and returns the promise to the caller.
|
||||
* For proper continuation of the paused state it is important
|
||||
* to await the promise resolving. On resolve the parse must be repeated
|
||||
* with the same chunk of data and the resolved value in `promiseResult`
|
||||
* until no promise is returned.
|
||||
*
|
||||
* Important: With only sync handlers defined, parsing is completely synchronous as well.
|
||||
* As soon as an async handler is involved, synchronous parsing is not possible anymore.
|
||||
*
|
||||
* Boilerplate for proper parsing of multiple chunks with async handlers:
|
||||
*
|
||||
* ```typescript
|
||||
* async function parseMultipleChunks(chunks: Uint32Array[]): Promise<void> {
|
||||
* for (const chunk of chunks) {
|
||||
* let result: void | Promise<boolean>;
|
||||
* let prev: boolean | undefined;
|
||||
* while (result = parser.parse(chunk, chunk.length, prev)) {
|
||||
* prev = await result;
|
||||
* }
|
||||
* }
|
||||
* // finished parsing all chunks...
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public parse(data: Uint32Array, length: number, promiseResult?: boolean): void | Promise<boolean> {
|
||||
let code: number;
|
||||
let transition: number;
|
||||
let start = 0;
|
||||
let handlerResult: void | boolean | Promise<boolean>;
|
||||
|
||||
// resume from async handler
|
||||
if (this._parseStack.state) {
|
||||
// allow sync parser reset even in continuation mode
|
||||
// Note: can be used to recover parser from improper continuation error below
|
||||
if (this._parseStack.state === ParserStackType.RESET) {
|
||||
this._parseStack.state = ParserStackType.NONE;
|
||||
start = this._parseStack.chunkPos + 1; // continue with next codepoint in GROUND
|
||||
} else {
|
||||
if (promiseResult === undefined || this._parseStack.state === ParserStackType.FAIL) {
|
||||
/**
|
||||
* Reject further parsing on improper continuation after pausing. This is a really bad
|
||||
* condition with screwed up execution order and prolly messed up terminal state,
|
||||
* therefore we exit hard with an exception and reject any further parsing.
|
||||
*
|
||||
* Note: With `Terminal.write` usage this exception should never occur, as the top level
|
||||
* calls are guaranteed to handle async conditions properly. If you ever encounter this
|
||||
* exception in your terminal integration it indicates, that you injected data chunks to
|
||||
* `InputHandler.parse` or `EscapeSequenceParser.parse` synchronously without waiting for
|
||||
* continuation of a running async handler.
|
||||
*
|
||||
* It is possible to get rid of this error by calling `reset`. But dont rely on that, as
|
||||
* the pending async handler still might mess up the terminal later. Instead fix the
|
||||
* faulty async handling, so this error will not be thrown anymore.
|
||||
*/
|
||||
this._parseStack.state = ParserStackType.FAIL;
|
||||
throw new Error('improper continuation due to previous async handler, giving up parsing');
|
||||
}
|
||||
|
||||
// we have to resume the old handler loop if:
|
||||
// - return value of the promise was `false`
|
||||
// - handlers are not exhausted yet
|
||||
const handlers = this._parseStack.handlers;
|
||||
let handlerPos = this._parseStack.handlerPos - 1;
|
||||
switch (this._parseStack.state) {
|
||||
case ParserStackType.CSI:
|
||||
if (promiseResult === false && handlerPos > -1) {
|
||||
for (; handlerPos >= 0; handlerPos--) {
|
||||
handlerResult = (handlers as CsiHandlerType[])[handlerPos](this._params);
|
||||
if (handlerResult === true) {
|
||||
break;
|
||||
} else if (handlerResult instanceof Promise) {
|
||||
this._parseStack.handlerPos = handlerPos;
|
||||
return handlerResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
this._parseStack.handlers = [];
|
||||
break;
|
||||
case ParserStackType.ESC:
|
||||
if (promiseResult === false && handlerPos > -1) {
|
||||
for (; handlerPos >= 0; handlerPos--) {
|
||||
handlerResult = (handlers as EscHandlerType[])[handlerPos]();
|
||||
if (handlerResult === true) {
|
||||
break;
|
||||
} else if (handlerResult instanceof Promise) {
|
||||
this._parseStack.handlerPos = handlerPos;
|
||||
return handlerResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
this._parseStack.handlers = [];
|
||||
break;
|
||||
case ParserStackType.DCS:
|
||||
code = data[this._parseStack.chunkPos];
|
||||
handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a, promiseResult);
|
||||
if (handlerResult) {
|
||||
return handlerResult;
|
||||
}
|
||||
if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE;
|
||||
this._params.resetZdm();
|
||||
this._collect = 0;
|
||||
break;
|
||||
case ParserStackType.OSC:
|
||||
code = data[this._parseStack.chunkPos];
|
||||
handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a, promiseResult);
|
||||
if (handlerResult) {
|
||||
return handlerResult;
|
||||
}
|
||||
if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE;
|
||||
this._params.resetZdm();
|
||||
this._collect = 0;
|
||||
break;
|
||||
case ParserStackType.APC:
|
||||
code = data[this._parseStack.chunkPos];
|
||||
handlerResult = this._apcParser.end(code !== 0x18 && code !== 0x1a, promiseResult);
|
||||
if (handlerResult) {
|
||||
return handlerResult;
|
||||
}
|
||||
if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE;
|
||||
this._params.resetZdm();
|
||||
this._collect = 0;
|
||||
break;
|
||||
}
|
||||
// cleanup before continuing with the main sync loop
|
||||
this._parseStack.state = ParserStackType.NONE;
|
||||
start = this._parseStack.chunkPos + 1;
|
||||
this.precedingJoinState = 0;
|
||||
this.currentState = this._parseStack.transition & TableAccess.TRANSITION_STATE_MASK;
|
||||
}
|
||||
}
|
||||
|
||||
// continue with main sync loop
|
||||
|
||||
// process input string
|
||||
for (let i = start; i < length; ++i) {
|
||||
code = data[i];
|
||||
|
||||
// EXE fast-path: common control bytes (0x00-0x17) in non-payload states
|
||||
if (code < 0x18 && this.currentState <= ParserState.CSI_IGNORE) {
|
||||
(this._executeHandlersArr[code] ?? this._executeHandlerFb)(code);
|
||||
this.precedingJoinState = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
// CSI fast-path: collapse ESC [ into a single entry, parse params+final in a tight loop
|
||||
if (code === 0x1b
|
||||
&& this.currentState < ParserState.OSC_STRING
|
||||
&& i + 2 < length && data[i + 1] === 0x5b
|
||||
) {
|
||||
this._params.resetZdm();
|
||||
this._collect = 0;
|
||||
let k = i + 2;
|
||||
let ch = data[k];
|
||||
if (ch >= 0x3c && ch <= 0x3f) {
|
||||
this._collect = ch;
|
||||
k++;
|
||||
}
|
||||
let csiDone = false;
|
||||
for (; k < length; k++) {
|
||||
ch = data[k];
|
||||
if (ch >= 0x30 && ch <= 0x39) {
|
||||
this._params.addDigit(ch - 48);
|
||||
} else if (ch === 0x3b) {
|
||||
this._params.addParam(0);
|
||||
} else if (ch === 0x3a) {
|
||||
this._params.addSubParam(-1);
|
||||
} else if (ch >= 0x40 && ch <= 0x7e) {
|
||||
const handlers = this._csiHandlers[this._collect << 8 | ch];
|
||||
let j = handlers ? handlers.length - 1 : -1;
|
||||
for (; j >= 0; j--) {
|
||||
handlerResult = handlers[j](this._params);
|
||||
if (handlerResult === true) {
|
||||
break;
|
||||
} else if (handlerResult instanceof Promise) {
|
||||
transition = ParserAction.CSI_DISPATCH << TableAccess.TRANSITION_ACTION_SHIFT | ParserState.GROUND;
|
||||
this._preserveStack(ParserStackType.CSI, handlers, j, transition, k);
|
||||
return handlerResult;
|
||||
}
|
||||
}
|
||||
if (j < 0) {
|
||||
this._csiHandlerFb(this._collect << 8 | ch, this._params);
|
||||
}
|
||||
this.precedingJoinState = 0;
|
||||
i = k;
|
||||
this.currentState = ParserState.GROUND;
|
||||
csiDone = true;
|
||||
break;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!csiDone) {
|
||||
i = k - 1;
|
||||
this.currentState = ParserState.CSI_PARAM;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// normal transition & action lookup
|
||||
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
|
||||
let c = i;
|
||||
const l4 = length - 4;
|
||||
while (c < l4
|
||||
&& data[++c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)
|
||||
&& data[++c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)
|
||||
&& data[++c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)
|
||||
&& data[++c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)
|
||||
) {}
|
||||
if (c >= l4) {
|
||||
while (c < length && data[c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)) {
|
||||
c++;
|
||||
}
|
||||
}
|
||||
this._printHandler(data, i, c);
|
||||
i = c - 1;
|
||||
break;
|
||||
case ParserAction.EXECUTE:
|
||||
if (this._executeHandlers[code]) this._executeHandlers[code]();
|
||||
else this._executeHandlerFb(code);
|
||||
this.precedingJoinState = 0;
|
||||
break;
|
||||
case ParserAction.IGNORE:
|
||||
break;
|
||||
case ParserAction.ERROR:
|
||||
const inject: IParsingState = this._errorHandler(
|
||||
{
|
||||
position: i,
|
||||
code,
|
||||
currentState: this.currentState,
|
||||
collect: this._collect,
|
||||
params: this._params,
|
||||
abort: false
|
||||
});
|
||||
if (inject.abort) return;
|
||||
// inject values: currently not implemented
|
||||
break;
|
||||
case ParserAction.CSI_DISPATCH:
|
||||
// Trigger CSI Handler
|
||||
const handlers = this._csiHandlers[this._collect << 8 | code];
|
||||
let j = handlers ? handlers.length - 1 : -1;
|
||||
for (; j >= 0; j--) {
|
||||
// true means success and to stop bubbling
|
||||
// a promise indicates an async handler that needs to finish before progressing
|
||||
handlerResult = handlers[j](this._params);
|
||||
if (handlerResult === true) {
|
||||
break;
|
||||
} else if (handlerResult instanceof Promise) {
|
||||
this._preserveStack(ParserStackType.CSI, handlers, j, transition, i);
|
||||
return handlerResult;
|
||||
}
|
||||
}
|
||||
if (j < 0) {
|
||||
this._csiHandlerFb(this._collect << 8 | code, this._params);
|
||||
}
|
||||
this.precedingJoinState = 0;
|
||||
break;
|
||||
case ParserAction.PARAM:
|
||||
// inner loop: digits (0x30 - 0x39) and ; (0x3b) and : (0x3a)
|
||||
do {
|
||||
switch (code) {
|
||||
case 0x3b:
|
||||
this._params.addParam(0); // ZDM
|
||||
break;
|
||||
case 0x3a:
|
||||
this._params.addSubParam(-1);
|
||||
break;
|
||||
default: // 0x30 - 0x39
|
||||
this._params.addDigit(code - 48);
|
||||
}
|
||||
} while (++i < length && (code = data[i]) > 0x2f && code < 0x3c);
|
||||
i--;
|
||||
break;
|
||||
case ParserAction.COLLECT:
|
||||
this._collect <<= 8;
|
||||
this._collect |= code;
|
||||
break;
|
||||
case ParserAction.ESC_DISPATCH:
|
||||
const handlersEsc = this._escHandlers[this._collect << 8 | code];
|
||||
let jj = handlersEsc ? handlersEsc.length - 1 : -1;
|
||||
for (; jj >= 0; jj--) {
|
||||
// true means success and to stop bubbling
|
||||
// a promise indicates an async handler that needs to finish before progressing
|
||||
handlerResult = handlersEsc[jj]();
|
||||
if (handlerResult === true) {
|
||||
break;
|
||||
} else if (handlerResult instanceof Promise) {
|
||||
this._preserveStack(ParserStackType.ESC, handlersEsc, jj, transition, i);
|
||||
return handlerResult;
|
||||
}
|
||||
}
|
||||
if (jj < 0) {
|
||||
this._escHandlerFb(this._collect << 8 | code);
|
||||
}
|
||||
this.precedingJoinState = 0;
|
||||
break;
|
||||
case ParserAction.CLEAR:
|
||||
this._params.resetZdm();
|
||||
this._collect = 0;
|
||||
break;
|
||||
case ParserAction.DCS_HOOK:
|
||||
this._dcsParser.hook(this._collect << 8 | code, this._params);
|
||||
break;
|
||||
case ParserAction.DCS_PUT:
|
||||
// inner loop - exit DCS_PUT: 0x18, 0x1a, 0x1b, 0x7f, 0x80 - 0x9f
|
||||
// unhook triggered by: 0x1b, 0x9c (success) and 0x18, 0x1a (abort)
|
||||
for (let j = i + 1; ; ++j) {
|
||||
if (j >= length || (code = data[j]) === 0x18 || code === 0x1a || code === 0x1b || (code > 0x7f && code < NON_ASCII_PRINTABLE)) {
|
||||
this._dcsParser.put(data, i, j);
|
||||
i = j - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ParserAction.DCS_UNHOOK:
|
||||
handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a);
|
||||
if (handlerResult) {
|
||||
this._preserveStack(ParserStackType.DCS, [], 0, transition, i);
|
||||
return handlerResult;
|
||||
}
|
||||
if (code === 0x1b) transition |= ParserState.ESCAPE;
|
||||
this._params.resetZdm();
|
||||
this._collect = 0;
|
||||
this.precedingJoinState = 0;
|
||||
break;
|
||||
case ParserAction.OSC_START:
|
||||
this._oscParser.start();
|
||||
break;
|
||||
case ParserAction.OSC_PUT:
|
||||
// inner loop: 0x20 (SP) included, 0x7F (DEL) included
|
||||
for (let j = i + 1; ; j++) {
|
||||
if (j >= length || (code = data[j]) < 0x20 || (code > 0x7f && code < NON_ASCII_PRINTABLE)) {
|
||||
this._oscParser.put(data, i, j);
|
||||
i = j - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ParserAction.OSC_END:
|
||||
handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a);
|
||||
if (handlerResult) {
|
||||
this._preserveStack(ParserStackType.OSC, [], 0, transition, i);
|
||||
return handlerResult;
|
||||
}
|
||||
if (code === 0x1b) transition |= ParserState.ESCAPE;
|
||||
this._params.resetZdm();
|
||||
this._collect = 0;
|
||||
this.precedingJoinState = 0;
|
||||
break;
|
||||
case ParserAction.APC_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 && (
|
||||
(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:
|
||||
handlerResult = this._apcParser.end(code !== 0x18 && code !== 0x1a);
|
||||
if (handlerResult) {
|
||||
this._preserveStack(ParserStackType.APC, [], 0, transition, i);
|
||||
return handlerResult;
|
||||
}
|
||||
if (code === 0x1b) transition |= ParserState.ESCAPE;
|
||||
this._params.resetZdm();
|
||||
this._collect = 0;
|
||||
this.precedingJoinState = 0;
|
||||
break;
|
||||
}
|
||||
this.currentState = transition & TableAccess.TRANSITION_STATE_MASK;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,491 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
import { assert } from 'chai';
|
||||
import { OscParser, OscHandler } from './OscParser';
|
||||
import { StringToUtf32, utf32ToString } from '../input/TextDecoder';
|
||||
import { IOscHandler } from './Types';
|
||||
|
||||
function toUtf32(s: string): Uint32Array {
|
||||
const utf32 = new Uint32Array(s.length);
|
||||
const decoder = new StringToUtf32();
|
||||
const length = decoder.decode(s, utf32);
|
||||
return utf32.subarray(0, length);
|
||||
}
|
||||
|
||||
class TestHandler implements IOscHandler {
|
||||
constructor(public id: number, public output: any[], public msg: string, public returnFalse: boolean = false) {}
|
||||
public start(): void {
|
||||
this.output.push([this.msg, this.id, 'START']);
|
||||
}
|
||||
public put(data: Uint32Array, start: number, end: number): void {
|
||||
this.output.push([this.msg, this.id, 'PUT', utf32ToString(data, start, end)]);
|
||||
}
|
||||
public end(success: boolean): boolean {
|
||||
this.output.push([this.msg, this.id, 'END', success]);
|
||||
if (this.returnFalse) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
describe('OscParser', () => {
|
||||
let parser: OscParser;
|
||||
let reports: any[] = [];
|
||||
beforeEach(() => {
|
||||
reports = [];
|
||||
parser = new OscParser();
|
||||
parser.setHandlerFallback((id, action, data) => {
|
||||
reports.push([id, action, data]);
|
||||
});
|
||||
});
|
||||
describe('identifier parsing', () => {
|
||||
it('no report for illegal ids', () => {
|
||||
const data = toUtf32('hello world!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, []);
|
||||
});
|
||||
it('no payload', () => {
|
||||
parser.start();
|
||||
let data = toUtf32('12');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('34');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [[1234, 'START', undefined], [1234, 'END', true]]);
|
||||
});
|
||||
it('with payload', () => {
|
||||
parser.start();
|
||||
let data = toUtf32('12');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('34');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(';h');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('ello');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [
|
||||
[1234, 'START', undefined],
|
||||
[1234, 'PUT', 'h'],
|
||||
[1234, 'PUT', 'ello'],
|
||||
[1234, 'END', true]
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('handler registration', () => {
|
||||
it('setOscHandler', () => {
|
||||
parser.registerHandler(1234, new TestHandler(1234, reports, 'th'));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [
|
||||
// messages from TestHandler
|
||||
['th', 1234, 'START'],
|
||||
['th', 1234, 'PUT', 'Here comes'],
|
||||
['th', 1234, 'PUT', 'the mouse!'],
|
||||
['th', 1234, 'END', true]
|
||||
]);
|
||||
});
|
||||
it('clearOscHandler', () => {
|
||||
parser.registerHandler(1234, new TestHandler(1234, reports, 'th'));
|
||||
parser.clearHandler(1234);
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [
|
||||
// messages from fallback handler
|
||||
[1234, 'START', undefined],
|
||||
[1234, 'PUT', 'Here comes'],
|
||||
[1234, 'PUT', 'the mouse!'],
|
||||
[1234, 'END', true]
|
||||
]);
|
||||
});
|
||||
it('addOscHandler', () => {
|
||||
parser.registerHandler(1234, new TestHandler(1234, reports, 'th1'));
|
||||
parser.registerHandler(1234, new TestHandler(1234, reports, 'th2'));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [
|
||||
['th2', 1234, 'START'],
|
||||
['th1', 1234, 'START'],
|
||||
['th2', 1234, 'PUT', 'Here comes'],
|
||||
['th1', 1234, 'PUT', 'Here comes'],
|
||||
['th2', 1234, 'PUT', 'the mouse!'],
|
||||
['th1', 1234, 'PUT', 'the mouse!'],
|
||||
['th2', 1234, 'END', true],
|
||||
['th1', 1234, 'END', false] // false due being already handled by th2!
|
||||
]);
|
||||
});
|
||||
it('addOscHandler with return false', () => {
|
||||
parser.registerHandler(1234, new TestHandler(1234, reports, 'th1'));
|
||||
parser.registerHandler(1234, new TestHandler(1234, reports, 'th2', true));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [
|
||||
['th2', 1234, 'START'],
|
||||
['th1', 1234, 'START'],
|
||||
['th2', 1234, 'PUT', 'Here comes'],
|
||||
['th1', 1234, 'PUT', 'Here comes'],
|
||||
['th2', 1234, 'PUT', 'the mouse!'],
|
||||
['th1', 1234, 'PUT', 'the mouse!'],
|
||||
['th2', 1234, 'END', true],
|
||||
['th1', 1234, 'END', true] // true since th2 indicated to keep bubbling
|
||||
]);
|
||||
});
|
||||
it('dispose handlers', () => {
|
||||
parser.registerHandler(1234, new TestHandler(1234, reports, 'th1'));
|
||||
const dispo = parser.registerHandler(1234, new TestHandler(1234, reports, 'th2', true));
|
||||
dispo.dispose();
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [
|
||||
['th1', 1234, 'START'],
|
||||
['th1', 1234, 'PUT', 'Here comes'],
|
||||
['th1', 1234, 'PUT', 'the mouse!'],
|
||||
['th1', 1234, 'END', true]
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('OscHandlerFactory', () => {
|
||||
const TEST_PAYLOAD_LIMIT = 100;
|
||||
const CHUNK_SIZE = 10;
|
||||
let originalPayloadLimit: number;
|
||||
|
||||
beforeEach(() => {
|
||||
const handlerConstructor = OscHandler as unknown as { _payloadLimit: number };
|
||||
originalPayloadLimit = handlerConstructor._payloadLimit;
|
||||
handlerConstructor._payloadLimit = TEST_PAYLOAD_LIMIT;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
const handlerConstructor = OscHandler as unknown as { _payloadLimit: number };
|
||||
handlerConstructor._payloadLimit = originalPayloadLimit;
|
||||
});
|
||||
|
||||
it('should be called once on end(true)', () => {
|
||||
parser.registerHandler(1234, new OscHandler(data => { reports.push([1234, data]); return true; }));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [[1234, 'Here comes the mouse!']]);
|
||||
});
|
||||
it('should not be called on end(false)', () => {
|
||||
parser.registerHandler(1234, new OscHandler(data => { reports.push([1234, data]); return true; }));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(false);
|
||||
assert.deepEqual(reports, []);
|
||||
});
|
||||
it('should be disposable', () => {
|
||||
parser.registerHandler(1234, new OscHandler(data => { reports.push(['one', data]); return true; }));
|
||||
const dispo = parser.registerHandler(1234, new OscHandler(data => { reports.push(['two', data]); return true; }));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;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!']]);
|
||||
dispo.dispose();
|
||||
parser.start();
|
||||
data = toUtf32('1234;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(1234, new OscHandler(data => { reports.push(['one', data]); return true; }));
|
||||
parser.registerHandler(1234, new OscHandler(data => { reports.push(['two', data]); return false; }));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;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(1234, new OscHandler(data => { reports.push([1234, data]); return true; }));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;');
|
||||
parser.put(data, 0, data.length);
|
||||
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(reports, [[1234, 'A'.repeat(TEST_PAYLOAD_LIMIT)]]);
|
||||
});
|
||||
it('should abort for payload limit +1', function(): void {
|
||||
this.timeout(30000);
|
||||
parser.registerHandler(1234, new OscHandler(data => { reports.push([1234, data]); return true; }));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;');
|
||||
parser.put(data, 0, data.length);
|
||||
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(reports, []);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
class TestHandlerAsync implements IOscHandler {
|
||||
constructor(public id: number, public output: any[], public msg: string, public returnFalse: boolean = false) {}
|
||||
public start(): void {
|
||||
this.output.push([this.msg, this.id, 'START']);
|
||||
}
|
||||
public put(data: Uint32Array, start: number, end: number): void {
|
||||
this.output.push([this.msg, this.id, 'PUT', utf32ToString(data, start, end)]);
|
||||
}
|
||||
public async end(success: boolean): Promise<boolean> {
|
||||
await Promise.resolve();
|
||||
this.output.push([this.msg, this.id, 'END', success]);
|
||||
if (this.returnFalse) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
async function endP(parser: OscParser, success: boolean): Promise<void> {
|
||||
let result: void | Promise<boolean>;
|
||||
let prev: boolean | undefined;
|
||||
while (result = parser.end(success, prev)) {
|
||||
prev = await result;
|
||||
}
|
||||
}
|
||||
|
||||
describe('OscParser - async tests', () => {
|
||||
let parser: OscParser;
|
||||
let reports: any[] = [];
|
||||
beforeEach(() => {
|
||||
reports = [];
|
||||
parser = new OscParser();
|
||||
parser.setHandlerFallback((id, action, data) => {
|
||||
reports.push([id, action, data]);
|
||||
});
|
||||
});
|
||||
describe('sync and async mixed', () => {
|
||||
describe('sync | async | sync', () => {
|
||||
it('first should run, cleanup action for others', async () => {
|
||||
parser.registerHandler(1234, new TestHandler(1234, reports, 's1'));
|
||||
parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 'a1'));
|
||||
parser.registerHandler(1234, new TestHandler(1234, reports, 's2'));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await endP(parser, true);
|
||||
assert.deepEqual(reports, [
|
||||
// messages from TestHandler
|
||||
['s2', 1234, 'START'],
|
||||
['a1', 1234, 'START'],
|
||||
['s1', 1234, 'START'],
|
||||
['s2', 1234, 'PUT', 'Here comes'],
|
||||
['a1', 1234, 'PUT', 'Here comes'],
|
||||
['s1', 1234, 'PUT', 'Here comes'],
|
||||
['s2', 1234, 'PUT', 'the mouse!'],
|
||||
['a1', 1234, 'PUT', 'the mouse!'],
|
||||
['s1', 1234, 'PUT', 'the mouse!'],
|
||||
['s2', 1234, 'END', true],
|
||||
['a1', 1234, 'END', false],
|
||||
['s1', 1234, 'END', false]
|
||||
]);
|
||||
});
|
||||
it('all should run', async () => {
|
||||
parser.registerHandler(1234, new TestHandler(1234, reports, 's1', true));
|
||||
parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 'a1', true));
|
||||
parser.registerHandler(1234, new TestHandler(1234, reports, 's2', true));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await endP(parser, true);
|
||||
assert.deepEqual(reports, [
|
||||
// messages from TestHandler
|
||||
['s2', 1234, 'START'],
|
||||
['a1', 1234, 'START'],
|
||||
['s1', 1234, 'START'],
|
||||
['s2', 1234, 'PUT', 'Here comes'],
|
||||
['a1', 1234, 'PUT', 'Here comes'],
|
||||
['s1', 1234, 'PUT', 'Here comes'],
|
||||
['s2', 1234, 'PUT', 'the mouse!'],
|
||||
['a1', 1234, 'PUT', 'the mouse!'],
|
||||
['s1', 1234, 'PUT', 'the mouse!'],
|
||||
['s2', 1234, 'END', true],
|
||||
['a1', 1234, 'END', true],
|
||||
['s1', 1234, 'END', true]
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('async | sync | async', () => {
|
||||
it('first should run, cleanup action for others', async () => {
|
||||
parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 's1'));
|
||||
parser.registerHandler(1234, new TestHandler(1234, reports, 'a1'));
|
||||
parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 's2'));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await endP(parser, true);
|
||||
assert.deepEqual(reports, [
|
||||
// messages from TestHandler
|
||||
['s2', 1234, 'START'],
|
||||
['a1', 1234, 'START'],
|
||||
['s1', 1234, 'START'],
|
||||
['s2', 1234, 'PUT', 'Here comes'],
|
||||
['a1', 1234, 'PUT', 'Here comes'],
|
||||
['s1', 1234, 'PUT', 'Here comes'],
|
||||
['s2', 1234, 'PUT', 'the mouse!'],
|
||||
['a1', 1234, 'PUT', 'the mouse!'],
|
||||
['s1', 1234, 'PUT', 'the mouse!'],
|
||||
['s2', 1234, 'END', true],
|
||||
['a1', 1234, 'END', false],
|
||||
['s1', 1234, 'END', false]
|
||||
]);
|
||||
});
|
||||
it('all should run', async () => {
|
||||
parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 's1', true));
|
||||
parser.registerHandler(1234, new TestHandler(1234, reports, 'a1', true));
|
||||
parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 's2', true));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await endP(parser, true);
|
||||
assert.deepEqual(reports, [
|
||||
// messages from TestHandler
|
||||
['s2', 1234, 'START'],
|
||||
['a1', 1234, 'START'],
|
||||
['s1', 1234, 'START'],
|
||||
['s2', 1234, 'PUT', 'Here comes'],
|
||||
['a1', 1234, 'PUT', 'Here comes'],
|
||||
['s1', 1234, 'PUT', 'Here comes'],
|
||||
['s2', 1234, 'PUT', 'the mouse!'],
|
||||
['a1', 1234, 'PUT', 'the mouse!'],
|
||||
['s1', 1234, 'PUT', 'the mouse!'],
|
||||
['s2', 1234, 'END', true],
|
||||
['a1', 1234, 'END', true],
|
||||
['s1', 1234, 'END', true]
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('OscHandlerFactory', () => {
|
||||
it('should be called once on end(true)', async () => {
|
||||
parser.registerHandler(1234, new OscHandler(async data => { reports.push([1234, data]); return true; }));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
await endP(parser, true);
|
||||
assert.deepEqual(reports, [[1234, 'Here comes the mouse!']]);
|
||||
});
|
||||
it('should not be called on end(false)', async () => {
|
||||
parser.registerHandler(1234, new OscHandler(async data => { reports.push([1234, data]); return true; }));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await endP(parser, false);
|
||||
assert.deepEqual(reports, []);
|
||||
});
|
||||
it('should be disposable', async () => {
|
||||
parser.registerHandler(1234, new OscHandler(async data => { reports.push(['one', data]); return true; }));
|
||||
const dispo = parser.registerHandler(1234, new OscHandler(async data => { reports.push(['two', data]); return true; }));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await endP(parser, true);
|
||||
assert.deepEqual(reports, [['two', 'Here comes the mouse!']]);
|
||||
dispo.dispose();
|
||||
parser.start();
|
||||
data = toUtf32('1234;some other');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' data');
|
||||
parser.put(data, 0, data.length);
|
||||
await endP(parser, true);
|
||||
assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'some other data']]);
|
||||
});
|
||||
it('should respect return false', async () => {
|
||||
parser.registerHandler(1234, new OscHandler(async data => { reports.push(['one', data]); return true; }));
|
||||
parser.registerHandler(1234, new OscHandler(async data => { reports.push(['two', data]); return false; }));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await endP(parser, true);
|
||||
assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'Here comes the mouse!']]);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('reset', () => {
|
||||
it('should abort active handlers with end(false) when reset during payload', () => {
|
||||
parser.registerHandler(1234, new TestHandler(1234, reports, 'th'));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;partial');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.reset();
|
||||
assert.deepEqual(reports, [
|
||||
['th', 1234, 'START'],
|
||||
['th', 1234, 'PUT', 'partial'],
|
||||
['th', 1234, 'END', false]
|
||||
]);
|
||||
reports.length = 0;
|
||||
parser.start();
|
||||
data = toUtf32('1234;complete');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [
|
||||
['th', 1234, 'START'],
|
||||
['th', 1234, 'PUT', 'complete'],
|
||||
['th', 1234, 'END', true]
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,237 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IOscHandler, IHandlerCollection, OscFallbackHandlerType, IOscParser, ISubParserStackState } from './Types';
|
||||
import { OscState, ParserConstants } from './Constants';
|
||||
import { utf32ToString } from '../input/TextDecoder';
|
||||
import { IDisposable } from '../../Types';
|
||||
import { LimitedStringBuilder } from '../StringBuilder';
|
||||
|
||||
const EMPTY_HANDLERS: IOscHandler[] = [];
|
||||
|
||||
export class OscParser implements IOscParser {
|
||||
private _state = OscState.START;
|
||||
private _active = EMPTY_HANDLERS;
|
||||
private _id = -1;
|
||||
private _handlers: IHandlerCollection<IOscHandler> = Object.create(null);
|
||||
private _handlerFb: OscFallbackHandlerType = () => { };
|
||||
private _stack: ISubParserStackState = {
|
||||
paused: false,
|
||||
loopPosition: 0,
|
||||
fallThrough: false
|
||||
};
|
||||
|
||||
public registerHandler(ident: number, handler: IOscHandler): IDisposable {
|
||||
this._handlers[ident] ??= [];
|
||||
const handlerList = this._handlers[ident];
|
||||
handlerList.push(handler);
|
||||
return {
|
||||
dispose: () => {
|
||||
const handlerIndex = handlerList.indexOf(handler);
|
||||
if (handlerIndex !== -1) {
|
||||
handlerList.splice(handlerIndex, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
public clearHandler(ident: number): void {
|
||||
if (this._handlers[ident]) delete this._handlers[ident];
|
||||
}
|
||||
public setHandlerFallback(handler: OscFallbackHandlerType): void {
|
||||
this._handlerFb = handler;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._handlers = Object.create(null);
|
||||
this._handlerFb = () => { };
|
||||
this._active = EMPTY_HANDLERS;
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
// force cleanup handlers if payload was already sent
|
||||
if (this._state === OscState.PAYLOAD) {
|
||||
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 = OscState.START;
|
||||
}
|
||||
|
||||
private _start(): void {
|
||||
this._active = this._handlers[this._id] || EMPTY_HANDLERS;
|
||||
if (!this._active.length) {
|
||||
this._handlerFb(this._id, 'START');
|
||||
} else {
|
||||
for (let j = this._active.length - 1; j >= 0; j--) {
|
||||
this._active[j].start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _put(data: Uint32Array, start: number, end: number): void {
|
||||
if (!this._active.length) {
|
||||
this._handlerFb(this._id, 'PUT', utf32ToString(data, start, end));
|
||||
} else {
|
||||
for (let j = this._active.length - 1; j >= 0; j--) {
|
||||
this._active[j].put(data, start, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
// always reset leftover handlers
|
||||
this.reset();
|
||||
this._state = OscState.ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Put data to current OSC command.
|
||||
* Expects the identifier of the OSC command in the form
|
||||
* OSC id ; payload ST/BEL
|
||||
* Payload chunks are not further processed and get
|
||||
* directly passed to the handlers.
|
||||
*/
|
||||
public put(data: Uint32Array, start: number, end: number): void {
|
||||
if (this._state === OscState.ABORT) {
|
||||
return;
|
||||
}
|
||||
if (this._state === OscState.ID) {
|
||||
while (start < end) {
|
||||
const code = data[start++];
|
||||
if (code === 0x3b) {
|
||||
this._state = OscState.PAYLOAD;
|
||||
this._start();
|
||||
break;
|
||||
}
|
||||
if (code < 0x30 || 0x39 < code) {
|
||||
this._state = OscState.ABORT;
|
||||
return;
|
||||
}
|
||||
if (this._id === -1) {
|
||||
this._id = 0;
|
||||
}
|
||||
this._id = this._id * 10 + code - 48;
|
||||
}
|
||||
}
|
||||
if (this._state === OscState.PAYLOAD && end - start > 0) {
|
||||
this._put(data, start, end);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates end of an OSC command.
|
||||
* Whether the OSC got aborted or finished normally
|
||||
* is indicated by `success`.
|
||||
*/
|
||||
public end(success: boolean, promiseResult: boolean = true): void | Promise<boolean> {
|
||||
if (this._state === OscState.START) {
|
||||
return;
|
||||
}
|
||||
// do nothing if command was faulty
|
||||
if (this._state !== OscState.ABORT) {
|
||||
// if we are still in ID state and get an early end
|
||||
// means that the command has no payload thus we still have
|
||||
// to announce START and send END right after
|
||||
if (this._state === OscState.ID) {
|
||||
this._start();
|
||||
}
|
||||
|
||||
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
|
||||
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 = OscState.START;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient class to allow attaching string based handler functions
|
||||
* as OSC handlers.
|
||||
*/
|
||||
export class OscHandler implements IOscHandler {
|
||||
private static _payloadLimit = ParserConstants.PAYLOAD_LIMIT;
|
||||
|
||||
private _data = new LimitedStringBuilder(OscHandler._payloadLimit);
|
||||
private _hitLimit: boolean = false;
|
||||
|
||||
constructor(private _handler: (data: string) => boolean | Promise<boolean>) { }
|
||||
|
||||
public start(): void {
|
||||
this._data.reset();
|
||||
this._hitLimit = false;
|
||||
}
|
||||
|
||||
public put(data: Uint32Array, start: number, end: number): void {
|
||||
if (this._hitLimit) {
|
||||
return;
|
||||
}
|
||||
if (this._data.append(utf32ToString(data, start, end))) {
|
||||
this._hitLimit = true;
|
||||
}
|
||||
}
|
||||
|
||||
public end(success: boolean): boolean | Promise<boolean> {
|
||||
let ret: boolean | Promise<boolean> = false;
|
||||
if (this._hitLimit) {
|
||||
ret = false;
|
||||
} else if (success) {
|
||||
ret = this._handler(this._data.toString());
|
||||
if (ret instanceof Promise) {
|
||||
// need to hold data until `ret` got resolved
|
||||
// dont care for errors, data will be freed anyway on next start
|
||||
return ret.then(res => {
|
||||
this._data.reset();
|
||||
this._hitLimit = false;
|
||||
return res;
|
||||
});
|
||||
}
|
||||
}
|
||||
this._data.reset();
|
||||
this._hitLimit = false;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
import { assert } from 'chai';
|
||||
import { Params } from './Params';
|
||||
import { ParamsArray } from './Types';
|
||||
|
||||
class TestParams extends Params {
|
||||
public get subParams(): Int32Array {
|
||||
return this._subParams;
|
||||
}
|
||||
public get subParamsLength(): number {
|
||||
return this._subParamsLength;
|
||||
}
|
||||
}
|
||||
|
||||
/** `Params` parser shim */
|
||||
function parse(params: Params, s: string | string[]): void {
|
||||
params.reset();
|
||||
params.addParam(0);
|
||||
if (typeof s === 'string') {
|
||||
s = [s];
|
||||
}
|
||||
for (const chunk of s) {
|
||||
for (let i = 0; i < chunk.length; ++i) {
|
||||
let code = chunk.charCodeAt(i);
|
||||
do {
|
||||
switch (code) {
|
||||
case 0x3b:
|
||||
params.addParam(0);
|
||||
break;
|
||||
case 0x3a:
|
||||
params.addSubParam(-1);
|
||||
break;
|
||||
default: // 0x30 - 0x39
|
||||
params.addDigit(code - 48);
|
||||
}
|
||||
} while (++i < s.length && (code = chunk.charCodeAt(i)) > 0x2f && code < 0x3c);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
describe('Params', () => {
|
||||
it('should respect ctor args', () => {
|
||||
const params = new TestParams(12, 23);
|
||||
assert.equal(params.params.length, 12);
|
||||
assert.equal(params.subParams.length, 23);
|
||||
assert.deepEqual(params.toArray(), []);
|
||||
});
|
||||
it('addParam', () => {
|
||||
const params = new TestParams();
|
||||
params.addParam(1);
|
||||
assert.equal(params.length, 1);
|
||||
assert.deepEqual(Array.prototype.slice.call(params.params, 0, params.length), [1]);
|
||||
assert.deepEqual(params.toArray(), [1]);
|
||||
params.addParam(23);
|
||||
assert.equal(params.length, 2);
|
||||
assert.deepEqual(Array.prototype.slice.call(params.params, 0, params.length), [1, 23]);
|
||||
assert.deepEqual(params.toArray(), [1, 23]);
|
||||
assert.equal(params.subParamsLength, 0);
|
||||
});
|
||||
it('addSubParam', () => {
|
||||
const params = new TestParams();
|
||||
params.addParam(1);
|
||||
params.addSubParam(2);
|
||||
params.addSubParam(3);
|
||||
assert.equal(params.length, 1);
|
||||
assert.equal(params.subParamsLength, 2);
|
||||
assert.deepEqual(params.toArray(), [1, [2, 3]]);
|
||||
params.addParam(12345);
|
||||
params.addSubParam(-1);
|
||||
assert.equal(params.length, 2);
|
||||
assert.equal(params.subParamsLength, 3);
|
||||
assert.deepEqual(params.toArray(), [1, [2, 3], 12345, [-1]]);
|
||||
});
|
||||
it('should not add sub params without previous param', () => {
|
||||
const params = new TestParams();
|
||||
params.addSubParam(2);
|
||||
params.addSubParam(3);
|
||||
assert.equal(params.length, 0);
|
||||
assert.equal(params.subParamsLength, 0);
|
||||
assert.deepEqual(params.toArray(), []);
|
||||
params.addParam(1);
|
||||
params.addSubParam(2);
|
||||
params.addSubParam(3);
|
||||
assert.equal(params.length, 1);
|
||||
assert.equal(params.subParamsLength, 2);
|
||||
assert.deepEqual(params.toArray(), [1, [2, 3]]);
|
||||
});
|
||||
it('reset', () => {
|
||||
const params = new TestParams();
|
||||
params.addParam(1);
|
||||
params.addSubParam(2);
|
||||
params.addSubParam(3);
|
||||
params.addParam(12345);
|
||||
params.addSubParam(-1);
|
||||
params.reset();
|
||||
assert.equal(params.length, 0);
|
||||
assert.equal(params.subParamsLength, 0);
|
||||
assert.deepEqual(params.toArray(), []);
|
||||
params.addParam(1);
|
||||
params.addSubParam(2);
|
||||
params.addSubParam(3);
|
||||
params.addParam(12345);
|
||||
params.addSubParam(-1);
|
||||
assert.equal(params.length, 2);
|
||||
assert.equal(params.subParamsLength, 3);
|
||||
assert.deepEqual(params.toArray(), [1, [2, 3], 12345, [-1]]);
|
||||
});
|
||||
it('Params.fromArray --> toArray', () => {
|
||||
let data: ParamsArray = [];
|
||||
assert.deepEqual(Params.fromArray(data).toArray(), data);
|
||||
data = [1, [2, 3], 12345, [-1]];
|
||||
assert.deepEqual(Params.fromArray(data).toArray(), data);
|
||||
data = [38, 2, 50, 100, 150];
|
||||
assert.deepEqual(Params.fromArray(data).toArray(), data);
|
||||
data = [38, 2, 50, 100, [150]];
|
||||
assert.deepEqual(Params.fromArray(data).toArray(), data);
|
||||
data = [38, [2, 50, 100, 150]];
|
||||
assert.deepEqual(Params.fromArray(data).toArray(), data);
|
||||
// strip empty sub params
|
||||
data = [38, [2, 50, 100, 150], 5, [], 6];
|
||||
assert.deepEqual(Params.fromArray(data).toArray(), [38, [2, 50, 100, 150], 5, 6]);
|
||||
});
|
||||
it('clone', () => {
|
||||
const params = Params.fromArray([38, [2, 50, 100, 150], 5, [], 6, 1, [2, 3], 12345, [-1]]);
|
||||
assert.deepEqual(params.clone(), params);
|
||||
});
|
||||
it('hasSubParams / getSubParams', () => {
|
||||
const params = Params.fromArray([38, [2, 50, 100, 150], 5, [], 6]);
|
||||
assert.equal(params.hasSubParams(0), true);
|
||||
assert.deepEqual(params.getSubParams(0), new Int32Array([2, 50, 100, 150]));
|
||||
assert.equal(params.hasSubParams(1), false);
|
||||
assert.deepEqual(params.getSubParams(1), null);
|
||||
assert.equal(params.hasSubParams(2), false);
|
||||
assert.deepEqual(params.getSubParams(2), null);
|
||||
});
|
||||
it('getSubParamsAll', () => {
|
||||
const params = Params.fromArray([1, [2, 3], 7, 12345, [-1]]);
|
||||
assert.deepEqual(params.getSubParamsAll(), {0: new Int32Array([2, 3]), 2: new Int32Array([-1])});
|
||||
});
|
||||
describe('parse tests', () => {
|
||||
it('param defaults to 0 (ZDM - zero default mode)', () => {
|
||||
const params = new Params();
|
||||
parse(params, '');
|
||||
assert.deepEqual(params.toArray(), [0]);
|
||||
});
|
||||
it('sub param defaults to -1', () => {
|
||||
const params = new Params();
|
||||
parse(params, ':');
|
||||
assert.deepEqual(params.toArray(), [0, [-1]]);
|
||||
});
|
||||
it('should correctly reset on new sequence', () => {
|
||||
const params = new Params();
|
||||
parse(params, '1;2;3');
|
||||
assert.deepEqual(params.toArray(), [1, 2, 3]);
|
||||
parse(params, '4');
|
||||
assert.deepEqual(params.toArray(), [4]);
|
||||
parse(params, '4::123:5;6;7');
|
||||
assert.deepEqual(params.toArray(), [4, [-1, 123, 5], 6, 7]);
|
||||
parse(params, '');
|
||||
assert.deepEqual(params.toArray(), [0]);
|
||||
});
|
||||
it('should handle length restrictions correctly', () => {
|
||||
// restrict to 3 params and 3 sub params
|
||||
const params = new Params(3, 3);
|
||||
parse(params, '1;2;3');
|
||||
assert.deepEqual(params.toArray(), [1, 2, 3]);
|
||||
parse(params, '4');
|
||||
assert.deepEqual(params.toArray(), [4]);
|
||||
parse(params, '4::123:5;6;7');
|
||||
assert.deepEqual(params.toArray(), [4, [-1, 123, 5], 6, 7]);
|
||||
parse(params, '');
|
||||
assert.deepEqual(params.toArray(), [0]);
|
||||
// overlong params
|
||||
parse(params, '1;2;3;4;5;6;7');
|
||||
assert.deepEqual(params.toArray(), [1, 2, 3]);
|
||||
// overlong sub params
|
||||
parse(params, '4;38:2::50:100:150;48:5:22');
|
||||
assert.deepEqual(params.toArray(), [4, 38, [2, -1, 50], 48]);
|
||||
});
|
||||
it('typical sequences', () => {
|
||||
const params = new Params();
|
||||
// SGR with semicolon syntax
|
||||
parse(params, '0;4;38;2;50;100;150;48;5;22');
|
||||
assert.deepEqual(params.toArray(), [0, 4, 38, 2, 50, 100, 150, 48, 5, 22]);
|
||||
// SGR mixed style (partly wrong)
|
||||
parse(params, '0;4;38;2;50:100:150;48;5:22');
|
||||
assert.deepEqual(params.toArray(), [0, 4, 38, 2, 50, [100, 150], 48, 5, [22]]);
|
||||
// SGR colon style
|
||||
parse(params, '0;4;38:2::50:100:150;48:5:22');
|
||||
assert.deepEqual(params.toArray(), [0, 4, 38, [2, -1, 50, 100, 150], 48, [5, 22]]);
|
||||
});
|
||||
});
|
||||
describe('should not overflow to negative', () => {
|
||||
it('reject params lesser -1', () => {
|
||||
const params = new Params();
|
||||
params.addParam(-1);
|
||||
assert.throws(() => params.addParam(-2), 'values less than -1 are not allowed');
|
||||
});
|
||||
it('reject subparams lesser -1', () => {
|
||||
const params = new Params();
|
||||
params.addParam(-1);
|
||||
params.addSubParam(-1);
|
||||
assert.throws(() => params.addSubParam(-2), 'values less than -1 are not allowed');
|
||||
assert.deepEqual(params.toArray(), [-1, [-1]]);
|
||||
});
|
||||
it('clamp parsed params', () => {
|
||||
const params = new Params();
|
||||
parse(params, '2147483648');
|
||||
assert.deepEqual(params.toArray(), [0x7FFFFFFF]);
|
||||
});
|
||||
it('clamp parsed subparams', () => {
|
||||
const params = new Params();
|
||||
parse(params, ':2147483648');
|
||||
assert.deepEqual(params.toArray(), [0, [0x7FFFFFFF]]);
|
||||
});
|
||||
});
|
||||
describe('issue 2389', () => {
|
||||
it('should cancel subdigits if beyond params limit', () => {
|
||||
const params = new Params();
|
||||
parse(params, ';;;;;;;;;10;;;;;;;;;;20;;;;;;;;;;30;31;32;33;34;35::::::::');
|
||||
assert.deepEqual(params.toArray(), [
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 10,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 20,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 30, 31, 32]);
|
||||
});
|
||||
it('should carry forward isSub state', () => {
|
||||
const params = new Params();
|
||||
parse(params, ['1:22:33', '44']);
|
||||
assert.deepEqual(params.toArray(), [1, [22, 3344]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,248 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
import { IParams, ParamsArray } from './Types';
|
||||
|
||||
const enum Constants {
|
||||
/**
|
||||
* Max value supported for a single param/subparam (clamped to positive int32 range)
|
||||
*/
|
||||
MAX_VALUE = 0x7FFFFFFF,
|
||||
/**
|
||||
* Max allowed subparams for a single sequence (hardcoded limitation)
|
||||
*/
|
||||
MAX_SUBPARAMS = 256
|
||||
}
|
||||
|
||||
/**
|
||||
* Params storage class.
|
||||
* This type is used by the parser to accumulate sequence parameters and sub parameters
|
||||
* and transmit them to the input handler actions.
|
||||
*
|
||||
* NOTES:
|
||||
* - params object for action handlers is borrowed, use `.toArray` or `.clone` to get a copy
|
||||
* - never read beyond `params.length - 1` (likely to contain arbitrary data)
|
||||
* - `.getSubParams` returns a borrowed typed array, use `.getSubParamsAll` for cloned sub params
|
||||
* - hardcoded limitations:
|
||||
* - max. value for a single (sub) param is 2^31 - 1 (greater values are clamped to that)
|
||||
* - max. 256 sub params possible
|
||||
* - negative values are not allowed beside -1 (placeholder for default value)
|
||||
*
|
||||
* About ZDM (Zero Default Mode):
|
||||
* ZDM is not orchestrated by this class. If the parser is in ZDM,
|
||||
* it should add 0 for empty params, otherwise -1. This does not apply
|
||||
* to subparams, empty subparams should always be added with -1.
|
||||
*/
|
||||
export class Params implements IParams {
|
||||
// params store and length
|
||||
public params: Int32Array;
|
||||
public length: number;
|
||||
|
||||
// sub params store and length
|
||||
protected _subParams: Int32Array;
|
||||
protected _subParamsLength: number;
|
||||
|
||||
// sub params offsets from param: param idx --> [start, end] offset
|
||||
private _subParamsIdx: Uint16Array;
|
||||
private _rejectDigits: boolean;
|
||||
private _rejectSubDigits: boolean;
|
||||
private _digitIsSub: boolean;
|
||||
|
||||
/**
|
||||
* Create a `Params` type from JS array representation.
|
||||
*/
|
||||
public static fromArray(values: ParamsArray): Params {
|
||||
const params = new Params();
|
||||
if (!values.length) {
|
||||
return params;
|
||||
}
|
||||
// skip leading sub params
|
||||
for (let i = (Array.isArray(values[0])) ? 1 : 0; i < values.length; ++i) {
|
||||
const value = values[i];
|
||||
if (Array.isArray(value)) {
|
||||
for (let k = 0; k < value.length; ++k) {
|
||||
params.addSubParam(value[k]);
|
||||
}
|
||||
} else {
|
||||
params.addParam(value);
|
||||
}
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param maxLength max length of storable parameters
|
||||
* @param maxSubParamsLength max length of storable sub parameters
|
||||
*/
|
||||
constructor(public maxLength: number = 32, public maxSubParamsLength: number = 32) {
|
||||
if (maxSubParamsLength > Constants.MAX_SUBPARAMS) {
|
||||
throw new Error('maxSubParamsLength must not be greater than 256');
|
||||
}
|
||||
this.params = new Int32Array(maxLength);
|
||||
this.length = 0;
|
||||
this._subParams = new Int32Array(maxSubParamsLength);
|
||||
this._subParamsLength = 0;
|
||||
this._subParamsIdx = new Uint16Array(maxLength);
|
||||
this._rejectDigits = false;
|
||||
this._rejectSubDigits = false;
|
||||
this._digitIsSub = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone object.
|
||||
*/
|
||||
public clone(): Params {
|
||||
const newParams = new Params(this.maxLength, this.maxSubParamsLength);
|
||||
newParams.params.set(this.params);
|
||||
newParams.length = this.length;
|
||||
newParams._subParams.set(this._subParams);
|
||||
newParams._subParamsLength = this._subParamsLength;
|
||||
newParams._subParamsIdx.set(this._subParamsIdx);
|
||||
newParams._rejectDigits = this._rejectDigits;
|
||||
newParams._rejectSubDigits = this._rejectSubDigits;
|
||||
newParams._digitIsSub = this._digitIsSub;
|
||||
return newParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a JS array representation of the current parameters and sub parameters.
|
||||
* The array is structured as follows:
|
||||
* sequence: "1;2:3:4;5::6"
|
||||
* array : [1, 2, [3, 4], 5, [-1, 6]]
|
||||
*/
|
||||
public toArray(): ParamsArray {
|
||||
const res: ParamsArray = [];
|
||||
for (let i = 0; i < this.length; ++i) {
|
||||
res.push(this.params[i]);
|
||||
const start = this._subParamsIdx[i] >> 8;
|
||||
const end = this._subParamsIdx[i] & 0xFF;
|
||||
if (end - start > 0) {
|
||||
res.push(Array.prototype.slice.call(this._subParams, start, end));
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset to initial empty state.
|
||||
*/
|
||||
public reset(): void {
|
||||
this.length = 0;
|
||||
this._subParamsLength = 0;
|
||||
this._rejectDigits = false;
|
||||
this._rejectSubDigits = false;
|
||||
this._digitIsSub = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset and add 0 as first param (ZDM).
|
||||
*/
|
||||
public resetZdm(): void {
|
||||
this.length = 1;
|
||||
this._subParamsLength = 0;
|
||||
this._rejectDigits = false;
|
||||
this._rejectSubDigits = false;
|
||||
this._digitIsSub = false;
|
||||
this._subParamsIdx[0] = 0;
|
||||
this.params[0] = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a parameter value.
|
||||
* `Params` only stores up to `maxLength` parameters, any later
|
||||
* parameter will be ignored.
|
||||
* Note: VT devices only stored up to 16 values, xterm seems to
|
||||
* store up to 30.
|
||||
*/
|
||||
public addParam(value: number): void {
|
||||
this._digitIsSub = false;
|
||||
if (this.length >= this.maxLength) {
|
||||
this._rejectDigits = true;
|
||||
return;
|
||||
}
|
||||
if (value < -1) {
|
||||
throw new Error('values less than -1 are not allowed');
|
||||
}
|
||||
this._subParamsIdx[this.length] = this._subParamsLength << 8 | this._subParamsLength;
|
||||
this.params[this.length++] = value > Constants.MAX_VALUE ? Constants.MAX_VALUE : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a sub parameter value.
|
||||
* The sub parameter is automatically associated with the last parameter value.
|
||||
* Thus it is not possible to add a subparameter without any parameter added yet.
|
||||
* `Params` only stores up to `maxSubParamsLength` sub parameters, any later
|
||||
* sub parameter will be ignored.
|
||||
*/
|
||||
public addSubParam(value: number): void {
|
||||
this._digitIsSub = true;
|
||||
if (!this.length) {
|
||||
return;
|
||||
}
|
||||
if (this._rejectDigits || this._subParamsLength >= this.maxSubParamsLength) {
|
||||
this._rejectSubDigits = true;
|
||||
return;
|
||||
}
|
||||
if (value < -1) {
|
||||
throw new Error('values less than -1 are not allowed');
|
||||
}
|
||||
this._subParams[this._subParamsLength++] = value > Constants.MAX_VALUE ? Constants.MAX_VALUE : value;
|
||||
this._subParamsIdx[this.length - 1]++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether parameter at index `idx` has sub parameters.
|
||||
*/
|
||||
public hasSubParams(idx: number): boolean {
|
||||
return ((this._subParamsIdx[idx] & 0xFF) - (this._subParamsIdx[idx] >> 8) > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return sub parameters for parameter at index `idx`.
|
||||
* Note: The values are borrowed, thus you need to copy
|
||||
* the values if you need to hold them in nonlocal scope.
|
||||
*/
|
||||
public getSubParams(idx: number): Int32Array | null {
|
||||
const start = this._subParamsIdx[idx] >> 8;
|
||||
const end = this._subParamsIdx[idx] & 0xFF;
|
||||
if (end - start > 0) {
|
||||
return this._subParams.subarray(start, end);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all sub parameters as {idx: subparams} mapping.
|
||||
* Note: The values are not borrowed.
|
||||
*/
|
||||
public getSubParamsAll(): {[idx: number]: Int32Array} {
|
||||
const result: {[idx: number]: Int32Array} = {};
|
||||
for (let i = 0; i < this.length; ++i) {
|
||||
const start = this._subParamsIdx[i] >> 8;
|
||||
const end = this._subParamsIdx[i] & 0xFF;
|
||||
if (end - start > 0) {
|
||||
result[i] = this._subParams.slice(start, end);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a single digit value to current parameter.
|
||||
* This is used by the parser to account digits on a char by char basis.
|
||||
*/
|
||||
public addDigit(value: number): void {
|
||||
let length;
|
||||
if (this._rejectDigits
|
||||
|| !(length = this._digitIsSub ? this._subParamsLength : this.length)
|
||||
|| (this._digitIsSub && this._rejectSubDigits)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const store = this._digitIsSub ? this._subParams : this.params;
|
||||
const cur = store[length - 1];
|
||||
store[length - 1] = ~cur ? Math.min(cur * 10 + value, Constants.MAX_VALUE) : value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable, IParams, ParamsArray } from '../../Types';
|
||||
import { ParserState } from './Constants';
|
||||
|
||||
export type { IParams, ParamsArray };
|
||||
|
||||
/** Params constructor type. */
|
||||
export interface IParamsConstructor {
|
||||
new(maxLength: number, maxSubParamsLength: number): IParams;
|
||||
|
||||
/** create params from ParamsArray */
|
||||
fromArray(values: ParamsArray): IParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal state of EscapeSequenceParser.
|
||||
* Used as argument of the error handler to allow
|
||||
* introspection at runtime on parse errors.
|
||||
* Return it with altered values to recover from
|
||||
* faulty states (not yet supported).
|
||||
* Set `abort` to `true` to abort the current parsing.
|
||||
*/
|
||||
export interface IParsingState {
|
||||
// position in parse string
|
||||
position: number;
|
||||
// actual character code
|
||||
code: number;
|
||||
// current parser state
|
||||
currentState: ParserState;
|
||||
// collect buffer with intermediate characters
|
||||
collect: number;
|
||||
// params buffer
|
||||
params: IParams;
|
||||
// should abort (default: false)
|
||||
abort: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Command handler interfaces.
|
||||
*/
|
||||
|
||||
/**
|
||||
* CSI handler types.
|
||||
* Note: `params` is borrowed.
|
||||
*/
|
||||
export type CsiHandlerType = (params: IParams) => boolean | Promise<boolean>;
|
||||
export type CsiFallbackHandlerType = (ident: number, params: IParams) => void;
|
||||
|
||||
/**
|
||||
* DCS handler types.
|
||||
*/
|
||||
export interface IDcsHandler {
|
||||
/**
|
||||
* Called when a DCS command starts.
|
||||
* Prepare needed data structures here.
|
||||
* Note: `params` is borrowed.
|
||||
*/
|
||||
hook(params: IParams): void;
|
||||
/**
|
||||
* Incoming payload chunk.
|
||||
* Note: `params` is borrowed.
|
||||
*/
|
||||
put(data: Uint32Array, start: number, end: number): void;
|
||||
/**
|
||||
* End of DCS command. `success` indicates whether the
|
||||
* command finished normally or got aborted, thus final
|
||||
* execution of the command should depend on `success`.
|
||||
* To save memory also cleanup data structures here.
|
||||
*/
|
||||
unhook(success: boolean): boolean | Promise<boolean>;
|
||||
}
|
||||
export type DcsFallbackHandlerType = (ident: number, action: 'HOOK' | 'PUT' | 'UNHOOK', payload?: any) => void;
|
||||
|
||||
/**
|
||||
* ESC handler types.
|
||||
*/
|
||||
export type EscHandlerType = () => boolean | Promise<boolean>;
|
||||
export type EscFallbackHandlerType = (identifier: number) => void;
|
||||
|
||||
/**
|
||||
* EXECUTE handler types.
|
||||
*/
|
||||
export type ExecuteHandlerType = (ident?: number) => boolean;
|
||||
export type ExecuteFallbackHandlerType = (ident: number) => void;
|
||||
|
||||
/**
|
||||
* OSC handler types.
|
||||
*/
|
||||
export interface IOscHandler {
|
||||
/**
|
||||
* Announces start of this OSC command.
|
||||
* Prepare needed data structures here.
|
||||
*/
|
||||
start(): void;
|
||||
/**
|
||||
* Incoming data chunk.
|
||||
* Note: Data is borrowed.
|
||||
*/
|
||||
put(data: Uint32Array, start: number, end: number): void;
|
||||
/**
|
||||
* End of OSC command. `success` indicates whether the
|
||||
* command finished normally or got aborted, thus final
|
||||
* execution of the command should depend on `success`.
|
||||
* To save memory also cleanup data structures here.
|
||||
*/
|
||||
end(success: boolean): boolean | Promise<boolean>;
|
||||
}
|
||||
export type OscFallbackHandlerType = (ident: number, action: 'START' | 'PUT' | 'END', payload?: any) => void;
|
||||
|
||||
/**
|
||||
* APC handler types.
|
||||
*/
|
||||
export interface IApcHandler {
|
||||
/**
|
||||
* Announces start of this APC command.
|
||||
* Prepare needed data structures here.
|
||||
*/
|
||||
start(): void;
|
||||
/**
|
||||
* Incoming data chunk.
|
||||
*/
|
||||
put(data: Uint32Array, start: number, end: number): void;
|
||||
/**
|
||||
* End of APC command. `success` indicates whether the
|
||||
* command finished normally or got aborted, thus final
|
||||
* execution of the command should depend on `success`.
|
||||
* To save memory also cleanup data structures here.
|
||||
*/
|
||||
end(success: boolean): boolean | Promise<boolean>;
|
||||
}
|
||||
export type ApcFallbackHandlerType = (ident: number, action: 'START' | 'PUT' | 'END', payload?: any) => void;
|
||||
|
||||
/**
|
||||
* PRINT handler types.
|
||||
*/
|
||||
export type PrintHandlerType = (data: Uint32Array, start: number, end: number) => void;
|
||||
export type PrintFallbackHandlerType = PrintHandlerType;
|
||||
|
||||
|
||||
/**
|
||||
* EscapeSequenceParser interface.
|
||||
*/
|
||||
export interface IEscapeSequenceParser extends IDisposable {
|
||||
/**
|
||||
* Preceding grapheme-join-state.
|
||||
* Used for joining grapheme clusters across calls to `print`.
|
||||
* Also used by REP to check if repeating a character is allowed.
|
||||
* It gets reset by the parser for any valid sequence besides text.
|
||||
*/
|
||||
precedingJoinState: number; // More specifically: UnicodeJoinProperties
|
||||
|
||||
/**
|
||||
* Reset the parser to its initial state (handlers are kept).
|
||||
*/
|
||||
reset(): void;
|
||||
|
||||
/**
|
||||
* Parse UTF32 codepoints in `data` up to `length`.
|
||||
* @param data The data to parse.
|
||||
*/
|
||||
parse(data: Uint32Array, length: number, promiseResult?: boolean): void | Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Get string from numercial function identifier `ident`.
|
||||
* Useful in fallback handlers which expose the low level
|
||||
* numcerical function identifier for debugging purposes.
|
||||
* Note: A full back translation to `IFunctionIdentifier`
|
||||
* is not implemented.
|
||||
*/
|
||||
identToString(ident: number): string;
|
||||
|
||||
setPrintHandler(handler: PrintHandlerType): void;
|
||||
clearPrintHandler(): void;
|
||||
|
||||
registerEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): IDisposable;
|
||||
clearEscHandler(id: IFunctionIdentifier): void;
|
||||
setEscHandlerFallback(handler: EscFallbackHandlerType): void;
|
||||
|
||||
setExecuteHandler(flag: string, handler: ExecuteHandlerType): void;
|
||||
clearExecuteHandler(flag: string): void;
|
||||
setExecuteHandlerFallback(handler: ExecuteFallbackHandlerType): void;
|
||||
|
||||
registerCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): IDisposable;
|
||||
clearCsiHandler(id: IFunctionIdentifier): void;
|
||||
setCsiHandlerFallback(callback: CsiFallbackHandlerType): void;
|
||||
|
||||
registerDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable;
|
||||
clearDcsHandler(id: IFunctionIdentifier): void;
|
||||
setDcsHandlerFallback(handler: DcsFallbackHandlerType): void;
|
||||
|
||||
registerOscHandler(ident: number, handler: IOscHandler): IDisposable;
|
||||
clearOscHandler(ident: number): void;
|
||||
setOscHandlerFallback(handler: OscFallbackHandlerType): void;
|
||||
|
||||
registerApcHandler(id: IFunctionIdentifier, handler: IApcHandler): IDisposable;
|
||||
clearApcHandler(id: IFunctionIdentifier): void;
|
||||
setApcHandlerFallback(handler: ApcFallbackHandlerType): void;
|
||||
|
||||
setErrorHandler(handler: (state: IParsingState) => IParsingState): void;
|
||||
clearErrorHandler(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subparser interfaces.
|
||||
* The subparsers are instantiated in `EscapeSequenceParser` and
|
||||
* called during `EscapeSequenceParser.parse`.
|
||||
*/
|
||||
export interface ISubParser<T, U> extends IDisposable {
|
||||
reset(): void;
|
||||
registerHandler(ident: number, handler: T): IDisposable;
|
||||
clearHandler(ident: number): void;
|
||||
setHandlerFallback(handler: U): void;
|
||||
put(data: Uint32Array, start: number, end: number): void;
|
||||
}
|
||||
|
||||
export interface IOscParser extends ISubParser<IOscHandler, OscFallbackHandlerType> {
|
||||
start(): void;
|
||||
end(success: boolean, promiseResult?: boolean): void | Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface IDcsParser extends ISubParser<IDcsHandler, DcsFallbackHandlerType> {
|
||||
hook(ident: number, params: IParams): void;
|
||||
unhook(success: boolean, promiseResult?: boolean): void | Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface IApcParser extends ISubParser<IApcHandler, ApcFallbackHandlerType> {
|
||||
start(ident: number): void;
|
||||
end(success: boolean, promiseResult?: boolean): void | Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface to denote a specific ESC, CSI or DCS handler slot.
|
||||
* The values are used to create an integer representation during handler
|
||||
* registration before passed to the subparsers as `ident`.
|
||||
* The integer translation is made to allow a faster handler access
|
||||
* in `EscapeSequenceParser.parse`.
|
||||
*/
|
||||
export interface IFunctionIdentifier {
|
||||
prefix?: string;
|
||||
intermediates?: string;
|
||||
final: string;
|
||||
}
|
||||
|
||||
export interface IHandlerCollection<T> {
|
||||
[key: string]: T[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Types for async parser support.
|
||||
*/
|
||||
|
||||
// type of saved stack state in parser
|
||||
export const enum ParserStackType {
|
||||
NONE = 0,
|
||||
FAIL,
|
||||
RESET,
|
||||
CSI,
|
||||
ESC,
|
||||
OSC,
|
||||
DCS,
|
||||
APC
|
||||
}
|
||||
|
||||
// aggregate of resumable handler lists
|
||||
export type ResumableHandlersType = CsiHandlerType[] | EscHandlerType[];
|
||||
|
||||
// saved stack state of the parser
|
||||
export interface IParserStackState {
|
||||
state: ParserStackType;
|
||||
handlers: ResumableHandlersType;
|
||||
handlerPos: number;
|
||||
transition: number;
|
||||
chunkPos: number;
|
||||
}
|
||||
|
||||
// saved stack state of subparser (OSC and DCS)
|
||||
export interface ISubParserStackState {
|
||||
paused: boolean;
|
||||
loopPosition: number;
|
||||
fallThrough: boolean;
|
||||
}
|
||||
Reference in New Issue
Block a user