async parser tests for CSI and ESC
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IParsingState, IParams, ParamsArray, IOscParser, IOscHandler, OscFallbackHandlerType, IFunctionIdentifier } from 'common/parser/Types';
|
||||
import { IParsingState, IParams, ParamsArray, IOscParser, IOscHandler, OscFallbackHandlerType, IFunctionIdentifier, IParserStackState, ParserStackType, ResumableHandlersType } from 'common/parser/Types';
|
||||
import { EscapeSequenceParser, TransitionTable, VT500_TRANSITION_TABLE } from 'common/parser/EscapeSequenceParser';
|
||||
import { assert } from 'chai';
|
||||
import { StringToUtf32, stringFromCodePoint, utf32ToString } from 'common/input/TextDecoder';
|
||||
@@ -24,7 +24,7 @@ function r(a: number, b: number): string[] {
|
||||
}
|
||||
|
||||
class MockOscPutParser implements IOscParser {
|
||||
private _fallback: OscFallbackHandlerType = () => {};
|
||||
private _fallback: OscFallbackHandlerType = () => { };
|
||||
public data = '';
|
||||
public reset(): void {
|
||||
this.data = '';
|
||||
@@ -92,6 +92,21 @@ class TestEscapeSequenceParser extends EscapeSequenceParser {
|
||||
public identifier(id: IFunctionIdentifier): number {
|
||||
return this._identifier(id);
|
||||
}
|
||||
public get parseStack(): IParserStackState {
|
||||
return this._parseStack;
|
||||
}
|
||||
private _trackStack = false;
|
||||
public trackStackSavesOnPause(): void {
|
||||
this._trackStack = true;
|
||||
}
|
||||
public trackedStack: IParserStackState[] = [];
|
||||
public parse(data: Uint32Array, length: number, promiseResult?: boolean): void | Promise<boolean> {
|
||||
const result = super.parse(data, length, promiseResult);
|
||||
if (result && this._trackStack) {
|
||||
this.trackedStack.push({ ...this.parseStack });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// test object to collect parser actions and compare them with expected values
|
||||
@@ -1206,71 +1221,71 @@ describe('EscapeSequenceParser', () => {
|
||||
assert.equal(print, '');
|
||||
});
|
||||
it('ESC handler', () => {
|
||||
parser2.registerEscHandler({intermediates: '%', final: 'G'}, function (): boolean {
|
||||
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, function (): boolean {
|
||||
esc.push('%G');
|
||||
return true;
|
||||
});
|
||||
parser2.registerEscHandler({final: 'E'}, function (): boolean {
|
||||
parser2.registerEscHandler({ final: 'E' }, function (): boolean {
|
||||
esc.push('E');
|
||||
return true;
|
||||
});
|
||||
parse(parser2, INPUT);
|
||||
assert.deepEqual(esc, ['%G', 'E']);
|
||||
parser2.clearEscHandler({intermediates: '%', final: 'G'});
|
||||
parser2.clearEscHandler({intermediates: '%', final: 'G'}); // should not throw
|
||||
parser2.clearEscHandler({ intermediates: '%', final: 'G' });
|
||||
parser2.clearEscHandler({ intermediates: '%', final: 'G' }); // should not throw
|
||||
clearAccu();
|
||||
parse(parser2, INPUT);
|
||||
assert.deepEqual(esc, ['E']);
|
||||
parser2.clearEscHandler({final: 'E'});
|
||||
parser2.clearEscHandler({ final: 'E' });
|
||||
clearAccu();
|
||||
parse(parser2, INPUT);
|
||||
assert.deepEqual(esc, []);
|
||||
});
|
||||
describe('ESC custom handlers', () => {
|
||||
it('prevent fallback', () => {
|
||||
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); return true; });
|
||||
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; });
|
||||
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('default - %G'); return true; });
|
||||
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom - %G'); return true; });
|
||||
parse(parser2, INPUT);
|
||||
assert.deepEqual(esc, ['custom - %G']);
|
||||
});
|
||||
it('allow fallback', () => {
|
||||
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); return true; });
|
||||
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return false; });
|
||||
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('default - %G'); return true; });
|
||||
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom - %G'); return false; });
|
||||
parse(parser2, INPUT);
|
||||
assert.deepEqual(esc, ['custom - %G', 'default - %G']);
|
||||
});
|
||||
it('Multiple custom handlers fallback once', () => {
|
||||
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); return true; });
|
||||
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; });
|
||||
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom2 - %G'); return false; });
|
||||
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('default - %G'); return true; });
|
||||
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom - %G'); return true; });
|
||||
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom2 - %G'); return false; });
|
||||
parse(parser2, INPUT);
|
||||
assert.deepEqual(esc, ['custom2 - %G', 'custom - %G']);
|
||||
});
|
||||
it('Multiple custom handlers no fallback', () => {
|
||||
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); return true; });
|
||||
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; });
|
||||
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom2 - %G'); return true; });
|
||||
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('default - %G'); return true; });
|
||||
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom - %G'); return true; });
|
||||
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom2 - %G'); return true; });
|
||||
parse(parser2, INPUT);
|
||||
assert.deepEqual(esc, ['custom2 - %G']);
|
||||
});
|
||||
it('Execution order should go from latest handler down to the original', () => {
|
||||
const order: number[] = [];
|
||||
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { order.push(1); return true; });
|
||||
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { order.push(2); return false; });
|
||||
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { order.push(3); return false; });
|
||||
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { order.push(1); return true; });
|
||||
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { order.push(2); return false; });
|
||||
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { order.push(3); return false; });
|
||||
parse(parser2, '\x1b%G');
|
||||
assert.deepEqual(order, [3, 2, 1]);
|
||||
});
|
||||
it('Dispose should work', () => {
|
||||
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); return true; });
|
||||
const dispo = parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; });
|
||||
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('default - %G'); return true; });
|
||||
const dispo = parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom - %G'); return true; });
|
||||
dispo.dispose();
|
||||
parse(parser2, INPUT);
|
||||
assert.deepEqual(esc, ['default - %G']);
|
||||
});
|
||||
it('Should not corrupt the parser when dispose is called twice', () => {
|
||||
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); return true; });
|
||||
const dispo = parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; });
|
||||
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('default - %G'); return true; });
|
||||
const dispo = parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom - %G'); return true; });
|
||||
dispo.dispose();
|
||||
dispo.dispose();
|
||||
parse(parser2, INPUT);
|
||||
@@ -1278,14 +1293,14 @@ describe('EscapeSequenceParser', () => {
|
||||
});
|
||||
});
|
||||
it('CSI handler', () => {
|
||||
parser2.registerCsiHandler({final: 'm'}, function (params: IParams): boolean {
|
||||
parser2.registerCsiHandler({ final: 'm' }, function (params: IParams): boolean {
|
||||
csi.push(['m', params.toArray(), '']);
|
||||
return true;
|
||||
});
|
||||
parse(parser2, INPUT);
|
||||
assert.deepEqual(csi, [['m', [1, 31], ''], ['m', [0], '']]);
|
||||
parser2.clearCsiHandler({final: 'm'});
|
||||
parser2.clearCsiHandler({final: 'm'}); // should not throw
|
||||
parser2.clearCsiHandler({ final: 'm' });
|
||||
parser2.clearCsiHandler({ final: 'm' }); // should not throw
|
||||
clearAccu();
|
||||
parse(parser2, INPUT);
|
||||
assert.deepEqual(csi, []);
|
||||
@@ -1293,16 +1308,16 @@ describe('EscapeSequenceParser', () => {
|
||||
describe('CSI custom handlers', () => {
|
||||
it('Prevent fallback', () => {
|
||||
const csiCustom: [string, ParamsArray, string][] = [];
|
||||
parser2.registerCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); return true; });
|
||||
parser2.registerCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
|
||||
parser2.registerCsiHandler({ final: 'm' }, params => { csi.push(['m', params.toArray(), '']); return true; });
|
||||
parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
|
||||
parse(parser2, INPUT);
|
||||
assert.deepEqual(csi, [], 'Should not fallback to original handler');
|
||||
assert.deepEqual(csiCustom, [['m', [1, 31], ''], ['m', [0], '']]);
|
||||
});
|
||||
it('Allow fallback', () => {
|
||||
const csiCustom: [string, ParamsArray, string][] = [];
|
||||
parser2.registerCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); return true; });
|
||||
parser2.registerCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return false; });
|
||||
parser2.registerCsiHandler({ final: 'm' }, params => { csi.push(['m', params.toArray(), '']); return true; });
|
||||
parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom.push(['m', params.toArray(), '']); return false; });
|
||||
parse(parser2, INPUT);
|
||||
assert.deepEqual(csi, [['m', [1, 31], ''], ['m', [0], '']], 'Should fallback to original handler');
|
||||
assert.deepEqual(csiCustom, [['m', [1, 31], ''], ['m', [0], '']]);
|
||||
@@ -1310,9 +1325,9 @@ describe('EscapeSequenceParser', () => {
|
||||
it('Multiple custom handlers fallback once', () => {
|
||||
const csiCustom: [string, ParamsArray, string][] = [];
|
||||
const csiCustom2: [string, ParamsArray, string][] = [];
|
||||
parser2.registerCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); return true; });
|
||||
parser2.registerCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
|
||||
parser2.registerCsiHandler({final: 'm'}, params => { csiCustom2.push(['m', params.toArray(), '']); return false; });
|
||||
parser2.registerCsiHandler({ final: 'm' }, params => { csi.push(['m', params.toArray(), '']); return true; });
|
||||
parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
|
||||
parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom2.push(['m', params.toArray(), '']); return false; });
|
||||
parse(parser2, INPUT);
|
||||
assert.deepEqual(csi, [], 'Should not fallback to original handler');
|
||||
assert.deepEqual(csiCustom, [['m', [1, 31], ''], ['m', [0], '']]);
|
||||
@@ -1321,9 +1336,9 @@ describe('EscapeSequenceParser', () => {
|
||||
it('Multiple custom handlers no fallback', () => {
|
||||
const csiCustom: [string, ParamsArray, string][] = [];
|
||||
const csiCustom2: [string, ParamsArray, string][] = [];
|
||||
parser2.registerCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); return true; });
|
||||
parser2.registerCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
|
||||
parser2.registerCsiHandler({final: 'm'}, params => { csiCustom2.push(['m', params.toArray(), '']); return true; });
|
||||
parser2.registerCsiHandler({ final: 'm' }, params => { csi.push(['m', params.toArray(), '']); return true; });
|
||||
parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
|
||||
parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom2.push(['m', params.toArray(), '']); return true; });
|
||||
parse(parser2, INPUT);
|
||||
assert.deepEqual(csi, [], 'Should not fallback to original handler');
|
||||
assert.deepEqual(csiCustom, [], 'Should not fallback once');
|
||||
@@ -1331,16 +1346,16 @@ describe('EscapeSequenceParser', () => {
|
||||
});
|
||||
it('Execution order should go from latest handler down to the original', () => {
|
||||
const order: number[] = [];
|
||||
parser2.registerCsiHandler({final: 'm'}, () => { order.push(1); return true; });
|
||||
parser2.registerCsiHandler({final: 'm'}, () => { order.push(2); return false; });
|
||||
parser2.registerCsiHandler({final: 'm'}, () => { order.push(3); return false; });
|
||||
parser2.registerCsiHandler({ final: 'm' }, () => { order.push(1); return true; });
|
||||
parser2.registerCsiHandler({ final: 'm' }, () => { order.push(2); return false; });
|
||||
parser2.registerCsiHandler({ final: 'm' }, () => { order.push(3); return false; });
|
||||
parse(parser2, '\x1b[0m');
|
||||
assert.deepEqual(order, [3, 2, 1]);
|
||||
});
|
||||
it('Dispose should work', () => {
|
||||
const csiCustom: [string, ParamsArray, string][] = [];
|
||||
parser2.registerCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); return true; });
|
||||
const customHandler = parser2.registerCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
|
||||
parser2.registerCsiHandler({ final: 'm' }, params => { csi.push(['m', params.toArray(), '']); return true; });
|
||||
const customHandler = parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
|
||||
customHandler.dispose();
|
||||
parse(parser2, INPUT);
|
||||
assert.deepEqual(csi, [['m', [1, 31], ''], ['m', [0], '']]);
|
||||
@@ -1348,8 +1363,8 @@ describe('EscapeSequenceParser', () => {
|
||||
});
|
||||
it('Should not corrupt the parser when dispose is called twice', () => {
|
||||
const csiCustom: [string, ParamsArray, string][] = [];
|
||||
parser2.registerCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); return true; });
|
||||
const customHandler = parser2.registerCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
|
||||
parser2.registerCsiHandler({ final: 'm' }, params => { csi.push(['m', params.toArray(), '']); return true; });
|
||||
const customHandler = parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
|
||||
customHandler.dispose();
|
||||
customHandler.dispose();
|
||||
parse(parser2, INPUT);
|
||||
@@ -1455,7 +1470,7 @@ describe('EscapeSequenceParser', () => {
|
||||
});
|
||||
});
|
||||
it('DCS handler', () => {
|
||||
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, {
|
||||
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, {
|
||||
hook: function (params: IParams): void {
|
||||
dcs.push(['hook', '', params.toArray(), 0]);
|
||||
},
|
||||
@@ -1478,8 +1493,8 @@ describe('EscapeSequenceParser', () => {
|
||||
['put', 'abc'], ['put', ';de'],
|
||||
['unhook']
|
||||
]);
|
||||
parser2.clearDcsHandler({intermediates: '+', final: 'p'});
|
||||
parser2.clearDcsHandler({intermediates: '+', final: 'p'}); // should not throw
|
||||
parser2.clearDcsHandler({ intermediates: '+', final: 'p' });
|
||||
parser2.clearDcsHandler({ intermediates: '+', final: 'p' }); // should not throw
|
||||
clearAccu();
|
||||
parse(parser2, '\x1bP1;2;3+pabc');
|
||||
parse(parser2, ';de\x9c');
|
||||
@@ -1489,54 +1504,54 @@ describe('EscapeSequenceParser', () => {
|
||||
const DCS_INPUT = '\x1bP1;2;3+pabc\x1b\\';
|
||||
it('Prevent fallback', () => {
|
||||
const dcsCustom: [string, (number | number[])[], string][] = [];
|
||||
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
|
||||
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
|
||||
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
|
||||
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
|
||||
parse(parser2, DCS_INPUT);
|
||||
assert.deepEqual(dcsCustom, [['B', [1, 2, 3], 'abc']]);
|
||||
});
|
||||
it('Allow fallback', () => {
|
||||
const dcsCustom: [string, (number | number[])[], string][] = [];
|
||||
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
|
||||
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return false; }));
|
||||
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
|
||||
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return false; }));
|
||||
parse(parser2, DCS_INPUT);
|
||||
assert.deepEqual(dcsCustom, [['B', [1, 2, 3], 'abc'], ['A', [1, 2, 3], 'abc']]);
|
||||
});
|
||||
it('Multiple custom handlers fallback once', () => {
|
||||
const dcsCustom: [string, (number | number[])[], string][] = [];
|
||||
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
|
||||
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
|
||||
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return false; }));
|
||||
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
|
||||
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
|
||||
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return false; }));
|
||||
parse(parser2, DCS_INPUT);
|
||||
assert.deepEqual(dcsCustom, [['C', [1, 2, 3], 'abc'], ['B', [1, 2, 3], 'abc']]);
|
||||
});
|
||||
it('Multiple custom handlers no fallback', () => {
|
||||
const dcsCustom: [string, (number | number[])[], string][] = [];
|
||||
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
|
||||
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
|
||||
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return true; }));
|
||||
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
|
||||
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
|
||||
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return true; }));
|
||||
parse(parser2, DCS_INPUT);
|
||||
assert.deepEqual(dcsCustom, [['C', [1, 2, 3], 'abc']]);
|
||||
});
|
||||
it('Execution order should go from latest handler down to the original', () => {
|
||||
const order: number[] = [];
|
||||
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler(() => { order.push(1); return true; }));
|
||||
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler(() => { order.push(2); return false; }));
|
||||
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler(() => { order.push(3); return false; }));
|
||||
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler(() => { order.push(1); return true; }));
|
||||
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler(() => { order.push(2); return false; }));
|
||||
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler(() => { order.push(3); return false; }));
|
||||
parse(parser2, DCS_INPUT);
|
||||
assert.deepEqual(order, [3, 2, 1]);
|
||||
});
|
||||
it('Dispose should work', () => {
|
||||
const dcsCustom: [string, (number | number[])[], string][] = [];
|
||||
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
|
||||
const dispo = parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
|
||||
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
|
||||
const dispo = parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
|
||||
dispo.dispose();
|
||||
parse(parser2, DCS_INPUT);
|
||||
assert.deepEqual(dcsCustom, [['A', [1, 2, 3], 'abc']]);
|
||||
});
|
||||
it('Should not corrupt the parser when dispose is called twice', () => {
|
||||
const dcsCustom: [string, (number | number[])[], string][] = [];
|
||||
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
|
||||
const dispo = parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
|
||||
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
|
||||
const dispo = parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
|
||||
dispo.dispose();
|
||||
dispo.dispose();
|
||||
parse(parser2, DCS_INPUT);
|
||||
@@ -1570,54 +1585,54 @@ describe('EscapeSequenceParser', () => {
|
||||
it('prefix range 0x3c .. 0x3f, one byte', () => {
|
||||
for (let i = 0x3c; i <= 0x3f; ++i) {
|
||||
const c = String.fromCharCode(i);
|
||||
assert.equal(parser.identToString(parser.identifier({prefix: c, final: 'z'})), c + 'z');
|
||||
assert.equal(parser.identToString(parser.identifier({ prefix: c, final: 'z' })), c + 'z');
|
||||
}
|
||||
assert.throws(() => { parser.identifier({prefix: '\x3b', final: 'z'}); }, 'prefix must be in range 0x3c .. 0x3f');
|
||||
assert.throws(() => { parser.identifier({prefix: '\x40', final: 'z'}); }, 'prefix must be in range 0x3c .. 0x3f');
|
||||
assert.throws(() => { parser.identifier({prefix: '??', final: 'z'}); }, 'only one byte as prefix supported');
|
||||
assert.throws(() => { parser.identifier({ prefix: '\x3b', final: 'z' }); }, 'prefix must be in range 0x3c .. 0x3f');
|
||||
assert.throws(() => { parser.identifier({ prefix: '\x40', final: 'z' }); }, 'prefix must be in range 0x3c .. 0x3f');
|
||||
assert.throws(() => { parser.identifier({ prefix: '??', final: 'z' }); }, 'only one byte as prefix supported');
|
||||
});
|
||||
it('intermediates range 0x20 .. 0x2f, up to two bytes', () => {
|
||||
for (let i = 0x20; i <= 0x2f; ++i) {
|
||||
const c = String.fromCharCode(i);
|
||||
assert.equal(parser.identToString(parser.identifier({intermediates: c + c, final: 'z'})), c + c + 'z');
|
||||
assert.equal(parser.identToString(parser.identifier({ intermediates: c + c, final: 'z' })), c + c + 'z');
|
||||
}
|
||||
assert.throws(() => { parser.identifier({intermediates: '\x1f', final: 'z'}); }, 'intermediate must be in range 0x20 .. 0x2f');
|
||||
assert.throws(() => { parser.identifier({intermediates: '\x30', final: 'z'}); }, 'intermediate must be in range 0x20 .. 0x2f');
|
||||
assert.throws(() => { parser.identifier({intermediates: '!!!', final: 'z'}); }, 'only two bytes as intermediates are supported');
|
||||
assert.throws(() => { parser.identifier({ intermediates: '\x1f', final: 'z' }); }, 'intermediate must be in range 0x20 .. 0x2f');
|
||||
assert.throws(() => { parser.identifier({ intermediates: '\x30', final: 'z' }); }, 'intermediate must be in range 0x20 .. 0x2f');
|
||||
assert.throws(() => { parser.identifier({ intermediates: '!!!', final: 'z' }); }, 'only two bytes as intermediates are supported');
|
||||
});
|
||||
it('final CSI/DCS range 0x40 .. 0x7e (default), one byte', () => {
|
||||
for (let i = 0x40; i <= 0x7e; ++i) {
|
||||
const c = String.fromCharCode(i);
|
||||
assert.equal(parser.identToString(parser.identifier({final: c})), c);
|
||||
assert.equal(parser.identToString(parser.identifier({ final: c })), c);
|
||||
}
|
||||
assert.throws(() => { parser.identifier({final: '\x3f'}); }, 'final must be in range 64 .. 126');
|
||||
assert.throws(() => { parser.identifier({final: '\x7f'}); }, 'final must be in range 64 .. 126');
|
||||
assert.throws(() => { parser.identifier({final: 'zz'}); }, 'final must be a single byte');
|
||||
assert.throws(() => { parser.identifier({ final: '\x3f' }); }, 'final must be in range 64 .. 126');
|
||||
assert.throws(() => { parser.identifier({ final: '\x7f' }); }, 'final must be in range 64 .. 126');
|
||||
assert.throws(() => { parser.identifier({ final: 'zz' }); }, 'final must be a single byte');
|
||||
});
|
||||
it('final ESC range 0x30 .. 0x7e, one byte', () => {
|
||||
for (let i = 0x30; i <= 0x7e; ++i) {
|
||||
const final = String.fromCharCode(i);
|
||||
let handler: IDisposable | undefined;
|
||||
assert.doesNotThrow(() => { handler = parser.registerEscHandler({final}, () => true); }, 'final must be in range 48 .. 126');
|
||||
assert.doesNotThrow(() => { handler = parser.registerEscHandler({ final }, () => true); }, 'final must be in range 48 .. 126');
|
||||
if (handler) handler.dispose();
|
||||
}
|
||||
assert.throws(() => { parser.registerEscHandler({final: '\x2f'}, () => true); }, 'final must be in range 48 .. 126');
|
||||
assert.throws(() => { parser.registerEscHandler({final: '\x7f'}, () => true); }, 'final must be in range 48 .. 126');
|
||||
assert.throws(() => { parser.registerEscHandler({ final: '\x2f' }, () => true); }, 'final must be in range 48 .. 126');
|
||||
assert.throws(() => { parser.registerEscHandler({ final: '\x7f' }, () => true); }, 'final must be in range 48 .. 126');
|
||||
});
|
||||
it('id calculation - should stacking prefix -> intermediate -> final', () => {
|
||||
assert.equal(parser.identToString(parser.identifier({final: 'z'})), 'z');
|
||||
assert.equal(parser.identToString(parser.identifier({prefix: '?', final: 'z'})), '?z');
|
||||
assert.equal(parser.identToString(parser.identifier({intermediates: '!', final: 'z'})), '!z');
|
||||
assert.equal(parser.identToString(parser.identifier({prefix: '?', intermediates: '!', final: 'z'})), '?!z');
|
||||
assert.equal(parser.identToString(parser.identifier({prefix: '?', intermediates: '!!', final: 'z'})), '?!!z');
|
||||
assert.equal(parser.identToString(parser.identifier({ final: 'z' })), 'z');
|
||||
assert.equal(parser.identToString(parser.identifier({ prefix: '?', final: 'z' })), '?z');
|
||||
assert.equal(parser.identToString(parser.identifier({ intermediates: '!', final: 'z' })), '!z');
|
||||
assert.equal(parser.identToString(parser.identifier({ prefix: '?', intermediates: '!', final: 'z' })), '?!z');
|
||||
assert.equal(parser.identToString(parser.identifier({ prefix: '?', intermediates: '!!', final: 'z' })), '?!!z');
|
||||
});
|
||||
});
|
||||
describe('identifier invocation', () => {
|
||||
it('ESC', () => {
|
||||
const callstack: string[] = [];
|
||||
const h1 = parser.registerEscHandler({final: 'z'}, () => { callstack.push('z'); return true; });
|
||||
const h2 = parser.registerEscHandler({intermediates: '!', final: 'z'}, () => { callstack.push('!z'); return true; });
|
||||
const h3 = parser.registerEscHandler({intermediates: '!!', final: 'z'}, () => { callstack.push('!!z'); return true; });
|
||||
const h1 = parser.registerEscHandler({ final: 'z' }, () => { callstack.push('z'); return true; });
|
||||
const h2 = parser.registerEscHandler({ intermediates: '!', final: 'z' }, () => { callstack.push('!z'); return true; });
|
||||
const h3 = parser.registerEscHandler({ intermediates: '!!', final: 'z' }, () => { callstack.push('!!z'); return true; });
|
||||
parse(parser, '\x1bz\x1b!z\x1b!!z');
|
||||
h1.dispose();
|
||||
h2.dispose();
|
||||
@@ -1627,12 +1642,12 @@ describe('EscapeSequenceParser', () => {
|
||||
});
|
||||
it('CSI', () => {
|
||||
const callstack: any[] = [];
|
||||
const h1 = parser.registerCsiHandler({final: 'z'}, params => { callstack.push(['z', params.toArray()]); return true; });
|
||||
const h2 = parser.registerCsiHandler({intermediates: '!', final: 'z'}, params => { callstack.push(['!z', params.toArray()]); return true; });
|
||||
const h3 = parser.registerCsiHandler({intermediates: '!!', final: 'z'}, params => { callstack.push(['!!z', params.toArray()]); return true; });
|
||||
const h4 = parser.registerCsiHandler({prefix: '?', final: 'z'}, params => { callstack.push(['?z', params.toArray()]); return true; });
|
||||
const h5 = parser.registerCsiHandler({prefix: '?', intermediates: '!', final: 'z'}, params => { callstack.push(['?!z', params.toArray()]); return true; });
|
||||
const h6 = parser.registerCsiHandler({prefix: '?', intermediates: '!!', final: 'z'}, params => { callstack.push(['?!!z', params.toArray()]); return true; });
|
||||
const h1 = parser.registerCsiHandler({ final: 'z' }, params => { callstack.push(['z', params.toArray()]); return true; });
|
||||
const h2 = parser.registerCsiHandler({ intermediates: '!', final: 'z' }, params => { callstack.push(['!z', params.toArray()]); return true; });
|
||||
const h3 = parser.registerCsiHandler({ intermediates: '!!', final: 'z' }, params => { callstack.push(['!!z', params.toArray()]); return true; });
|
||||
const h4 = parser.registerCsiHandler({ prefix: '?', final: 'z' }, params => { callstack.push(['?z', params.toArray()]); return true; });
|
||||
const h5 = parser.registerCsiHandler({ prefix: '?', intermediates: '!', final: 'z' }, params => { callstack.push(['?!z', params.toArray()]); return true; });
|
||||
const h6 = parser.registerCsiHandler({ prefix: '?', intermediates: '!!', final: 'z' }, params => { callstack.push(['?!!z', params.toArray()]); return true; });
|
||||
parse(parser, '\x1b[1;z\x1b[1;!z\x1b[1;!!z\x1b[?1;z\x1b[?1;!z\x1b[?1;!!z');
|
||||
h1.dispose();
|
||||
h2.dispose();
|
||||
@@ -1648,12 +1663,12 @@ describe('EscapeSequenceParser', () => {
|
||||
});
|
||||
it('DCS', () => {
|
||||
const callstack: any[] = [];
|
||||
const h1 = parser.registerDcsHandler({final: 'z'}, new DcsHandler((data, params) => { callstack.push(['z', params.toArray(), data]); return true; }));
|
||||
const h2 = parser.registerDcsHandler({intermediates: '!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['!z', params.toArray(), data]); return true; }));
|
||||
const h3 = parser.registerDcsHandler({intermediates: '!!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['!!z', params.toArray(), data]); return true; }));
|
||||
const h4 = parser.registerDcsHandler({prefix: '?', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['?z', params.toArray(), data]); return true; }));
|
||||
const h5 = parser.registerDcsHandler({prefix: '?', intermediates: '!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['?!z', params.toArray(), data]); return true; }));
|
||||
const h6 = parser.registerDcsHandler({prefix: '?', intermediates: '!!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['?!!z', params.toArray(), data]); return true; }));
|
||||
const h1 = parser.registerDcsHandler({ final: 'z' }, new DcsHandler((data, params) => { callstack.push(['z', params.toArray(), data]); return true; }));
|
||||
const h2 = parser.registerDcsHandler({ intermediates: '!', final: 'z' }, new DcsHandler((data, params) => { callstack.push(['!z', params.toArray(), data]); return true; }));
|
||||
const h3 = parser.registerDcsHandler({ intermediates: '!!', final: 'z' }, new DcsHandler((data, params) => { callstack.push(['!!z', params.toArray(), data]); return true; }));
|
||||
const h4 = parser.registerDcsHandler({ prefix: '?', final: 'z' }, new DcsHandler((data, params) => { callstack.push(['?z', params.toArray(), data]); return true; }));
|
||||
const h5 = parser.registerDcsHandler({ prefix: '?', intermediates: '!', final: 'z' }, new DcsHandler((data, params) => { callstack.push(['?!z', params.toArray(), data]); return true; }));
|
||||
const h6 = parser.registerDcsHandler({ prefix: '?', intermediates: '!!', final: 'z' }, new DcsHandler((data, params) => { callstack.push(['?!!z', params.toArray(), data]); return true; }));
|
||||
parse(parser, '\x1bP1;zAB\x1b\\\x1bP1;!zAB\x1b\\\x1bP1;!!zAB\x1b\\\x1bP?1;zAB\x1b\\\x1bP?1;!zAB\x1b\\\x1bP?1;!!zAB\x1b\\');
|
||||
h1.dispose();
|
||||
h2.dispose();
|
||||
@@ -1678,3 +1693,365 @@ describe('EscapeSequenceParser', () => {
|
||||
});
|
||||
// TODO: error conditions and error recovery (not implemented yet in parser)
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* async handler tests.
|
||||
*/
|
||||
|
||||
function parseSync(parser: TestEscapeSequenceParser, data: string): void | Promise<boolean> {
|
||||
const container = new Uint32Array(data.length);
|
||||
const decoder = new StringToUtf32();
|
||||
return parser.parse(container, decoder.decode(data, container));
|
||||
}
|
||||
async function parseP(parser: TestEscapeSequenceParser, data: string): Promise<void> {
|
||||
const container = new Uint32Array(data.length);
|
||||
const decoder = new StringToUtf32();
|
||||
const len = decoder.decode(data, container);
|
||||
let result: void | Promise<boolean>;
|
||||
let prev: boolean | undefined;
|
||||
while (result = parser.parse(container, len, prev)) {
|
||||
prev = await result;
|
||||
}
|
||||
}
|
||||
function evalStackSaves(stackSaves: IParserStackState[], data: [number, ParserStackType, number][]): void {
|
||||
assert.equal(stackSaves.length, data.length);
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
assert.equal(stackSaves[i].chunkPos, data[i][0]);
|
||||
assert.equal(stackSaves[i].state, data[i][1]);
|
||||
assert.equal(stackSaves[i].handlerPos, data[i][2]);
|
||||
}
|
||||
}
|
||||
// helper similiar to assert.throws for async functions
|
||||
async function throwsAsync(fn: () => Promise<any>, message?: string | undefined): Promise<void> {
|
||||
let msg: string | undefined;
|
||||
try {
|
||||
await fn();
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
msg = e.message;
|
||||
} else if (typeof e === 'string') {
|
||||
msg = e;
|
||||
}
|
||||
if (typeof message === 'string') {
|
||||
assert.equal(msg, message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
assert.throws(fn, message);
|
||||
}
|
||||
|
||||
describe('EscapeSequenceParser - async', () => {
|
||||
// sequences: SGR 1;31 | hello SP | ESC %G | wor | ESC E | ld! | SGR 0 | EXE \r\n | $> | OSC 1;foo=bar ST
|
||||
// needed handlers: CSI m, PRINT, ESC %G, ESC E, EXE \r, EXE \n, OSC 1
|
||||
const INPUT = '\x1b[1;31mhello \x1b%Gwor\x1bEld!\x1b[0m\r\n$>\x1b]1;foo=bar\x1b\\';
|
||||
let RESULT: any[];
|
||||
let parser: TestEscapeSequenceParser;
|
||||
const callstack: any[] = [];
|
||||
function clearAccu(): void {
|
||||
callstack.length = 0;
|
||||
parser.trackedStack.length = 0;
|
||||
}
|
||||
beforeEach(() => {
|
||||
RESULT = [
|
||||
['SGR', [1, 31]],
|
||||
['PRINT', 'hello '],
|
||||
['ESC %G'],
|
||||
['PRINT', 'wor'],
|
||||
['ESC E'],
|
||||
['PRINT', 'ld!'],
|
||||
['SGR', [0]],
|
||||
['EXE \r'],
|
||||
['EXE \n'],
|
||||
['PRINT', '$>'],
|
||||
['OSC 1', 'foo=bar']
|
||||
];
|
||||
parser = new TestEscapeSequenceParser();
|
||||
parser.reset();
|
||||
parser.trackStackSavesOnPause();
|
||||
clearAccu();
|
||||
});
|
||||
describe('sync handlers should behave as before', () => {
|
||||
beforeEach(() => {
|
||||
parser.setPrintHandler((data, start, end) => {
|
||||
let result = '';
|
||||
for (let i = start; i < end; ++i) {
|
||||
result += stringFromCodePoint(data[i]);
|
||||
}
|
||||
callstack.push(['PRINT', result]);
|
||||
});
|
||||
parser.registerCsiHandler({ final: 'm' }, params => { callstack.push(['SGR', params.toArray()]); return true; });
|
||||
parser.registerEscHandler({ intermediates: '%', final: 'G' }, () => { callstack.push(['ESC %G']); return true; });
|
||||
parser.registerEscHandler({ final: 'E' }, () => { callstack.push(['ESC E']); return true; });
|
||||
parser.setExecuteHandler('\r', () => { callstack.push(['EXE \r']); return true; });
|
||||
parser.setExecuteHandler('\n', () => { callstack.push(['EXE \n']); return true; });
|
||||
parser.registerOscHandler(1, new OscHandler(data => { callstack.push(['OSC 1', data]); return true; }));
|
||||
});
|
||||
|
||||
it('sync handlers keep parsed in sync mode', () => {
|
||||
// note: if we have only sync handlers, a parse call should never return anything
|
||||
assert.equal(!parseSync(parser, INPUT), true);
|
||||
assert.equal(parser.parseStack.state, ParserStackType.NONE); // not paused
|
||||
assert.equal(parser.trackedStack.length, 0); // never got paused
|
||||
});
|
||||
it('correct result on sync parse call', () => {
|
||||
parseSync(parser, INPUT);
|
||||
assert.deepEqual(callstack, RESULT);
|
||||
assert.equal(parser.trackedStack.length, 0);
|
||||
});
|
||||
it('correct result on async parse call', async () => {
|
||||
await parseP(parser, INPUT);
|
||||
assert.deepEqual(callstack, RESULT);
|
||||
assert.equal(parser.trackedStack.length, 0);
|
||||
});
|
||||
});
|
||||
describe('async handlers', () => {
|
||||
beforeEach(() => {
|
||||
parser.setPrintHandler((data, start, end) => {
|
||||
let result = '';
|
||||
for (let i = start; i < end; ++i) {
|
||||
result += stringFromCodePoint(data[i]);
|
||||
}
|
||||
callstack.push(['PRINT', result]);
|
||||
});
|
||||
parser.registerCsiHandler({ final: 'm' }, async params => { callstack.push(['SGR', params.toArray()]); return true; });
|
||||
parser.registerEscHandler({ intermediates: '%', final: 'G' }, async () => { callstack.push(['ESC %G']); return true; });
|
||||
parser.registerEscHandler({ final: 'E' }, async () => { callstack.push(['ESC E']); return true; });
|
||||
parser.setExecuteHandler('\r', () => { callstack.push(['EXE \r']); return true; });
|
||||
parser.setExecuteHandler('\n', () => { callstack.push(['EXE \n']); return true; });
|
||||
parser.registerOscHandler(1, new OscHandler(data => { callstack.push(['OSC 1', data]); return true; }));
|
||||
});
|
||||
|
||||
it('sync parse call does not work anymore', () => {
|
||||
assert.notEqual(!parseSync(parser, INPUT), true);
|
||||
assert.notDeepEqual(callstack, RESULT);
|
||||
// due to sync calling we should save exactly one saved stack
|
||||
// proper continuation is not possible anymore, as we lost the promise resolve value
|
||||
assert.equal(parser.trackedStack.length, 1);
|
||||
});
|
||||
it('improper continuation should throw', async () => {
|
||||
/**
|
||||
* Explanation:
|
||||
* The first sync call will stop at the first promise returned,
|
||||
* but does not await its resolve value.
|
||||
* The second sync call to parse will fail due to missing `promiseResult`,
|
||||
* which is needed for correct continuation.
|
||||
*/
|
||||
assert.notEqual(!parseSync(parser, INPUT), true);
|
||||
assert.notDeepEqual(callstack, RESULT);
|
||||
assert.throws(() => parseSync(parser, INPUT), 'improper continuation due to previous async handler, giving up parsing');
|
||||
// keeps being broken for further parse calls (sync and async)
|
||||
assert.throws(() => parseSync(parser, 'random'), 'improper continuation due to previous async handler, giving up parsing');
|
||||
await throwsAsync(() => parseP(parser, 'foobar'), 'improper continuation due to previous async handler, giving up parsing');
|
||||
// FIXME: come up with a good recovery strategy
|
||||
});
|
||||
it('correct result on awaited parse call', async () => {
|
||||
await parseP(parser, INPUT);
|
||||
assert.deepEqual(callstack, RESULT);
|
||||
evalStackSaves(parser.trackedStack, [
|
||||
[6, ParserStackType.CSI, 0], [15, ParserStackType.ESC, 0], [20, ParserStackType.ESC, 0], [27, ParserStackType.CSI, 0]
|
||||
]);
|
||||
});
|
||||
it('correct result on chunked awaited parse calls', async () => {
|
||||
RESULT = [
|
||||
['SGR', [1, 31]],
|
||||
['PRINT', 'h'], // due to single char input PRINT is split
|
||||
['PRINT', 'e'],
|
||||
['PRINT', 'l'],
|
||||
['PRINT', 'l'],
|
||||
['PRINT', 'o'],
|
||||
['PRINT', ' '],
|
||||
['ESC %G'],
|
||||
['PRINT', 'w'],
|
||||
['PRINT', 'o'],
|
||||
['PRINT', 'r'],
|
||||
['ESC E'],
|
||||
['PRINT', 'l'],
|
||||
['PRINT', 'd'],
|
||||
['PRINT', '!'],
|
||||
['SGR', [0]],
|
||||
['EXE \r'],
|
||||
['EXE \n'],
|
||||
['PRINT', '$'],
|
||||
['PRINT', '>'],
|
||||
['OSC 1', 'foo=bar']
|
||||
];
|
||||
|
||||
// split to single char input
|
||||
for (let i = 0; i < INPUT.length; ++i) {
|
||||
// Note: a single fully awaited parse call always ends in sync mode,
|
||||
// which re-enables faster sync processing in the higher up callstack
|
||||
await parseP(parser, INPUT[i]);
|
||||
}
|
||||
assert.deepEqual(callstack, RESULT);
|
||||
evalStackSaves(parser.trackedStack, [
|
||||
[0, ParserStackType.CSI, 0],
|
||||
[0, ParserStackType.ESC, 0],
|
||||
[0, ParserStackType.ESC, 0],
|
||||
[0, ParserStackType.CSI, 0]
|
||||
]);
|
||||
});
|
||||
it('multiple async SGR handlers', async () => {
|
||||
// register with fallback
|
||||
const SGR2 = parser.registerCsiHandler({ final: 'm' }, async params => { callstack.push(['2# SGR', params.toArray()]); return false; });
|
||||
await parseP(parser, INPUT);
|
||||
// should contain [2# SGR, SGR] call pairs
|
||||
for (let i = 0; i < callstack.length; ++i) {
|
||||
const entry = callstack[i];
|
||||
if (entry[0] === '2# SGR') assert.equal(callstack[i + 1][0], 'SGR', 'Should fallback to original handler');
|
||||
}
|
||||
evalStackSaves(parser.trackedStack, [
|
||||
[6, ParserStackType.CSI, 1],
|
||||
[6, ParserStackType.CSI, 0],
|
||||
[15, ParserStackType.ESC, 0],
|
||||
[20, ParserStackType.ESC, 0],
|
||||
[27, ParserStackType.CSI, 1],
|
||||
[27, ParserStackType.CSI, 0]
|
||||
]);
|
||||
clearAccu();
|
||||
// after dispose we should be back to RESULT
|
||||
SGR2.dispose();
|
||||
await parseP(parser, INPUT);
|
||||
assert.deepEqual(callstack, RESULT, 'Should not call custom handler');
|
||||
evalStackSaves(parser.trackedStack, [
|
||||
[6, ParserStackType.CSI, 0],
|
||||
[15, ParserStackType.ESC, 0],
|
||||
[20, ParserStackType.ESC, 0],
|
||||
[27, ParserStackType.CSI, 0]
|
||||
]);
|
||||
clearAccu();
|
||||
|
||||
// register without fallback
|
||||
const SGR22 = parser.registerCsiHandler({ final: 'm' }, async params => { callstack.push(['2# SGR', params.toArray()]); return true; });
|
||||
await parseP(parser, INPUT);
|
||||
// should only contain 2# SGR
|
||||
for (let i = 0; i < callstack.length; ++i) {
|
||||
const entry = callstack[i];
|
||||
if (entry[0] === '2# SGR') assert.notEqual(callstack[i + 1][0], 'SGR', 'Should not fallback to original handler');
|
||||
}
|
||||
evalStackSaves(parser.trackedStack, [
|
||||
[6, ParserStackType.CSI, 1],
|
||||
[15, ParserStackType.ESC, 0],
|
||||
[20, ParserStackType.ESC, 0],
|
||||
[27, ParserStackType.CSI, 1]
|
||||
]);
|
||||
clearAccu();
|
||||
// after dispose we should be back to RESULT
|
||||
SGR22.dispose();
|
||||
await parseP(parser, INPUT);
|
||||
assert.deepEqual(callstack, RESULT, 'Should not call custom handler');
|
||||
evalStackSaves(parser.trackedStack, [
|
||||
[6, ParserStackType.CSI, 0],
|
||||
[15, ParserStackType.ESC, 0],
|
||||
[20, ParserStackType.ESC, 0],
|
||||
[27, ParserStackType.CSI, 0]
|
||||
]);
|
||||
});
|
||||
it('multiple async ESC handlers', async () => {
|
||||
// register with fallback
|
||||
const ESC2 = parser.registerEscHandler({ final: 'E' }, async () => { callstack.push(['2# ESC E']); return false; });
|
||||
await parseP(parser, INPUT);
|
||||
for (let i = 0; i < callstack.length; ++i) {
|
||||
const entry = callstack[i];
|
||||
if (entry[0] === '2# ESC E') assert.equal(callstack[i + 1][0], 'ESC E', 'Should fallback to original handler');
|
||||
}
|
||||
evalStackSaves(parser.trackedStack, [
|
||||
[6, ParserStackType.CSI, 0],
|
||||
[15, ParserStackType.ESC, 0],
|
||||
[20, ParserStackType.ESC, 1],
|
||||
[20, ParserStackType.ESC, 0],
|
||||
[27, ParserStackType.CSI, 0]
|
||||
]);
|
||||
clearAccu();
|
||||
// after dispose we should be back to RESULT
|
||||
ESC2.dispose();
|
||||
await parseP(parser, INPUT);
|
||||
assert.deepEqual(callstack, RESULT, 'Should not call custom handler');
|
||||
evalStackSaves(parser.trackedStack, [
|
||||
[6, ParserStackType.CSI, 0],
|
||||
[15, ParserStackType.ESC, 0],
|
||||
[20, ParserStackType.ESC, 0],
|
||||
[27, ParserStackType.CSI, 0]
|
||||
]);
|
||||
clearAccu();
|
||||
|
||||
// register without fallback
|
||||
const ESC22 = parser.registerEscHandler({ final: 'E' }, async () => { callstack.push(['2# ESC E']); return true; });
|
||||
await parseP(parser, INPUT);
|
||||
for (let i = 0; i < callstack.length; ++i) {
|
||||
const entry = callstack[i];
|
||||
if (entry[0] === '2# ESC E') assert.notEqual(callstack[i + 1][0], 'ESC E', 'Should not fallback to original handler');
|
||||
}
|
||||
evalStackSaves(parser.trackedStack, [
|
||||
[6, ParserStackType.CSI, 0],
|
||||
[15, ParserStackType.ESC, 0],
|
||||
[20, ParserStackType.ESC, 1],
|
||||
[27, ParserStackType.CSI, 0]
|
||||
]);
|
||||
clearAccu();
|
||||
// after dispose we should be back to RESULT
|
||||
ESC22.dispose();
|
||||
await parseP(parser, INPUT);
|
||||
assert.deepEqual(callstack, RESULT, 'Should not call custom handler');
|
||||
evalStackSaves(parser.trackedStack, [
|
||||
[6, ParserStackType.CSI, 0],
|
||||
[15, ParserStackType.ESC, 0],
|
||||
[20, ParserStackType.ESC, 0],
|
||||
[27, ParserStackType.CSI, 0]
|
||||
]);
|
||||
});
|
||||
it('sync/async SGR mixed', async () => {
|
||||
// sync with fallback
|
||||
const SGR2 = parser.registerCsiHandler({ final: 'm' }, params => { callstack.push(['2# SGR', params.toArray()]); return false; });
|
||||
// async with fallback
|
||||
const SGR3 = parser.registerCsiHandler({ final: 'm' }, async params => { callstack.push(['3# SGR', params.toArray()]); return false; });
|
||||
await parseP(parser, INPUT);
|
||||
// should contain [3# SGR, 2# SGR, SGR] call triples
|
||||
for (let i = 0; i < callstack.length; ++i) {
|
||||
const entry = callstack[i];
|
||||
if (entry[0] === '3# SGR') {
|
||||
assert.equal(callstack[i + 1][0], '2# SGR', 'Should fallback to next handler');
|
||||
assert.equal(callstack[i + 2][0], 'SGR', 'Should fallback to original handler');
|
||||
}
|
||||
}
|
||||
evalStackSaves(parser.trackedStack, [
|
||||
[6, ParserStackType.CSI, 2],
|
||||
[6, ParserStackType.CSI, 0],
|
||||
[15, ParserStackType.ESC, 0],
|
||||
[20, ParserStackType.ESC, 0],
|
||||
[27, ParserStackType.CSI, 2],
|
||||
[27, ParserStackType.CSI, 0]
|
||||
]);
|
||||
clearAccu();
|
||||
// dispose SGR2 (sync one)
|
||||
SGR2.dispose();
|
||||
await parseP(parser, INPUT);
|
||||
// should contain [3# SGR, SGR] call pairs
|
||||
for (let i = 0; i < callstack.length; ++i) {
|
||||
const entry = callstack[i];
|
||||
if (entry[0] === '3# SGR') {
|
||||
assert.equal(callstack[i + 1][0], 'SGR', 'Should fallback to original handler');
|
||||
}
|
||||
}
|
||||
evalStackSaves(parser.trackedStack, [
|
||||
[6, ParserStackType.CSI, 1],
|
||||
[6, ParserStackType.CSI, 0],
|
||||
[15, ParserStackType.ESC, 0],
|
||||
[20, ParserStackType.ESC, 0],
|
||||
[27, ParserStackType.CSI, 1],
|
||||
[27, ParserStackType.CSI, 0]
|
||||
]);
|
||||
clearAccu();
|
||||
// dispose SGR3 (async one)
|
||||
SGR3.dispose();
|
||||
await parseP(parser, INPUT);
|
||||
assert.deepEqual(callstack, RESULT, 'Should not call custom handler');
|
||||
evalStackSaves(parser.trackedStack, [
|
||||
[6, ParserStackType.CSI, 0],
|
||||
[15, ParserStackType.ESC, 0],
|
||||
[20, ParserStackType.ESC, 0],
|
||||
[27, ParserStackType.CSI, 0]
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -440,14 +440,14 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
/**
|
||||
* Async parse support.
|
||||
*/
|
||||
private _parseStack: IParserStackState = {
|
||||
protected _parseStack: IParserStackState = {
|
||||
state: ParserStackType.NONE,
|
||||
handlers: [],
|
||||
handlerPos: 0,
|
||||
transition: 0,
|
||||
chunkPos: 0
|
||||
};
|
||||
private _preserveStack(
|
||||
protected _preserveStack(
|
||||
state: ParserStackType,
|
||||
handlers: ResumableHandlersType,
|
||||
handlerPos: number,
|
||||
@@ -474,6 +474,39 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
* - DCS_PARAM:PARAM
|
||||
* - OSC_STRING:OSC_PUT
|
||||
* - DCS_PASSTHROUGH:DCS_PUT
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* FIXME: to be discussed
|
||||
* While awaiting parse promises the terminal buffer state may not change.
|
||||
* --> Implement lock semantics / promise chaining on buffer alterations? Waah, pandora's box ;)
|
||||
* --> Maybe easier: Give up on non-mutating rule for async handlers...
|
||||
* (needs explanation in docs about exact executor/thenable/worker execution contexts)
|
||||
*
|
||||
* Example for proper parsing of multiple chunks:
|
||||
*
|
||||
* ```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 = 0;
|
||||
@@ -483,6 +516,17 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
|
||||
// resume from async handler
|
||||
if (this._parseStack.state) {
|
||||
if (promiseResult === undefined || this._parseStack.state === ParserStackType.FAIL) {
|
||||
/**
|
||||
* Reject further parsing on improper continuation after pausing.
|
||||
* This will happen with sync parse calls not awaiting a returned promise.
|
||||
* It is a really bad condition with screwed up execution order,
|
||||
* therefore we exit hard with an exception.
|
||||
* FIXME: Do we need a method to escape from this broken parser state? (hard to achieve properly)
|
||||
*/
|
||||
this._parseStack.state = ParserStackType.FAIL;
|
||||
throw new Error('improper continuation due to previous async handler, giving up parsing');
|
||||
}
|
||||
let handlerPos = this._parseStack.handlerPos - 1;
|
||||
|
||||
// we have to resume the old handler loop if:
|
||||
|
||||
Vendored
+1
@@ -244,6 +244,7 @@ export interface IHandlerCollection<T> {
|
||||
*/
|
||||
export const enum ParserStackType {
|
||||
NONE = 0,
|
||||
FAIL,
|
||||
CSI,
|
||||
ESC,
|
||||
OSC,
|
||||
|
||||
Reference in New Issue
Block a user