From fb7e5d590c21467f777897e20dfcc3f92ec74a44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 21 Apr 2018 05:12:24 +0200 Subject: [PATCH 1/3] new parser --- src/EscapeSequenceParser.test.ts | 1042 ++++++++++++++++++++++++++++++ src/EscapeSequenceParser.ts | 718 ++++++++++++++++++++ src/InputHandler.ts | 61 +- src/Terminal.ts | 15 +- 4 files changed, 1802 insertions(+), 34 deletions(-) create mode 100644 src/EscapeSequenceParser.test.ts create mode 100644 src/EscapeSequenceParser.ts diff --git a/src/EscapeSequenceParser.test.ts b/src/EscapeSequenceParser.test.ts new file mode 100644 index 00000000..eefa7603 --- /dev/null +++ b/src/EscapeSequenceParser.test.ts @@ -0,0 +1,1042 @@ +import { AnsiParser, IParserTerminal } from './EscapeSequenceParser'; +import * as chai from 'chai'; + +function r(a: number, b: number): string[] { + let c = b - a; + let arr = new Array(c); + while (c--) { + arr[c] = String.fromCharCode(--b); + } + return arr; +} + +interface ITestTerminal extends IParserTerminal { + calls: any[]; + clear: () => void; + compare: (value: any) => void; +} + +let testTerminal: ITestTerminal = { + calls: [], + clear: function (): void { + this.calls = []; + }, + compare: function (value: any): void { + chai.expect(this.calls.slice()).eql(value); // weird bug w'o slicing here + }, + inst_p: function (s: string, start: number, end: number): void { + this.calls.push(['print', s.substring(start, end)]); + }, + inst_o: function (s: string): void { + this.calls.push(['osc', s]); + }, + inst_x: function (flag: string): void { + this.calls.push(['exe', flag]); + }, + inst_c: function (collected: string, params: number[], flag: string): void { + this.calls.push(['csi', collected, params, flag]); + }, + inst_e: function (collected: string, flag: string): void { + this.calls.push(['esc', collected, flag]); + }, + inst_H: function (collected: string, params: number[], flag: string): void { + this.calls.push(['dcs hook', collected, params, flag]); + }, + inst_P: function (dcs: string): void { + this.calls.push(['dcs put', dcs]); + }, + inst_U: function (): void { + this.calls.push(['dcs unhook']); + } +}; + +let parser = new AnsiParser(testTerminal); + +describe('Parser init and methods', function(): void { + it('parser init', function (): void { + let p = new AnsiParser({}); + chai.expect(p.term).a('object'); + chai.expect(p.term.inst_p).a('function'); + chai.expect(p.term.inst_o).a('function'); + chai.expect(p.term.inst_x).a('function'); + chai.expect(p.term.inst_c).a('function'); + chai.expect(p.term.inst_e).a('function'); + chai.expect(p.term.inst_H).a('function'); + chai.expect(p.term.inst_P).a('function'); + chai.expect(p.term.inst_U).a('function'); + p.parse('\x1b[31mHello World!'); + }); + it('terminal callbacks', function (): void { + chai.expect(parser.term).equal(testTerminal); + chai.expect(parser.term.inst_p).equal(testTerminal.inst_p); + chai.expect(parser.term.inst_o).equal(testTerminal.inst_o); + chai.expect(parser.term.inst_x).equal(testTerminal.inst_x); + chai.expect(parser.term.inst_c).equal(testTerminal.inst_c); + chai.expect(parser.term.inst_e).equal(testTerminal.inst_e); + chai.expect(parser.term.inst_H).equal(testTerminal.inst_H); + chai.expect(parser.term.inst_P).equal(testTerminal.inst_P); + chai.expect(parser.term.inst_U).equal(testTerminal.inst_U); + }); + it('inital states', function (): void { + chai.expect(parser.initialState).equal(0); + chai.expect(parser.currentState).equal(0); + chai.expect(parser.osc).equal(''); + chai.expect(parser.params).eql([0]); + chai.expect(parser.collected).equal(''); + }); + it('reset states', function (): void { + parser.currentState = 124; + parser.osc = '#'; + parser.params = [123]; + parser.collected = '#'; + + parser.reset(); + chai.expect(parser.currentState).equal(0); + chai.expect(parser.osc).equal(''); + chai.expect(parser.params).eql([0]); + chai.expect(parser.collected).equal(''); + }); +}); + +describe('state transitions and actions', function(): void { + it('state GROUND execute action', function (): void { + parser.reset(); + testTerminal.clear(); + let exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = 0; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(0); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state GROUND print action', function (): void { + parser.reset(); + testTerminal.clear(); + let printables = r(0x20, 0x7f); // NOTE: DEL excluded + for (let i = 0; i < printables.length; ++i) { + parser.currentState = 0; + parser.parse(printables[i]); + chai.expect(parser.currentState).equal(0); + testTerminal.compare([['print', printables[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans ANYWHERE --> GROUND with actions', function (): void { + let exes = [ + '\x18', '\x1a', + '\x80', '\x81', '\x82', '\x83', '\x84', '\x85', '\x86', '\x87', '\x88', + '\x89', '\x8a', '\x8b', '\x8c', '\x8d', '\x8e', '\x8f', + '\x91', '\x92', '\x93', '\x94', '\x95', '\x96', '\x97', '\x99', '\x9a' + ]; + let exceptions = { + 8: {'\x18': [], '\x1a': []} // simply abort osc state + }; + parser.reset(); + testTerminal.clear(); + for (let state = 0; state < 14; ++state) { + for (let i = 0; i < exes.length; ++i) { + parser.currentState = state; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(0); + testTerminal.compare(((exceptions[state]) ? exceptions[state][exes[i]] : 0) || [['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + parser.parse('\x9c'); + chai.expect(parser.currentState).equal(0); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans ANYWHERE --> ESCAPE with clear', function (): void { + parser.reset(); + for (let state = 0; state < 14; ++state) { + parser.currentState = state; + parser.osc = '#'; + parser.params = [23]; + parser.collected = '#'; + parser.parse('\x1b'); + chai.expect(parser.currentState).equal(1); + chai.expect(parser.osc).equal(''); + chai.expect(parser.params).eql([0]); + chai.expect(parser.collected).equal(''); + parser.reset(); + } + }); + it('state ESCAPE execute rules', function (): void { + parser.reset(); + testTerminal.clear(); + let exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = 1; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(1); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state ESCAPE ignore', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = 1; + parser.parse('\x7f'); + chai.expect(parser.currentState).equal(1); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('trans ESCAPE --> GROUND with ecs_dispatch action', function (): void { + parser.reset(); + testTerminal.clear(); + let dispatches = r(0x30, 0x50); + dispatches.concat(r(0x51, 0x58)); + dispatches.concat(['\x59', '\x5a', '\x5c']); + dispatches.concat(r(0x60, 0x7f)); + for (let i = 0; i < dispatches.length; ++i) { + parser.currentState = 1; + parser.parse(dispatches[i]); + chai.expect(parser.currentState).equal(0); + testTerminal.compare([['esc', '', dispatches[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans ESCAPE --> ESCAPE_INTERMEDIATE with collect action', function (): void { + parser.reset(); + let collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = 1; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(2); + chai.expect(parser.collected).equal(collect[i]); + parser.reset(); + } + }); + it('state ESCAPE_INTERMEDIATE execute rules', function (): void { + parser.reset(); + testTerminal.clear(); + let exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = 2; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(2); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state ESCAPE_INTERMEDIATE ignore', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = 2; + parser.parse('\x7f'); + chai.expect(parser.currentState).equal(2); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('state ESCAPE_INTERMEDIATE collect action', function (): void { + parser.reset(); + let collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = 2; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(2); + chai.expect(parser.collected).equal(collect[i]); + parser.reset(); + } + }); + it('trans ESCAPE_INTERMEDIATE --> GROUND with esc_dispatch action', function (): void { + parser.reset(); + testTerminal.clear(); + let collect = r(0x30, 0x7f); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = 2; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(0); + testTerminal.compare([['esc', '', collect[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans ANYWHERE/ESCAPE --> CSI_ENTRY with clear', function (): void { + parser.reset(); + // C0 + parser.currentState = 1; + parser.osc = '#'; + parser.params = [123]; + parser.collected = '#'; + parser.parse('['); + chai.expect(parser.currentState).equal(3); + chai.expect(parser.osc).equal(''); + chai.expect(parser.params).eql([0]); + chai.expect(parser.collected).equal(''); + parser.reset(); + // C1 + for (let state = 0; state < 14; ++state) { + parser.currentState = state; + parser.osc = '#'; + parser.params = [123]; + parser.collected = '#'; + parser.parse('\x9b'); + chai.expect(parser.currentState).equal(3); + chai.expect(parser.osc).equal(''); + chai.expect(parser.params).eql([0]); + chai.expect(parser.collected).equal(''); + parser.reset(); + } + }); + it('state CSI_ENTRY execute rules', function (): void { + parser.reset(); + testTerminal.clear(); + let exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = 3; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(3); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state CSI_ENTRY ignore', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = 3; + parser.parse('\x7f'); + chai.expect(parser.currentState).equal(3); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('trans CSI_ENTRY --> GROUND with csi_dispatch action', function (): void { + parser.reset(); + let dispatches = r(0x40, 0x7f); + for (let i = 0; i < dispatches.length; ++i) { + parser.currentState = 3; + parser.parse(dispatches[i]); + chai.expect(parser.currentState).equal(0); + testTerminal.compare([['csi', '', [0], dispatches[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans CSI_ENTRY --> CSI_PARAM with param/collect actions', function (): void { + parser.reset(); + let params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; + let collect = ['\x3c', '\x3d', '\x3e', '\x3f']; + for (let i = 0; i < params.length; ++i) { + parser.currentState = 3; + parser.parse(params[i]); + chai.expect(parser.currentState).equal(4); + chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); + parser.reset(); + } + // ';' + parser.currentState = 3; + parser.parse('\x3b'); + chai.expect(parser.currentState).equal(4); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = 3; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(4); + chai.expect(parser.collected).equal(collect[i]); + parser.reset(); + } + }); + it('state CSI_PARAM execute rules', function (): void { + parser.reset(); + testTerminal.clear(); + let exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = 4; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(4); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state CSI_PARAM param action', function (): void { + parser.reset(); + let params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; + for (let i = 0; i < params.length; ++i) { + parser.currentState = 4; + parser.parse(params[i]); + chai.expect(parser.currentState).equal(4); + chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); + parser.reset(); + } + parser.currentState = 4; + parser.parse('\x3b'); + chai.expect(parser.currentState).equal(4); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + }); + it('state CSI_PARAM ignore', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = 4; + parser.parse('\x7f'); + chai.expect(parser.currentState).equal(4); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('trans CSI_PARAM --> GROUND with csi_dispatch action', function (): void { + parser.reset(); + let dispatches = r(0x40, 0x7f); + for (let i = 0; i < dispatches.length; ++i) { + parser.currentState = 4; + parser.params = [0, 1]; + parser.parse(dispatches[i]); + chai.expect(parser.currentState).equal(0); + testTerminal.compare([['csi', '', [0, 1], dispatches[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans CSI_ENTRY --> CSI_INTERMEDIATE with collect action', function (): void { + parser.reset(); + let collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = 3; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(5); + chai.expect(parser.collected).equal(collect[i]); + parser.reset(); + } + }); + it('trans CSI_PARAM --> CSI_INTERMEDIATE with collect action', function (): void { + parser.reset(); + let collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = 4; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(5); + chai.expect(parser.collected).equal(collect[i]); + parser.reset(); + } + }); + it('state CSI_INTERMEDIATE execute rules', function (): void { + parser.reset(); + testTerminal.clear(); + let exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = 5; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(5); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state CSI_INTERMEDIATE collect', function (): void { + parser.reset(); + let collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = 5; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(5); + chai.expect(parser.collected).equal(collect[i]); + parser.reset(); + } + }); + it('state CSI_INTERMEDIATE ignore', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = 5; + parser.parse('\x7f'); + chai.expect(parser.currentState).equal(5); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('trans CSI_INTERMEDIATE --> GROUND with csi_dispatch action', function (): void { + parser.reset(); + let dispatches = r(0x40, 0x7f); + for (let i = 0; i < dispatches.length; ++i) { + parser.currentState = 5; + parser.params = [0, 1]; + parser.parse(dispatches[i]); + chai.expect(parser.currentState).equal(0); + testTerminal.compare([['csi', '', [0, 1], dispatches[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans CSI_ENTRY --> CSI_IGNORE', function (): void { + parser.reset(); + parser.currentState = 3; + parser.parse('\x3a'); + chai.expect(parser.currentState).equal(6); + parser.reset(); + }); + it('trans CSI_PARAM --> CSI_IGNORE', function (): void { + parser.reset(); + let chars = ['\x3a', '\x3c', '\x3d', '\x3e', '\x3f']; + for (let i = 0; i < chars.length; ++i) { + parser.currentState = 4; + parser.parse('\x3b' + chars[i]); + chai.expect(parser.currentState).equal(6); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + } + }); + it('trans CSI_INTERMEDIATE --> CSI_IGNORE', function (): void { + parser.reset(); + let chars = r(0x30, 0x40); + for (let i = 0; i < chars.length; ++i) { + parser.currentState = 5; + parser.parse(chars[i]); + chai.expect(parser.currentState).equal(6); + chai.expect(parser.params).eql([0]); + parser.reset(); + } + }); + it('state CSI_IGNORE execute rules', function (): void { + parser.reset(); + testTerminal.clear(); + let exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = 6; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(6); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state CSI_IGNORE ignore', function (): void { + parser.reset(); + testTerminal.clear(); + let ignored = r(0x20, 0x40); + ignored.concat(['\x7f']); + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = 6; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(6); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans CSI_IGNORE --> GROUND', function (): void { + parser.reset(); + let dispatches = r(0x40, 0x7f); + for (let i = 0; i < dispatches.length; ++i) { + parser.currentState = 6; + parser.params = [0, 1]; + parser.parse(dispatches[i]); + chai.expect(parser.currentState).equal(0); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans ANYWHERE/ESCAPE --> SOS_PM_APC_STRING', function (): void { + parser.reset(); + // C0 + let initializers = ['\x58', '\x5e', '\x5f']; + for (let i = 0; i < initializers.length; ++i) { + parser.parse('\x1b' + initializers[i]); + chai.expect(parser.currentState).equal(7); + parser.reset(); + } + // C1 + for (let state = 0; state < 14; ++state) { + parser.currentState = state; + initializers = ['\x98', '\x9e', '\x9f']; + for (let i = 0; i < initializers.length; ++i) { + parser.parse(initializers[i]); + chai.expect(parser.currentState).equal(7); + parser.reset(); + } + } + }); + it('state SOS_PM_APC_STRING ignore rules', function (): void { + parser.reset(); + let ignored = r(0x00, 0x18); + ignored.concat(['\x19']); + ignored.concat(r(0x1c, 0x20)); + ignored.concat(r(0x20, 0x80)); + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = 7; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(7); + parser.reset(); + } + }); + it('trans ANYWHERE/ESCAPE --> OSC_STRING', function (): void { + parser.reset(); + // C0 + parser.parse('\x1b]'); + chai.expect(parser.currentState).equal(8); + parser.reset(); + // C1 + for (let state = 0; state < 14; ++state) { + parser.currentState = state; + parser.parse('\x9d'); + chai.expect(parser.currentState).equal(8); + parser.reset(); + } + }); + it('state OSC_STRING ignore rules', function (): void { + parser.reset(); + let ignored = [ + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', /*'\x07',*/ '\x08', + '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', + '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f']; + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = 8; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(8); + chai.expect(parser.osc).equal(''); + parser.reset(); + } + }); + it('state OSC_STRING put action', function (): void { + parser.reset(); + let puts = r(0x20, 0x80); + for (let i = 0; i < puts.length; ++i) { + parser.currentState = 8; + parser.parse(puts[i]); + chai.expect(parser.currentState).equal(8); + chai.expect(parser.osc).equal(puts[i]); + parser.reset(); + } + }); + it('state DCS_ENTRY', function (): void { + parser.reset(); + // C0 + parser.parse('\x1bP'); + chai.expect(parser.currentState).equal(9); + parser.reset(); + // C1 + for (let state = 0; state < 14; ++state) { + parser.currentState = state; + parser.parse('\x90'); + chai.expect(parser.currentState).equal(9); + parser.reset(); + } + }); + it('state DCS_ENTRY ignore rules', function (): void { + parser.reset(); + let ignored = [ + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', + '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', + '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = 9; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(9); + parser.reset(); + } + }); + it('state DCS_ENTRY --> DCS_PARAM with param/collect actions', function (): void { + parser.reset(); + let params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; + let collect = ['\x3c', '\x3d', '\x3e', '\x3f']; + for (let i = 0; i < params.length; ++i) { + parser.currentState = 9; + parser.parse(params[i]); + chai.expect(parser.currentState).equal(10); + chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); + parser.reset(); + } + parser.currentState = 9; + parser.parse('\x3b'); + chai.expect(parser.currentState).equal(10); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = 9; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(10); + chai.expect(parser.collected).equal(collect[i]); + parser.reset(); + } + }); + it('state DCS_PARAM ignore rules', function (): void { + parser.reset(); + let ignored = [ + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', + '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', + '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = 10; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(10); + parser.reset(); + } + }); + it('state DCS_PARAM param action', function (): void { + parser.reset(); + let params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; + for (let i = 0; i < params.length; ++i) { + parser.currentState = 10; + parser.parse(params[i]); + chai.expect(parser.currentState).equal(10); + chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); + parser.reset(); + } + parser.currentState = 10; + parser.parse('\x3b'); + chai.expect(parser.currentState).equal(10); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + }); + it('trans DCS_ENTRY --> DCS_IGNORE', function (): void { + parser.reset(); + parser.currentState = 9; + parser.parse('\x3a'); + chai.expect(parser.currentState).equal(11); + parser.reset(); + }); + it('trans DCS_PARAM --> DCS_IGNORE', function (): void { + parser.reset(); + let chars = ['\x3a', '\x3c', '\x3d', '\x3e', '\x3f']; + for (let i = 0; i < chars.length; ++i) { + parser.currentState = 10; + parser.parse('\x3b' + chars[i]); + chai.expect(parser.currentState).equal(11); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + } + }); + it('trans DCS_INTERMEDIATE --> DCS_IGNORE', function (): void { + parser.reset(); + let chars = r(0x30, 0x40); + for (let i = 0; i < chars.length; ++i) { + parser.currentState = 12; + parser.parse(chars[i]); + chai.expect(parser.currentState).equal(11); + parser.reset(); + } + }); + it('state DCS_IGNORE ignore rules', function (): void { + parser.reset(); + let ignored = [ + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', + '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', + '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; + ignored.concat(r(0x20, 0x80)); + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = 11; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(11); + parser.reset(); + } + }); + it('trans DCS_ENTRY --> DCS_INTERMEDIATE with collect action', function (): void { + parser.reset(); + let collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = 9; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(12); + chai.expect(parser.collected).equal(collect[i]); + parser.reset(); + } + }); + it('trans DCS_PARAM --> DCS_INTERMEDIATE with collect action', function (): void { + parser.reset(); + let collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = 10; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(12); + chai.expect(parser.collected).equal(collect[i]); + parser.reset(); + } + }); + it('state DCS_INTERMEDIATE ignore rules', function (): void { + parser.reset(); + let ignored = [ + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', + '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', + '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = 12; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(12); + parser.reset(); + } + }); + it('state DCS_INTERMEDIATE collect action', function (): void { + parser.reset(); + let collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = 12; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(12); + chai.expect(parser.collected).equal(collect[i]); + parser.reset(); + } + }); + it('trans DCS_INTERMEDIATE --> DCS_IGNORE', function (): void { + parser.reset(); + let chars = r(0x30, 0x40); + for (let i = 0; i < chars.length; ++i) { + parser.currentState = 12; + parser.parse('\x20' + chars[i]); + chai.expect(parser.currentState).equal(11); + chai.expect(parser.collected).equal('\x20'); + parser.reset(); + } + }); + it('trans DCS_ENTRY --> DCS_PASSTHROUGH with hook', function (): void { + parser.reset(); + testTerminal.clear(); + let collect = r(0x40, 0x7f); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = 9; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(13); + testTerminal.compare([['dcs hook', '', [0], collect[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans DCS_PARAM --> DCS_PASSTHROUGH with hook', function (): void { + parser.reset(); + testTerminal.clear(); + let collect = r(0x40, 0x7f); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = 10; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(13); + testTerminal.compare([['dcs hook', '', [0], collect[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans DCS_INTERMEDIATE --> DCS_PASSTHROUGH with hook', function (): void { + parser.reset(); + testTerminal.clear(); + let collect = r(0x40, 0x7f); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = 12; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(13); + testTerminal.compare([['dcs hook', '', [0], collect[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state DCS_PASSTHROUGH put action', function (): void { + parser.reset(); + testTerminal.clear(); + let puts = r(0x00, 0x18); + puts.concat(['\x19']); + puts.concat(r(0x1c, 0x20)); + puts.concat(r(0x20, 0x7f)); + for (let i = 0; i < puts.length; ++i) { + parser.currentState = 13; + parser.parse(puts[i]); + chai.expect(parser.currentState).equal(13); + testTerminal.compare([['dcs put', puts[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state DCS_PASSTHROUGH ignore', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = 13; + parser.parse('\x7f'); + chai.expect(parser.currentState).equal(13); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); +}); + +function test(s: string, value: any, noReset: any): void { + if (!noReset) { + parser.reset(); + testTerminal.clear(); + } + parser.parse(s); + testTerminal.compare(value); +} + +describe('escape sequence examples', function(): void { + it('CSI with print and execute', function (): void { + test('\x1b[<31;5mHello World! öäü€\nabc', + [ + ['csi', '<', [31, 5], 'm'], + ['print', 'Hello World! öäü€'], + ['exe', '\n'], + ['print', 'abc'] + ], null); + }); + it('OSC', function (): void { + test('\x1b]0;abc123€öäü\x07', [ + ['osc', '0;abc123€öäü'] + ], null); + }); + it('single DCS', function (): void { + test('\x1bP1;2;3+$abc;de\x9c', [ + ['dcs hook', '+$', [1, 2, 3], 'a'], + ['dcs put', 'bc;de'], + ['dcs unhook'] + ], null); + }); + it('multi DCS', function (): void { + test('\x1bP1;2;3+$abc;de', [ + ['dcs hook', '+$', [1, 2, 3], 'a'], + ['dcs put', 'bc;de'] + ], null); + testTerminal.clear(); + test('abc\x9c', [ + ['dcs put', 'abc'], + ['dcs unhook'] + ], true); + }); + it('print + DCS(C1)', function (): void { + test('abc\x901;2;3+$abc;de\x9c', [ + ['print', 'abc'], + ['dcs hook', '+$', [1, 2, 3], 'a'], + ['dcs put', 'bc;de'], + ['dcs unhook'] + ], null); + }); + it('print + PM(C1) + print', function (): void { + test('abc\x98123tzf\x9cdefg', [ + ['print', 'abc'], + ['print', 'defg'] + ], null); + }); + it('print + OSC(C1) + print', function (): void { + test('abc\x9d123tzf\x9cdefg', [ + ['print', 'abc'], + ['osc', '123tzf'], + ['print', 'defg'] + ], null); + }); + it('error recovery', function (): void { + test('\x1b[1€abcdefg\x9b<;c', [ + ['print', 'abcdefg'], + ['csi', '<', [0, 0], 'c'] + ], null); + }); +}); + +describe('coverage tests', function(): void { + it('CSI_IGNORE error', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = 6; + parser.parse('€öäü'); + chai.expect(parser.currentState).equal(6); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('DCS_IGNORE error', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = 11; + parser.parse('€öäü'); + chai.expect(parser.currentState).equal(11); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('DCS_PASSTHROUGH error', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = 13; + parser.parse('€öäü'); + chai.expect(parser.currentState).equal(13); + testTerminal.compare([['dcs put', '€öäü']]); + parser.reset(); + testTerminal.clear(); + }); + it('error else of if (code > 159)', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = 0; + parser.parse('\x1e'); + chai.expect(parser.currentState).equal(0); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); +}); + +let errorTerminal1 = function(): void {}; +errorTerminal1.prototype = testTerminal; +let errTerminal1 = new errorTerminal1(); +errTerminal1.inst_E = function(e: any): void { + this.calls.push(['error', e]); +}; +let errParser1 = new AnsiParser(errTerminal1); + +let errorTerminal2 = function(): void {}; +errorTerminal2.prototype = testTerminal; +let errTerminal2 = new errorTerminal2(); +errTerminal2.inst_E = function(e: any): any { + this.calls.push(['error', e]); + return true; // --> abort parsing +}; +let errParser2 = new AnsiParser(errTerminal2); + +describe('error tests', function(): void { + it('CSI_PARAM unicode error - inst_E output w/o abort', function (): void { + errParser1.parse('\x1b[<31;5€normal print'); + errTerminal1.compare([ + ['error', { + pos: 7, + character: '€', + state: 4, + print: -1, + dcs: -1, + osc: '', + collect: '<', + params: [31, 5]}], + ['print', 'normal print'] + ]); + parser.reset(); + testTerminal.clear(); + }); + it('CSI_PARAM unicode error - inst_E output with abort', function (): void { + errParser2.parse('\x1b[<31;5€no print'); + errTerminal2.compare([ + ['error', { + pos: 7, + character: '€', + state: 4, + print: -1, + dcs: -1, + osc: '', + collect: '<', + params: [31, 5]}] + ]); + parser.reset(); + testTerminal.clear(); + }); +}); diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts new file mode 100644 index 00000000..6facddea --- /dev/null +++ b/src/EscapeSequenceParser.ts @@ -0,0 +1,718 @@ +export interface IParserTerminal { + inst_p?: (s: string, start: number, end: number) => void; + inst_o?: (s: string) => void; + inst_x?: (flag: string) => void; + inst_c?: (collected: string, params: number[], flag: string) => void; + inst_e?: (collected: string, flag: string) => void; + inst_H?: (collected: string, params: number[], flag: string) => void; + inst_P?: (dcs: string) => void; + inst_U?: () => void; + inst_E?: () => void; // TODO: real signature +} + + +export function r(a: number, b: number): number[] { + let c = b - a; + let arr = new Array(c); + while (c--) { + arr[c] = --b; + } + return arr; +} + + +export class TransitionTable { + public table: Uint8Array; + constructor(length: number) { + this.table = new Uint8Array(length); + } + add(inp: number, state: number, action: number | null, next: number | null): void { + this.table[state << 8 | inp] = ((action | 0) << 4) | ((next === undefined) ? state : next); + } + add_list(inps: number[], state: number, action: number | null, next: number | null): void { + for (let i = 0; i < inps.length; i++) { + this.add(inps[i], state, action, next); + } + } +} + + +let PRINTABLES = r(0x20, 0x7f); +let EXECUTABLES = r(0x00, 0x18); +EXECUTABLES.push(0x19); +EXECUTABLES.concat(r(0x1c, 0x20)); + + +export const TRANSITION_TABLE = (function (): TransitionTable { + let t: TransitionTable = new TransitionTable(4095); + + // table with default transition [any] --> [error, GROUND] + for (let state = 0; state < 14; ++state) { + for (let code = 0; code < 160; ++code) { + t[state << 8 | code] = 16; + } + } + + // apply transitions + // printables + t.add_list(PRINTABLES, 0, 2, 0); + // global anywhere rules + for (let state = 0; state < 14; ++state) { + t.add_list([0x18, 0x1a, 0x99, 0x9a], state, 3, 0); + t.add_list(r(0x80, 0x90), state, 3, 0); + t.add_list(r(0x90, 0x98), state, 3, 0); + t.add(0x9c, state, 0, 0); // ST as terminator + t.add(0x1b, state, 11, 1); // ESC + t.add(0x9d, state, 4, 8); // OSC + t.add_list([0x98, 0x9e, 0x9f], state, 0, 7); + t.add(0x9b, state, 11, 3); // CSI + t.add(0x90, state, 11, 9); // DCS + } + // rules for executables and 7f + t.add_list(EXECUTABLES, 0, 3, 0); + t.add_list(EXECUTABLES, 1, 3, 1); + t.add(0x7f, 1, null, 1); + t.add_list(EXECUTABLES, 8, null, 8); + t.add_list(EXECUTABLES, 3, 3, 3); + t.add(0x7f, 3, null, 3); + t.add_list(EXECUTABLES, 4, 3, 4); + t.add(0x7f, 4, null, 4); + t.add_list(EXECUTABLES, 6, 3, 6); + t.add_list(EXECUTABLES, 5, 3, 5); + t.add(0x7f, 5, null, 5); + t.add_list(EXECUTABLES, 2, 3, 2); + t.add(0x7f, 2, null, 2); + // osc + t.add(0x5d, 1, 4, 8); + t.add_list(PRINTABLES, 8, 5, 8); + t.add(0x7f, 8, 5, 8); + t.add_list([0x9c, 0x1b, 0x18, 0x1a, 0x07], 8, 6, 0); + t.add_list(r(0x1c, 0x20), 8, 0, 8); + // sos/pm/apc does nothing + t.add_list([0x58, 0x5e, 0x5f], 1, 0, 7); + t.add_list(PRINTABLES, 7, null, 7); + t.add_list(EXECUTABLES, 7, null, 7); + t.add(0x9c, 7, 0, 0); + // csi entries + t.add(0x5b, 1, 11, 3); + t.add_list(r(0x40, 0x7f), 3, 7, 0); + t.add_list(r(0x30, 0x3a), 3, 8, 4); + t.add(0x3b, 3, 8, 4); + t.add_list([0x3c, 0x3d, 0x3e, 0x3f], 3, 9, 4); + t.add_list(r(0x30, 0x3a), 4, 8, 4); + t.add(0x3b, 4, 8, 4); + t.add_list(r(0x40, 0x7f), 4, 7, 0); + t.add_list([0x3a, 0x3c, 0x3d, 0x3e, 0x3f], 4, 0, 6); + t.add_list(r(0x20, 0x40), 6, null, 6); + t.add(0x7f, 6, null, 6); + t.add_list(r(0x40, 0x7f), 6, 0, 0); + t.add(0x3a, 3, 0, 6); + t.add_list(r(0x20, 0x30), 3, 9, 5); + t.add_list(r(0x20, 0x30), 5, 9, 5); + t.add_list(r(0x30, 0x40), 5, 0, 6); + t.add_list(r(0x40, 0x7f), 5, 7, 0); + t.add_list(r(0x20, 0x30), 4, 9, 5); + // esc_intermediate + t.add_list(r(0x20, 0x30), 1, 9, 2); + t.add_list(r(0x20, 0x30), 2, 9, 2); + t.add_list(r(0x30, 0x7f), 2, 10, 0); + t.add_list(r(0x30, 0x50), 1, 10, 0); + t.add_list([0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x59, 0x5a, 0x5c], 1, 10, 0); + t.add_list(r(0x60, 0x7f), 1, 10, 0); + // dcs entry + t.add(0x50, 1, 11, 9); + t.add_list(EXECUTABLES, 9, null, 9); + t.add(0x7f, 9, null, 9); + t.add_list(r(0x1c, 0x20), 9, null, 9); + t.add_list(r(0x20, 0x30), 9, 9, 12); + t.add(0x3a, 9, 0, 11); + t.add_list(r(0x30, 0x3a), 9, 8, 10); + t.add(0x3b, 9, 8, 10); + t.add_list([0x3c, 0x3d, 0x3e, 0x3f], 9, 9, 10); + t.add_list(EXECUTABLES, 11, null, 11); + t.add_list(r(0x20, 0x80), 11, null, 11); + t.add_list(r(0x1c, 0x20), 11, null, 11); + t.add_list(EXECUTABLES, 10, null, 10); + t.add(0x7f, 10, null, 10); + t.add_list(r(0x1c, 0x20), 10, null, 10); + t.add_list(r(0x30, 0x3a), 10, 8, 10); + t.add(0x3b, 10, 8, 10); + t.add_list([0x3a, 0x3c, 0x3d, 0x3e, 0x3f], 10, 0, 11); + t.add_list(r(0x20, 0x30), 10, 9, 12); + t.add_list(EXECUTABLES, 12, null, 12); + t.add(0x7f, 12, null, 12); + t.add_list(r(0x1c, 0x20), 12, null, 12); + t.add_list(r(0x20, 0x30), 12, 9, 12); + t.add_list(r(0x30, 0x40), 12, 0, 11); + t.add_list(r(0x40, 0x7f), 12, 12, 13); + t.add_list(r(0x40, 0x7f), 10, 12, 13); + t.add_list(r(0x40, 0x7f), 9, 12, 13); + t.add_list(EXECUTABLES, 13, 13, 13); + t.add_list(PRINTABLES, 13, 13, 13); + t.add(0x7f, 13, null, 13); + t.add_list([0x1b, 0x9c], 13, 14, 0); + + return t; +})(); + +export class AnsiParser { + public initialState: number; + public currentState: number; + public transitions: TransitionTable; + public osc: string; + public params: number[]; + public collected: string; + public term: any; + constructor(terminal: IParserTerminal) { + this.initialState = 0; + this.currentState = this.initialState | 0; + this.transitions = new TransitionTable(4095); + this.transitions.table.set(TRANSITION_TABLE.table); + this.osc = ''; + this.params = [0]; + this.collected = ''; + this.term = terminal || {}; + let instructions = ['inst_p', 'inst_o', 'inst_x', 'inst_c', + 'inst_e', 'inst_H', 'inst_P', 'inst_U', 'inst_E']; + for (let i = 0; i < instructions.length; ++i) { + if (!(instructions[i] in this.term)) { + this.term[instructions[i]] = function(): void {}; + } + } + } + reset(): void { + this.currentState = this.initialState; + this.osc = ''; + this.params = [0]; + this.collected = ''; + } + parse(s: string): void { + let code = 0; + let transition = 0; + let error = false; + let currentState = this.currentState; + + // local buffers + let printed = -1; + let dcs = -1; + let osc = this.osc; + let collected = this.collected; + let params = this.params; + let table: Uint8Array = this.transitions.table; + + // process input string + let l = s.length; + for (let i = 0; i < l; ++i) { + code = s.charCodeAt(i); + // shortcut for most chars (print action) + if (currentState === 0 && (code > 0x1f && code < 0x80)) { + printed = (~printed) ? printed : i; + continue; + } + if (currentState === 4) { + if (code === 0x3b) { + params.push(0); + continue; + } + if (code > 0x2f && code < 0x39) { + params[params.length - 1] = params[params.length - 1] * 10 + code - 48; + continue; + } + } + transition = ((code < 0xa0) ? (table[currentState << 8 | code]) : 16); + switch (transition >> 4) { + case 2: // print + printed = (~printed) ? printed : i; + break; + case 3: // execute + if (printed + 1) { + this.term.inst_p(s, printed, i); + printed = -1; + } + this.term.inst_x(String.fromCharCode(code)); + break; + case 0: // ignore + // handle leftover print and dcs chars + if (printed + 1) { + this.term.inst_p(s, printed, i); + printed = -1; + } else if (dcs + 1) { + this.term.inst_P(s.substring(dcs, i)); + dcs = -1; + } + break; + case 1: // error + // handle unicode chars in write buffers w'o state change + if (code > 0x9f) { + switch (currentState) { + case 0: // GROUND -> add char to print string + printed = (~printed) ? printed : i; + break; + case 8: // OSC_STRING -> add char to osc string + osc += String.fromCharCode(code); + transition |= 8; + break; + case 6: // CSI_IGNORE -> ignore char + transition |= 6; + break; + case 11: // DCS_IGNORE -> ignore char + transition |= 11; + break; + case 13: // DCS_PASSTHROUGH -> add char to dcs + if (!(~dcs)) dcs = i | 0; + transition |= 13; + break; + default: // real error + error = true; + } + } else { // real error + error = true; + } + if (error) { + if (this.term.inst_E( + { + pos: i, // position in parse string + character: String.fromCharCode(code), // wrong character + state: currentState, // in state + print: printed, // print buffer + dcs: dcs, // dcs buffer + osc: osc, // osc buffer + collect: collected, // collect buffer + params: params // params buffer + })) { + return; + } + error = false; + } + break; + case 7: // csi_dispatch + this.term.inst_c(collected, params, String.fromCharCode(code)); + break; + case 8: // param + if (code === 0x3b) params.push(0); + else params[params.length - 1] = params[params.length - 1] * 10 + code - 48; + break; + case 9: // collect + collected += String.fromCharCode(code); + break; + case 10: // esc_dispatch + this.term.inst_e(collected, String.fromCharCode(code)); + break; + case 11: // clear + if (~printed) { + this.term.inst_p(s, printed, i); + printed = -1; + } + osc = ''; + params = [0]; + collected = ''; + dcs = -1; + break; + case 12: // dcs_hook + this.term.inst_H(collected, params, String.fromCharCode(code)); + break; + case 13: // dcs_put + if (!(~dcs)) dcs = i; + break; + case 14: // dcs_unhook + if (~dcs) this.term.inst_P(s.substring(dcs, i)); + this.term.inst_U(); + if (code === 0x1b) transition |= 1; + osc = ''; + params = [0]; + collected = ''; + dcs = -1; + break; + case 4: // osc_start + if (~printed) { + this.term.inst_p(s, printed, i); + printed = -1; + } + osc = ''; + break; + case 5: // osc_put + osc += s.charAt(i); + break; + case 6: // osc_end + if (osc && code !== 0x18 && code !== 0x1a) this.term.inst_o(osc); + if (code === 0x1b) transition |= 1; + osc = ''; + params = [0]; + collected = ''; + dcs = -1; + break; + } + currentState = transition & 15; + } + + // push leftover pushable buffers to terminal + if (!currentState && (printed + 1)) { + this.term.inst_p(s, printed, s.length); + } else if (currentState === 13 && (dcs + 1)) { + this.term.inst_P(s.substring(dcs)); + } + + // save non pushable buffers + this.osc = osc; + this.collected = collected; + this.params = params; + + // save state + this.currentState = currentState; + } +} + + + + + +import { IInputHandler, IInputHandlingTerminal } from './Types'; +import { CHARSETS, DEFAULT_CHARSET } from './Charsets'; +import { C0 } from './EscapeSequences'; + +// glue code between AnsiParser and Terminal +export class ParserTerminal implements IParserTerminal { + private _parser: AnsiParser; + private _terminal: any; + private _inputHandler: IInputHandler; + + constructor(_terminal: any, _inputHandler: IInputHandler) { + this._parser = new AnsiParser(this); + this._terminal = _terminal; + this._inputHandler = _inputHandler; + } + + write(data: string): void { + const cursorStartX = this._terminal.buffer.x; + const cursorStartY = this._terminal.buffer.y; + if (this._terminal.debug) { + this._terminal.log('data: ' + data); + } + // apply leftover surrogate high from last write + if (this._terminal.surrogate_high) { + data = this._terminal.surrogate_high + data; + this._terminal.surrogate_high = ''; + } + + this._parser.parse(data); + + if (this._terminal.buffer.x !== cursorStartX || this._terminal.buffer.y !== cursorStartY) { + this._terminal.emit('cursormove'); + } + } + + inst_p(data: string, start: number, end: number): void { + // const l = data.length; + let ch; + let code; + let low; + for (let i = start; i < end; ++i) { + ch = data.charAt(i); + code = data.charCodeAt(i); + if (0xD800 <= code && code <= 0xDBFF) { + // we got a surrogate high + // get surrogate low (next 2 bytes) + low = data.charCodeAt(i + 1); + if (isNaN(low)) { + // end of data stream, save surrogate high + this._terminal.surrogate_high = ch; + continue; + } + code = ((code - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000; + ch += data.charAt(i + 1); + } + // surrogate low - already handled above + if (0xDC00 <= code && code <= 0xDFFF) { + continue; + } + this._inputHandler.addChar(ch, code); + } + } + + inst_o(data: string): void { + let params = data.split(';'); + switch (parseInt(params[0])) { + case 0: + case 1: + case 2: + if (params[1]) { + this._terminal.title = params[1]; + this._terminal.handleTitle(this._terminal.title); + } + break; + case 3: + // set X property + break; + case 4: + case 5: + // change dynamic colors + break; + case 10: + case 11: + case 12: + case 13: + case 14: + case 15: + case 16: + case 17: + case 18: + case 19: + // change dynamic ui colors + break; + case 46: + // change log file + break; + case 50: + // dynamic font + break; + case 51: + // emacs shell + break; + case 52: + // manipulate selection data + break; + case 104: + case 105: + case 110: + case 111: + case 112: + case 113: + case 114: + case 115: + case 116: + case 117: + case 118: + // reset colors + break; + } + } + + inst_x(flag: string): void { + switch (flag) { + case C0.BEL: return this._inputHandler.bell(); + case C0.LF: return this._inputHandler.lineFeed(); + case C0.VT: return this._inputHandler.lineFeed(); + case C0.FF: return this._inputHandler.lineFeed(); + case C0.CR: return this._inputHandler.carriageReturn(); + case C0.BS: return this._inputHandler.backspace(); + case C0.HT: return this._inputHandler.tab(); + case C0.SO: return this._inputHandler.shiftOut(); + case C0.SI: return this._inputHandler.shiftIn(); + default: + this._inputHandler.addChar(flag, flag.charCodeAt(0)); + } + this._terminal.error('Unknown EXEC flag: %s.', flag); + } + + inst_c(collected: string, params: number[], flag: string): void { + this._terminal.prefix = collected; + switch (flag) { + case '@': return this._inputHandler.insertChars(params); + case 'A': return this._inputHandler.cursorUp(params); + case 'B': return this._inputHandler.cursorDown(params); + case 'C': return this._inputHandler.cursorForward(params); + case 'D': return this._inputHandler.cursorBackward(params); + case 'E': return this._inputHandler.cursorNextLine(params); + case 'F': return this._inputHandler.cursorPrecedingLine(params); + case 'G': return this._inputHandler.cursorCharAbsolute(params); + case 'H': return this._inputHandler.cursorPosition(params); + case 'I': return this._inputHandler.cursorForwardTab(params); + case 'J': return this._inputHandler.eraseInDisplay(params); + case 'K': return this._inputHandler.eraseInLine(params); + case 'L': return this._inputHandler.insertLines(params); + case 'M': return this._inputHandler.deleteLines(params); + case 'P': return this._inputHandler.deleteChars(params); + case 'S': return this._inputHandler.scrollUp(params); + case 'T': + if (params.length < 2 && !collected) { + return this._inputHandler.scrollDown(params); + } + break; + case 'X': return this._inputHandler.eraseChars(params); + case 'Z': return this._inputHandler.cursorBackwardTab(params); + case '`': return this._inputHandler.charPosAbsolute(params); + case 'a': return this._inputHandler.HPositionRelative(params); + case 'b': return this._inputHandler.repeatPrecedingCharacter(params); + case 'c': return this._inputHandler.sendDeviceAttributes(params); + case 'd': return this._inputHandler.linePosAbsolute(params); + case 'e': return this._inputHandler.VPositionRelative(params); + case 'f': return this._inputHandler.HVPosition(params); + case 'g': return this._inputHandler.tabClear(params); + case 'h': return this._inputHandler.setMode(params); + case 'l': return this._inputHandler.resetMode(params); + case 'm': return this._inputHandler.charAttributes(params); + case 'n': return this._inputHandler.deviceStatus(params); + case 'p': + if (collected === '!') { + return this._inputHandler.softReset(params); + } + break; + case 'q': + if (collected === ' ') { + return this._inputHandler.setCursorStyle(params); + } + break; + case 'r': return this._inputHandler.setScrollRegion(params); + case 's': return this._inputHandler.saveCursor(params); + case 'u': return this._inputHandler.restoreCursor(params); + } + this._terminal.error('Unknown CSI code: %s %s %s.', collected, params, flag); + } + + inst_e(collected: string, flag: string): void { + let cs; + + switch (collected) { + case '': + switch (flag) { + // case '6': // Back Index (DECBI), VT420 and up - not supported + case '7': // Save Cursor (DECSC) + this._inputHandler.saveCursor(); + return; + case '8': // Restore Cursor (DECRC) + this._inputHandler.restoreCursor(); + return; + // case '9': // Forward Index (DECFI), VT420 and up - not supported + case 'D': // Index (IND is 0x84) + this._terminal.index(); + return; + case 'E': // Next Line (NEL is 0x85) + this._terminal.buffer.x = 0; + this._terminal.index(); + return; + case 'H': // ESC H Tab Set (HTS is 0x88) + (this._terminal).tabSet(); + return; + case 'M': // Reverse Index (RI is 0x8d) + this._terminal.reverseIndex(); + return; + case 'N': // Single Shift Select of G2 Character Set ( SS2 is 0x8e) - Is this supported? + case 'O': // Single Shift Select of G3 Character Set ( SS3 is 0x8f) + return; + // case 'P': // Device Control String (DCS is 0x90) - covered by parser + // case 'V': // Start of Guarded Area (SPA is 0x96) - not supported + // case 'W': // End of Guarded Area (EPA is 0x97) - not supported + // case 'X': // Start of String (SOS is 0x98) - covered by parser (unsupported) + // case 'Z': // Return Terminal ID (DECID is 0x9a). Obsolete form of CSI c (DA). - not supported + // case '[': // Control Sequence Introducer (CSI is 0x9b) - covered by parser + // case '\': // String Terminator (ST is 0x9c) - covered by parser + // case ']': // Operating System Command (OSC is 0x9d) - covered by parser + // case '^': // Privacy Message (PM is 0x9e) - covered by parser (unsupported) + // case '_': // Application Program Command (APC is 0x9f) - covered by parser (unsupported) + case '=': // Application Keypad (DECKPAM) + this._terminal.log('Serial port requested application keypad.'); + this._terminal.applicationKeypad = true; + if (this._terminal.viewport) { + this._terminal.viewport.syncScrollArea(); + } + return; + case '>': // Normal Keypad (DECKPNM) + this._terminal.log('Switching back to normal keypad.'); + this._terminal.applicationKeypad = false; + if (this._terminal.viewport) { + this._terminal.viewport.syncScrollArea(); + } + return; + // case 'F': // Cursor to lower left corner of screen + case 'c': // Full Reset (RIS) http://vt100.net/docs/vt220-rm/chapter4.html + this._terminal.reset(); + return; + // case 'l': // Memory Lock (per HP terminals). Locks memory above the cursor. + // case 'm': // Memory Unlock (per HP terminals). + case 'n': // Invoke the G2 Character Set as GL (LS2). + this._terminal.setgLevel(2); + return; + case 'o': // Invoke the G3 Character Set as GL (LS3). + this._terminal.setgLevel(3); + return; + case '|': // Invoke the G3 Character Set as GR (LS3R). + this._terminal.setgLevel(3); + return; + case '}': // Invoke the G2 Character Set as GR (LS2R). + this._terminal.setgLevel(2); + return; + case '~': // Invoke the G1 Character Set as GR (LS1R). + this._terminal.setgLevel(1); + return; + } + // case ' ': + // switch (flag) { + // case 'F': // (SP) 7-bit controls (S7C1T) + // case 'G': // (SP) 8-bit controls (S8C1T) + // case 'L': // (SP) Set ANSI conformance level 1 (dpANS X3.134.1) + // case 'M': // (SP) Set ANSI conformance level 2 (dpANS X3.134.1) + // case 'N': // (SP) Set ANSI conformance level 3 (dpANS X3.134.1) + // } + + // case '#': + // switch (flag) { + // case '3': // DEC double-height line, top half (DECDHL) + // case '4': // DEC double-height line, bottom half (DECDHL) + // case '5': // DEC single-width line (DECSWL) + // case '6': // DEC double-width line (DECDWL) + // case '8': // DEC Screen Alignment Test (DECALN) + // } + + case '%': + // switch (flag) { + // case '@': // (%) Select default character set. That is ISO 8859-1 (ISO 2022) + // case 'G': // (%) Select UTF-8 character set (ISO 2022) + // } + this._terminal.setgLevel(0); + this._terminal.setgCharset(0, DEFAULT_CHARSET); // US (default) + return; + + // load character sets + case '(': // G0 (VT100) + cs = CHARSETS[flag]; + if (!cs) cs = DEFAULT_CHARSET; + this._terminal.setgCharset(0, cs); + return; + case ')': // G1 (VT100) + cs = CHARSETS[flag]; + if (!cs) cs = DEFAULT_CHARSET; + this._terminal.setgCharset(1, cs); + return; + case '*': // G2 (VT220) + cs = CHARSETS[flag]; + if (!cs) cs = DEFAULT_CHARSET; + this._terminal.setgCharset(2, cs); + return; + case '+': // G3 (VT220) + cs = CHARSETS[flag]; + if (!cs) cs = DEFAULT_CHARSET; + this._terminal.setgCharset(3, cs); + return; + case '-': // G1 (VT300) + cs = CHARSETS[flag]; + if (!cs) cs = DEFAULT_CHARSET; + this._terminal.setgCharset(1, cs); + return; + case '.': // G2 (VT300) + if (!cs) cs = DEFAULT_CHARSET; + this._terminal.setgCharset(2, cs); + return; + case '/': // G3 (VT300) + // not supported - how to deal with this? (original code is not reachable) + return; + default: + this._terminal.error('Unknown ESC control: %s %s.', collected, flag); + } + } + + inst_H(collected: string, params: number[], flag: string): void { + // TODO + } + + inst_P(dcs: string): void { + // TODO + } + + inst_U(): void { + // TODO + } + + inst_E(): void { + // TODO + } +} diff --git a/src/InputHandler.ts b/src/InputHandler.ts index acf7af1f..3d29f7b4 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -22,11 +22,14 @@ export class InputHandler implements IInputHandler { constructor(private _terminal: IInputHandlingTerminal) { } public addChar(char: string, code: number): void { - if (char >= ' ') { + // if (char >= ' ') { // calculate print space // expensive call, therefore we save width in line buffer const chWidth = wcwidth(code); + // localize buffer + let buffer = this._terminal.buffer; + if (this._terminal.charset && this._terminal.charset[char]) { char = this._terminal.charset[char]; } @@ -35,42 +38,42 @@ export class InputHandler implements IInputHandler { this._terminal.emit('a11y.char', char); } - let row = this._terminal.buffer.y + this._terminal.buffer.ybase; + let row = buffer.y + buffer.ybase; // insert combining char in last cell // FIXME: needs handling after cursor jumps - if (!chWidth && this._terminal.buffer.x) { + if (!chWidth && buffer.x) { // dont overflow left - if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1]) { - if (!this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) { + if (buffer.lines.get(row)[buffer.x - 1]) { + if (!buffer.lines.get(row)[buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) { // found empty cell after fullwidth, need to go 2 cells back - if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2]) { - this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char; - this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2][3] = char.charCodeAt(0); + if (buffer.lines.get(row)[buffer.x - 2]) { + buffer.lines.get(row)[buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char; + buffer.lines.get(row)[buffer.x - 2][3] = char.charCodeAt(0); } } else { - this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char; - this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][3] = char.charCodeAt(0); + buffer.lines.get(row)[buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char; + buffer.lines.get(row)[buffer.x - 1][3] = char.charCodeAt(0); } - this._terminal.updateRange(this._terminal.buffer.y); + this._terminal.updateRange(buffer.y); } return; } // goto next line if ch would overflow // TODO: needs a global min terminal width of 2 - if (this._terminal.buffer.x + chWidth - 1 >= this._terminal.cols) { + if (buffer.x + chWidth - 1 >= this._terminal.cols) { // autowrap - DECAWM if (this._terminal.wraparoundMode) { - this._terminal.buffer.x = 0; - this._terminal.buffer.y++; - if (this._terminal.buffer.y > this._terminal.buffer.scrollBottom) { - this._terminal.buffer.y--; + buffer.x = 0; + buffer.y++; + if (buffer.y > buffer.scrollBottom) { + buffer.y--; this._terminal.scroll(true); } else { // The line already exists (eg. the initial viewport), mark it as a // wrapped line - (this._terminal.buffer.lines.get(this._terminal.buffer.y)).isWrapped = true; + (buffer.lines.get(buffer.y)).isWrapped = true; } } else { if (chWidth === 2) { // FIXME: check for xterm behavior @@ -78,7 +81,7 @@ export class InputHandler implements IInputHandler { } } } - row = this._terminal.buffer.y + this._terminal.buffer.ybase; + row = buffer.y + buffer.ybase; // insert mode: move characters to right if (this._terminal.insertMode) { @@ -86,28 +89,28 @@ export class InputHandler implements IInputHandler { for (let moves = 0; moves < chWidth; ++moves) { // remove last cell, if it's width is 0 // we have to adjust the second last cell as well - const removed = this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).pop(); + const removed = buffer.lines.get(buffer.y + buffer.ybase).pop(); if (removed[CHAR_DATA_WIDTH_INDEX] === 0 - && this._terminal.buffer.lines.get(row)[this._terminal.cols - 2] - && this._terminal.buffer.lines.get(row)[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) { - this._terminal.buffer.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]; + && buffer.lines.get(row)[this._terminal.cols - 2] + && buffer.lines.get(row)[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) { + buffer.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]; } // insert empty cell at cursor - this._terminal.buffer.lines.get(row).splice(this._terminal.buffer.x, 0, [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]); + buffer.lines.get(row).splice(buffer.x, 0, [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]); } } - this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, char, chWidth, char.charCodeAt(0)]; - this._terminal.buffer.x++; - this._terminal.updateRange(this._terminal.buffer.y); + buffer.lines.get(row)[buffer.x] = [this._terminal.curAttr, char, chWidth, char.charCodeAt(0)]; + buffer.x++; + this._terminal.updateRange(buffer.y); // fullwidth char - set next cell width to zero and advance cursor if (chWidth === 2) { - this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, '', 0, undefined]; - this._terminal.buffer.x++; + buffer.lines.get(row)[buffer.x] = [this._terminal.curAttr, '', 0, undefined]; + buffer.x++; } - } + // } } /** diff --git a/src/Terminal.ts b/src/Terminal.ts index 233b8a17..5bec8a9a 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -32,7 +32,7 @@ import { Viewport } from './Viewport'; import { rightClickHandler, moveTextAreaUnderMouseCursor, pasteHandler, copyHandler } from './handlers/Clipboard'; import { C0 } from './EscapeSequences'; import { InputHandler } from './InputHandler'; -import { Parser } from './Parser'; +// import { Parser } from './Parser'; import { Renderer } from './renderer/Renderer'; import { Linkifier } from './Linkifier'; import { SelectionManager } from './SelectionManager'; @@ -47,6 +47,7 @@ import { MouseZoneManager } from './input/MouseZoneManager'; import { AccessibilityManager } from './AccessibilityManager'; import { ScreenDprMonitor } from './utils/ScreenDprMonitor'; import { ITheme, ILocalizableStrings, IMarker } from 'xterm'; +import { ParserTerminal } from './EscapeSequenceParser'; // reg + shift key mappings for digits and special chars const KEYCODE_KEY_MAPPINGS = { @@ -212,7 +213,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT private _inputHandler: InputHandler; public soundManager: SoundManager; - private _parser: Parser; + // private _parser: Parser; + private _newParser: ParserTerminal; public renderer: IRenderer; public selectionManager: SelectionManager; public linkifier: ILinkifier; @@ -306,7 +308,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this._userScrolling = false; this._inputHandler = new InputHandler(this); - this._parser = new Parser(this._inputHandler, this); + // this._parser = new Parser(this._inputHandler, this); + this._newParser = new ParserTerminal(this, this._inputHandler); // Reuse renderer if the Terminal is being recreated via a reset call. this.renderer = this.renderer || null; this.selectionManager = this.selectionManager || null; @@ -1303,8 +1306,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // middle of parsing escape sequence in two chunks. For some reason the // state of the parser resets to 0 after exiting parser.parse. This change // just sets the state back based on the correct return statement. - const state = this._parser.parse(data); - this._parser.setState(state); + + // const state = this._parser.parse(data); + this._newParser.write(data); + // this._parser.setState(state); this.updateRange(this.buffer.y); this.refresh(this._refreshStart, this._refreshEnd); From 9b085d2bddaec865039d859b8364a12fcc92cc10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 21 Apr 2018 05:12:24 +0200 Subject: [PATCH 2/3] new parser --- src/EscapeSequenceParser.test.ts | 1042 ++++++++++++++++++++++++++++++ src/EscapeSequenceParser.ts | 718 ++++++++++++++++++++ src/InputHandler.ts | 61 +- src/Terminal.ts | 15 +- 4 files changed, 1802 insertions(+), 34 deletions(-) create mode 100644 src/EscapeSequenceParser.test.ts create mode 100644 src/EscapeSequenceParser.ts diff --git a/src/EscapeSequenceParser.test.ts b/src/EscapeSequenceParser.test.ts new file mode 100644 index 00000000..eefa7603 --- /dev/null +++ b/src/EscapeSequenceParser.test.ts @@ -0,0 +1,1042 @@ +import { AnsiParser, IParserTerminal } from './EscapeSequenceParser'; +import * as chai from 'chai'; + +function r(a: number, b: number): string[] { + let c = b - a; + let arr = new Array(c); + while (c--) { + arr[c] = String.fromCharCode(--b); + } + return arr; +} + +interface ITestTerminal extends IParserTerminal { + calls: any[]; + clear: () => void; + compare: (value: any) => void; +} + +let testTerminal: ITestTerminal = { + calls: [], + clear: function (): void { + this.calls = []; + }, + compare: function (value: any): void { + chai.expect(this.calls.slice()).eql(value); // weird bug w'o slicing here + }, + inst_p: function (s: string, start: number, end: number): void { + this.calls.push(['print', s.substring(start, end)]); + }, + inst_o: function (s: string): void { + this.calls.push(['osc', s]); + }, + inst_x: function (flag: string): void { + this.calls.push(['exe', flag]); + }, + inst_c: function (collected: string, params: number[], flag: string): void { + this.calls.push(['csi', collected, params, flag]); + }, + inst_e: function (collected: string, flag: string): void { + this.calls.push(['esc', collected, flag]); + }, + inst_H: function (collected: string, params: number[], flag: string): void { + this.calls.push(['dcs hook', collected, params, flag]); + }, + inst_P: function (dcs: string): void { + this.calls.push(['dcs put', dcs]); + }, + inst_U: function (): void { + this.calls.push(['dcs unhook']); + } +}; + +let parser = new AnsiParser(testTerminal); + +describe('Parser init and methods', function(): void { + it('parser init', function (): void { + let p = new AnsiParser({}); + chai.expect(p.term).a('object'); + chai.expect(p.term.inst_p).a('function'); + chai.expect(p.term.inst_o).a('function'); + chai.expect(p.term.inst_x).a('function'); + chai.expect(p.term.inst_c).a('function'); + chai.expect(p.term.inst_e).a('function'); + chai.expect(p.term.inst_H).a('function'); + chai.expect(p.term.inst_P).a('function'); + chai.expect(p.term.inst_U).a('function'); + p.parse('\x1b[31mHello World!'); + }); + it('terminal callbacks', function (): void { + chai.expect(parser.term).equal(testTerminal); + chai.expect(parser.term.inst_p).equal(testTerminal.inst_p); + chai.expect(parser.term.inst_o).equal(testTerminal.inst_o); + chai.expect(parser.term.inst_x).equal(testTerminal.inst_x); + chai.expect(parser.term.inst_c).equal(testTerminal.inst_c); + chai.expect(parser.term.inst_e).equal(testTerminal.inst_e); + chai.expect(parser.term.inst_H).equal(testTerminal.inst_H); + chai.expect(parser.term.inst_P).equal(testTerminal.inst_P); + chai.expect(parser.term.inst_U).equal(testTerminal.inst_U); + }); + it('inital states', function (): void { + chai.expect(parser.initialState).equal(0); + chai.expect(parser.currentState).equal(0); + chai.expect(parser.osc).equal(''); + chai.expect(parser.params).eql([0]); + chai.expect(parser.collected).equal(''); + }); + it('reset states', function (): void { + parser.currentState = 124; + parser.osc = '#'; + parser.params = [123]; + parser.collected = '#'; + + parser.reset(); + chai.expect(parser.currentState).equal(0); + chai.expect(parser.osc).equal(''); + chai.expect(parser.params).eql([0]); + chai.expect(parser.collected).equal(''); + }); +}); + +describe('state transitions and actions', function(): void { + it('state GROUND execute action', function (): void { + parser.reset(); + testTerminal.clear(); + let exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = 0; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(0); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state GROUND print action', function (): void { + parser.reset(); + testTerminal.clear(); + let printables = r(0x20, 0x7f); // NOTE: DEL excluded + for (let i = 0; i < printables.length; ++i) { + parser.currentState = 0; + parser.parse(printables[i]); + chai.expect(parser.currentState).equal(0); + testTerminal.compare([['print', printables[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans ANYWHERE --> GROUND with actions', function (): void { + let exes = [ + '\x18', '\x1a', + '\x80', '\x81', '\x82', '\x83', '\x84', '\x85', '\x86', '\x87', '\x88', + '\x89', '\x8a', '\x8b', '\x8c', '\x8d', '\x8e', '\x8f', + '\x91', '\x92', '\x93', '\x94', '\x95', '\x96', '\x97', '\x99', '\x9a' + ]; + let exceptions = { + 8: {'\x18': [], '\x1a': []} // simply abort osc state + }; + parser.reset(); + testTerminal.clear(); + for (let state = 0; state < 14; ++state) { + for (let i = 0; i < exes.length; ++i) { + parser.currentState = state; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(0); + testTerminal.compare(((exceptions[state]) ? exceptions[state][exes[i]] : 0) || [['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + parser.parse('\x9c'); + chai.expect(parser.currentState).equal(0); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans ANYWHERE --> ESCAPE with clear', function (): void { + parser.reset(); + for (let state = 0; state < 14; ++state) { + parser.currentState = state; + parser.osc = '#'; + parser.params = [23]; + parser.collected = '#'; + parser.parse('\x1b'); + chai.expect(parser.currentState).equal(1); + chai.expect(parser.osc).equal(''); + chai.expect(parser.params).eql([0]); + chai.expect(parser.collected).equal(''); + parser.reset(); + } + }); + it('state ESCAPE execute rules', function (): void { + parser.reset(); + testTerminal.clear(); + let exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = 1; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(1); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state ESCAPE ignore', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = 1; + parser.parse('\x7f'); + chai.expect(parser.currentState).equal(1); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('trans ESCAPE --> GROUND with ecs_dispatch action', function (): void { + parser.reset(); + testTerminal.clear(); + let dispatches = r(0x30, 0x50); + dispatches.concat(r(0x51, 0x58)); + dispatches.concat(['\x59', '\x5a', '\x5c']); + dispatches.concat(r(0x60, 0x7f)); + for (let i = 0; i < dispatches.length; ++i) { + parser.currentState = 1; + parser.parse(dispatches[i]); + chai.expect(parser.currentState).equal(0); + testTerminal.compare([['esc', '', dispatches[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans ESCAPE --> ESCAPE_INTERMEDIATE with collect action', function (): void { + parser.reset(); + let collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = 1; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(2); + chai.expect(parser.collected).equal(collect[i]); + parser.reset(); + } + }); + it('state ESCAPE_INTERMEDIATE execute rules', function (): void { + parser.reset(); + testTerminal.clear(); + let exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = 2; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(2); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state ESCAPE_INTERMEDIATE ignore', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = 2; + parser.parse('\x7f'); + chai.expect(parser.currentState).equal(2); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('state ESCAPE_INTERMEDIATE collect action', function (): void { + parser.reset(); + let collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = 2; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(2); + chai.expect(parser.collected).equal(collect[i]); + parser.reset(); + } + }); + it('trans ESCAPE_INTERMEDIATE --> GROUND with esc_dispatch action', function (): void { + parser.reset(); + testTerminal.clear(); + let collect = r(0x30, 0x7f); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = 2; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(0); + testTerminal.compare([['esc', '', collect[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans ANYWHERE/ESCAPE --> CSI_ENTRY with clear', function (): void { + parser.reset(); + // C0 + parser.currentState = 1; + parser.osc = '#'; + parser.params = [123]; + parser.collected = '#'; + parser.parse('['); + chai.expect(parser.currentState).equal(3); + chai.expect(parser.osc).equal(''); + chai.expect(parser.params).eql([0]); + chai.expect(parser.collected).equal(''); + parser.reset(); + // C1 + for (let state = 0; state < 14; ++state) { + parser.currentState = state; + parser.osc = '#'; + parser.params = [123]; + parser.collected = '#'; + parser.parse('\x9b'); + chai.expect(parser.currentState).equal(3); + chai.expect(parser.osc).equal(''); + chai.expect(parser.params).eql([0]); + chai.expect(parser.collected).equal(''); + parser.reset(); + } + }); + it('state CSI_ENTRY execute rules', function (): void { + parser.reset(); + testTerminal.clear(); + let exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = 3; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(3); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state CSI_ENTRY ignore', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = 3; + parser.parse('\x7f'); + chai.expect(parser.currentState).equal(3); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('trans CSI_ENTRY --> GROUND with csi_dispatch action', function (): void { + parser.reset(); + let dispatches = r(0x40, 0x7f); + for (let i = 0; i < dispatches.length; ++i) { + parser.currentState = 3; + parser.parse(dispatches[i]); + chai.expect(parser.currentState).equal(0); + testTerminal.compare([['csi', '', [0], dispatches[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans CSI_ENTRY --> CSI_PARAM with param/collect actions', function (): void { + parser.reset(); + let params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; + let collect = ['\x3c', '\x3d', '\x3e', '\x3f']; + for (let i = 0; i < params.length; ++i) { + parser.currentState = 3; + parser.parse(params[i]); + chai.expect(parser.currentState).equal(4); + chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); + parser.reset(); + } + // ';' + parser.currentState = 3; + parser.parse('\x3b'); + chai.expect(parser.currentState).equal(4); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = 3; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(4); + chai.expect(parser.collected).equal(collect[i]); + parser.reset(); + } + }); + it('state CSI_PARAM execute rules', function (): void { + parser.reset(); + testTerminal.clear(); + let exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = 4; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(4); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state CSI_PARAM param action', function (): void { + parser.reset(); + let params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; + for (let i = 0; i < params.length; ++i) { + parser.currentState = 4; + parser.parse(params[i]); + chai.expect(parser.currentState).equal(4); + chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); + parser.reset(); + } + parser.currentState = 4; + parser.parse('\x3b'); + chai.expect(parser.currentState).equal(4); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + }); + it('state CSI_PARAM ignore', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = 4; + parser.parse('\x7f'); + chai.expect(parser.currentState).equal(4); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('trans CSI_PARAM --> GROUND with csi_dispatch action', function (): void { + parser.reset(); + let dispatches = r(0x40, 0x7f); + for (let i = 0; i < dispatches.length; ++i) { + parser.currentState = 4; + parser.params = [0, 1]; + parser.parse(dispatches[i]); + chai.expect(parser.currentState).equal(0); + testTerminal.compare([['csi', '', [0, 1], dispatches[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans CSI_ENTRY --> CSI_INTERMEDIATE with collect action', function (): void { + parser.reset(); + let collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = 3; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(5); + chai.expect(parser.collected).equal(collect[i]); + parser.reset(); + } + }); + it('trans CSI_PARAM --> CSI_INTERMEDIATE with collect action', function (): void { + parser.reset(); + let collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = 4; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(5); + chai.expect(parser.collected).equal(collect[i]); + parser.reset(); + } + }); + it('state CSI_INTERMEDIATE execute rules', function (): void { + parser.reset(); + testTerminal.clear(); + let exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = 5; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(5); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state CSI_INTERMEDIATE collect', function (): void { + parser.reset(); + let collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = 5; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(5); + chai.expect(parser.collected).equal(collect[i]); + parser.reset(); + } + }); + it('state CSI_INTERMEDIATE ignore', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = 5; + parser.parse('\x7f'); + chai.expect(parser.currentState).equal(5); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('trans CSI_INTERMEDIATE --> GROUND with csi_dispatch action', function (): void { + parser.reset(); + let dispatches = r(0x40, 0x7f); + for (let i = 0; i < dispatches.length; ++i) { + parser.currentState = 5; + parser.params = [0, 1]; + parser.parse(dispatches[i]); + chai.expect(parser.currentState).equal(0); + testTerminal.compare([['csi', '', [0, 1], dispatches[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans CSI_ENTRY --> CSI_IGNORE', function (): void { + parser.reset(); + parser.currentState = 3; + parser.parse('\x3a'); + chai.expect(parser.currentState).equal(6); + parser.reset(); + }); + it('trans CSI_PARAM --> CSI_IGNORE', function (): void { + parser.reset(); + let chars = ['\x3a', '\x3c', '\x3d', '\x3e', '\x3f']; + for (let i = 0; i < chars.length; ++i) { + parser.currentState = 4; + parser.parse('\x3b' + chars[i]); + chai.expect(parser.currentState).equal(6); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + } + }); + it('trans CSI_INTERMEDIATE --> CSI_IGNORE', function (): void { + parser.reset(); + let chars = r(0x30, 0x40); + for (let i = 0; i < chars.length; ++i) { + parser.currentState = 5; + parser.parse(chars[i]); + chai.expect(parser.currentState).equal(6); + chai.expect(parser.params).eql([0]); + parser.reset(); + } + }); + it('state CSI_IGNORE execute rules', function (): void { + parser.reset(); + testTerminal.clear(); + let exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = 6; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(6); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state CSI_IGNORE ignore', function (): void { + parser.reset(); + testTerminal.clear(); + let ignored = r(0x20, 0x40); + ignored.concat(['\x7f']); + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = 6; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(6); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans CSI_IGNORE --> GROUND', function (): void { + parser.reset(); + let dispatches = r(0x40, 0x7f); + for (let i = 0; i < dispatches.length; ++i) { + parser.currentState = 6; + parser.params = [0, 1]; + parser.parse(dispatches[i]); + chai.expect(parser.currentState).equal(0); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans ANYWHERE/ESCAPE --> SOS_PM_APC_STRING', function (): void { + parser.reset(); + // C0 + let initializers = ['\x58', '\x5e', '\x5f']; + for (let i = 0; i < initializers.length; ++i) { + parser.parse('\x1b' + initializers[i]); + chai.expect(parser.currentState).equal(7); + parser.reset(); + } + // C1 + for (let state = 0; state < 14; ++state) { + parser.currentState = state; + initializers = ['\x98', '\x9e', '\x9f']; + for (let i = 0; i < initializers.length; ++i) { + parser.parse(initializers[i]); + chai.expect(parser.currentState).equal(7); + parser.reset(); + } + } + }); + it('state SOS_PM_APC_STRING ignore rules', function (): void { + parser.reset(); + let ignored = r(0x00, 0x18); + ignored.concat(['\x19']); + ignored.concat(r(0x1c, 0x20)); + ignored.concat(r(0x20, 0x80)); + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = 7; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(7); + parser.reset(); + } + }); + it('trans ANYWHERE/ESCAPE --> OSC_STRING', function (): void { + parser.reset(); + // C0 + parser.parse('\x1b]'); + chai.expect(parser.currentState).equal(8); + parser.reset(); + // C1 + for (let state = 0; state < 14; ++state) { + parser.currentState = state; + parser.parse('\x9d'); + chai.expect(parser.currentState).equal(8); + parser.reset(); + } + }); + it('state OSC_STRING ignore rules', function (): void { + parser.reset(); + let ignored = [ + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', /*'\x07',*/ '\x08', + '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', + '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f']; + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = 8; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(8); + chai.expect(parser.osc).equal(''); + parser.reset(); + } + }); + it('state OSC_STRING put action', function (): void { + parser.reset(); + let puts = r(0x20, 0x80); + for (let i = 0; i < puts.length; ++i) { + parser.currentState = 8; + parser.parse(puts[i]); + chai.expect(parser.currentState).equal(8); + chai.expect(parser.osc).equal(puts[i]); + parser.reset(); + } + }); + it('state DCS_ENTRY', function (): void { + parser.reset(); + // C0 + parser.parse('\x1bP'); + chai.expect(parser.currentState).equal(9); + parser.reset(); + // C1 + for (let state = 0; state < 14; ++state) { + parser.currentState = state; + parser.parse('\x90'); + chai.expect(parser.currentState).equal(9); + parser.reset(); + } + }); + it('state DCS_ENTRY ignore rules', function (): void { + parser.reset(); + let ignored = [ + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', + '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', + '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = 9; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(9); + parser.reset(); + } + }); + it('state DCS_ENTRY --> DCS_PARAM with param/collect actions', function (): void { + parser.reset(); + let params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; + let collect = ['\x3c', '\x3d', '\x3e', '\x3f']; + for (let i = 0; i < params.length; ++i) { + parser.currentState = 9; + parser.parse(params[i]); + chai.expect(parser.currentState).equal(10); + chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); + parser.reset(); + } + parser.currentState = 9; + parser.parse('\x3b'); + chai.expect(parser.currentState).equal(10); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = 9; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(10); + chai.expect(parser.collected).equal(collect[i]); + parser.reset(); + } + }); + it('state DCS_PARAM ignore rules', function (): void { + parser.reset(); + let ignored = [ + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', + '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', + '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = 10; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(10); + parser.reset(); + } + }); + it('state DCS_PARAM param action', function (): void { + parser.reset(); + let params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; + for (let i = 0; i < params.length; ++i) { + parser.currentState = 10; + parser.parse(params[i]); + chai.expect(parser.currentState).equal(10); + chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); + parser.reset(); + } + parser.currentState = 10; + parser.parse('\x3b'); + chai.expect(parser.currentState).equal(10); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + }); + it('trans DCS_ENTRY --> DCS_IGNORE', function (): void { + parser.reset(); + parser.currentState = 9; + parser.parse('\x3a'); + chai.expect(parser.currentState).equal(11); + parser.reset(); + }); + it('trans DCS_PARAM --> DCS_IGNORE', function (): void { + parser.reset(); + let chars = ['\x3a', '\x3c', '\x3d', '\x3e', '\x3f']; + for (let i = 0; i < chars.length; ++i) { + parser.currentState = 10; + parser.parse('\x3b' + chars[i]); + chai.expect(parser.currentState).equal(11); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + } + }); + it('trans DCS_INTERMEDIATE --> DCS_IGNORE', function (): void { + parser.reset(); + let chars = r(0x30, 0x40); + for (let i = 0; i < chars.length; ++i) { + parser.currentState = 12; + parser.parse(chars[i]); + chai.expect(parser.currentState).equal(11); + parser.reset(); + } + }); + it('state DCS_IGNORE ignore rules', function (): void { + parser.reset(); + let ignored = [ + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', + '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', + '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; + ignored.concat(r(0x20, 0x80)); + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = 11; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(11); + parser.reset(); + } + }); + it('trans DCS_ENTRY --> DCS_INTERMEDIATE with collect action', function (): void { + parser.reset(); + let collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = 9; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(12); + chai.expect(parser.collected).equal(collect[i]); + parser.reset(); + } + }); + it('trans DCS_PARAM --> DCS_INTERMEDIATE with collect action', function (): void { + parser.reset(); + let collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = 10; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(12); + chai.expect(parser.collected).equal(collect[i]); + parser.reset(); + } + }); + it('state DCS_INTERMEDIATE ignore rules', function (): void { + parser.reset(); + let ignored = [ + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', + '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', + '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = 12; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(12); + parser.reset(); + } + }); + it('state DCS_INTERMEDIATE collect action', function (): void { + parser.reset(); + let collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = 12; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(12); + chai.expect(parser.collected).equal(collect[i]); + parser.reset(); + } + }); + it('trans DCS_INTERMEDIATE --> DCS_IGNORE', function (): void { + parser.reset(); + let chars = r(0x30, 0x40); + for (let i = 0; i < chars.length; ++i) { + parser.currentState = 12; + parser.parse('\x20' + chars[i]); + chai.expect(parser.currentState).equal(11); + chai.expect(parser.collected).equal('\x20'); + parser.reset(); + } + }); + it('trans DCS_ENTRY --> DCS_PASSTHROUGH with hook', function (): void { + parser.reset(); + testTerminal.clear(); + let collect = r(0x40, 0x7f); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = 9; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(13); + testTerminal.compare([['dcs hook', '', [0], collect[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans DCS_PARAM --> DCS_PASSTHROUGH with hook', function (): void { + parser.reset(); + testTerminal.clear(); + let collect = r(0x40, 0x7f); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = 10; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(13); + testTerminal.compare([['dcs hook', '', [0], collect[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans DCS_INTERMEDIATE --> DCS_PASSTHROUGH with hook', function (): void { + parser.reset(); + testTerminal.clear(); + let collect = r(0x40, 0x7f); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = 12; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(13); + testTerminal.compare([['dcs hook', '', [0], collect[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state DCS_PASSTHROUGH put action', function (): void { + parser.reset(); + testTerminal.clear(); + let puts = r(0x00, 0x18); + puts.concat(['\x19']); + puts.concat(r(0x1c, 0x20)); + puts.concat(r(0x20, 0x7f)); + for (let i = 0; i < puts.length; ++i) { + parser.currentState = 13; + parser.parse(puts[i]); + chai.expect(parser.currentState).equal(13); + testTerminal.compare([['dcs put', puts[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state DCS_PASSTHROUGH ignore', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = 13; + parser.parse('\x7f'); + chai.expect(parser.currentState).equal(13); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); +}); + +function test(s: string, value: any, noReset: any): void { + if (!noReset) { + parser.reset(); + testTerminal.clear(); + } + parser.parse(s); + testTerminal.compare(value); +} + +describe('escape sequence examples', function(): void { + it('CSI with print and execute', function (): void { + test('\x1b[<31;5mHello World! öäü€\nabc', + [ + ['csi', '<', [31, 5], 'm'], + ['print', 'Hello World! öäü€'], + ['exe', '\n'], + ['print', 'abc'] + ], null); + }); + it('OSC', function (): void { + test('\x1b]0;abc123€öäü\x07', [ + ['osc', '0;abc123€öäü'] + ], null); + }); + it('single DCS', function (): void { + test('\x1bP1;2;3+$abc;de\x9c', [ + ['dcs hook', '+$', [1, 2, 3], 'a'], + ['dcs put', 'bc;de'], + ['dcs unhook'] + ], null); + }); + it('multi DCS', function (): void { + test('\x1bP1;2;3+$abc;de', [ + ['dcs hook', '+$', [1, 2, 3], 'a'], + ['dcs put', 'bc;de'] + ], null); + testTerminal.clear(); + test('abc\x9c', [ + ['dcs put', 'abc'], + ['dcs unhook'] + ], true); + }); + it('print + DCS(C1)', function (): void { + test('abc\x901;2;3+$abc;de\x9c', [ + ['print', 'abc'], + ['dcs hook', '+$', [1, 2, 3], 'a'], + ['dcs put', 'bc;de'], + ['dcs unhook'] + ], null); + }); + it('print + PM(C1) + print', function (): void { + test('abc\x98123tzf\x9cdefg', [ + ['print', 'abc'], + ['print', 'defg'] + ], null); + }); + it('print + OSC(C1) + print', function (): void { + test('abc\x9d123tzf\x9cdefg', [ + ['print', 'abc'], + ['osc', '123tzf'], + ['print', 'defg'] + ], null); + }); + it('error recovery', function (): void { + test('\x1b[1€abcdefg\x9b<;c', [ + ['print', 'abcdefg'], + ['csi', '<', [0, 0], 'c'] + ], null); + }); +}); + +describe('coverage tests', function(): void { + it('CSI_IGNORE error', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = 6; + parser.parse('€öäü'); + chai.expect(parser.currentState).equal(6); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('DCS_IGNORE error', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = 11; + parser.parse('€öäü'); + chai.expect(parser.currentState).equal(11); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('DCS_PASSTHROUGH error', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = 13; + parser.parse('€öäü'); + chai.expect(parser.currentState).equal(13); + testTerminal.compare([['dcs put', '€öäü']]); + parser.reset(); + testTerminal.clear(); + }); + it('error else of if (code > 159)', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = 0; + parser.parse('\x1e'); + chai.expect(parser.currentState).equal(0); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); +}); + +let errorTerminal1 = function(): void {}; +errorTerminal1.prototype = testTerminal; +let errTerminal1 = new errorTerminal1(); +errTerminal1.inst_E = function(e: any): void { + this.calls.push(['error', e]); +}; +let errParser1 = new AnsiParser(errTerminal1); + +let errorTerminal2 = function(): void {}; +errorTerminal2.prototype = testTerminal; +let errTerminal2 = new errorTerminal2(); +errTerminal2.inst_E = function(e: any): any { + this.calls.push(['error', e]); + return true; // --> abort parsing +}; +let errParser2 = new AnsiParser(errTerminal2); + +describe('error tests', function(): void { + it('CSI_PARAM unicode error - inst_E output w/o abort', function (): void { + errParser1.parse('\x1b[<31;5€normal print'); + errTerminal1.compare([ + ['error', { + pos: 7, + character: '€', + state: 4, + print: -1, + dcs: -1, + osc: '', + collect: '<', + params: [31, 5]}], + ['print', 'normal print'] + ]); + parser.reset(); + testTerminal.clear(); + }); + it('CSI_PARAM unicode error - inst_E output with abort', function (): void { + errParser2.parse('\x1b[<31;5€no print'); + errTerminal2.compare([ + ['error', { + pos: 7, + character: '€', + state: 4, + print: -1, + dcs: -1, + osc: '', + collect: '<', + params: [31, 5]}] + ]); + parser.reset(); + testTerminal.clear(); + }); +}); diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts new file mode 100644 index 00000000..6facddea --- /dev/null +++ b/src/EscapeSequenceParser.ts @@ -0,0 +1,718 @@ +export interface IParserTerminal { + inst_p?: (s: string, start: number, end: number) => void; + inst_o?: (s: string) => void; + inst_x?: (flag: string) => void; + inst_c?: (collected: string, params: number[], flag: string) => void; + inst_e?: (collected: string, flag: string) => void; + inst_H?: (collected: string, params: number[], flag: string) => void; + inst_P?: (dcs: string) => void; + inst_U?: () => void; + inst_E?: () => void; // TODO: real signature +} + + +export function r(a: number, b: number): number[] { + let c = b - a; + let arr = new Array(c); + while (c--) { + arr[c] = --b; + } + return arr; +} + + +export class TransitionTable { + public table: Uint8Array; + constructor(length: number) { + this.table = new Uint8Array(length); + } + add(inp: number, state: number, action: number | null, next: number | null): void { + this.table[state << 8 | inp] = ((action | 0) << 4) | ((next === undefined) ? state : next); + } + add_list(inps: number[], state: number, action: number | null, next: number | null): void { + for (let i = 0; i < inps.length; i++) { + this.add(inps[i], state, action, next); + } + } +} + + +let PRINTABLES = r(0x20, 0x7f); +let EXECUTABLES = r(0x00, 0x18); +EXECUTABLES.push(0x19); +EXECUTABLES.concat(r(0x1c, 0x20)); + + +export const TRANSITION_TABLE = (function (): TransitionTable { + let t: TransitionTable = new TransitionTable(4095); + + // table with default transition [any] --> [error, GROUND] + for (let state = 0; state < 14; ++state) { + for (let code = 0; code < 160; ++code) { + t[state << 8 | code] = 16; + } + } + + // apply transitions + // printables + t.add_list(PRINTABLES, 0, 2, 0); + // global anywhere rules + for (let state = 0; state < 14; ++state) { + t.add_list([0x18, 0x1a, 0x99, 0x9a], state, 3, 0); + t.add_list(r(0x80, 0x90), state, 3, 0); + t.add_list(r(0x90, 0x98), state, 3, 0); + t.add(0x9c, state, 0, 0); // ST as terminator + t.add(0x1b, state, 11, 1); // ESC + t.add(0x9d, state, 4, 8); // OSC + t.add_list([0x98, 0x9e, 0x9f], state, 0, 7); + t.add(0x9b, state, 11, 3); // CSI + t.add(0x90, state, 11, 9); // DCS + } + // rules for executables and 7f + t.add_list(EXECUTABLES, 0, 3, 0); + t.add_list(EXECUTABLES, 1, 3, 1); + t.add(0x7f, 1, null, 1); + t.add_list(EXECUTABLES, 8, null, 8); + t.add_list(EXECUTABLES, 3, 3, 3); + t.add(0x7f, 3, null, 3); + t.add_list(EXECUTABLES, 4, 3, 4); + t.add(0x7f, 4, null, 4); + t.add_list(EXECUTABLES, 6, 3, 6); + t.add_list(EXECUTABLES, 5, 3, 5); + t.add(0x7f, 5, null, 5); + t.add_list(EXECUTABLES, 2, 3, 2); + t.add(0x7f, 2, null, 2); + // osc + t.add(0x5d, 1, 4, 8); + t.add_list(PRINTABLES, 8, 5, 8); + t.add(0x7f, 8, 5, 8); + t.add_list([0x9c, 0x1b, 0x18, 0x1a, 0x07], 8, 6, 0); + t.add_list(r(0x1c, 0x20), 8, 0, 8); + // sos/pm/apc does nothing + t.add_list([0x58, 0x5e, 0x5f], 1, 0, 7); + t.add_list(PRINTABLES, 7, null, 7); + t.add_list(EXECUTABLES, 7, null, 7); + t.add(0x9c, 7, 0, 0); + // csi entries + t.add(0x5b, 1, 11, 3); + t.add_list(r(0x40, 0x7f), 3, 7, 0); + t.add_list(r(0x30, 0x3a), 3, 8, 4); + t.add(0x3b, 3, 8, 4); + t.add_list([0x3c, 0x3d, 0x3e, 0x3f], 3, 9, 4); + t.add_list(r(0x30, 0x3a), 4, 8, 4); + t.add(0x3b, 4, 8, 4); + t.add_list(r(0x40, 0x7f), 4, 7, 0); + t.add_list([0x3a, 0x3c, 0x3d, 0x3e, 0x3f], 4, 0, 6); + t.add_list(r(0x20, 0x40), 6, null, 6); + t.add(0x7f, 6, null, 6); + t.add_list(r(0x40, 0x7f), 6, 0, 0); + t.add(0x3a, 3, 0, 6); + t.add_list(r(0x20, 0x30), 3, 9, 5); + t.add_list(r(0x20, 0x30), 5, 9, 5); + t.add_list(r(0x30, 0x40), 5, 0, 6); + t.add_list(r(0x40, 0x7f), 5, 7, 0); + t.add_list(r(0x20, 0x30), 4, 9, 5); + // esc_intermediate + t.add_list(r(0x20, 0x30), 1, 9, 2); + t.add_list(r(0x20, 0x30), 2, 9, 2); + t.add_list(r(0x30, 0x7f), 2, 10, 0); + t.add_list(r(0x30, 0x50), 1, 10, 0); + t.add_list([0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x59, 0x5a, 0x5c], 1, 10, 0); + t.add_list(r(0x60, 0x7f), 1, 10, 0); + // dcs entry + t.add(0x50, 1, 11, 9); + t.add_list(EXECUTABLES, 9, null, 9); + t.add(0x7f, 9, null, 9); + t.add_list(r(0x1c, 0x20), 9, null, 9); + t.add_list(r(0x20, 0x30), 9, 9, 12); + t.add(0x3a, 9, 0, 11); + t.add_list(r(0x30, 0x3a), 9, 8, 10); + t.add(0x3b, 9, 8, 10); + t.add_list([0x3c, 0x3d, 0x3e, 0x3f], 9, 9, 10); + t.add_list(EXECUTABLES, 11, null, 11); + t.add_list(r(0x20, 0x80), 11, null, 11); + t.add_list(r(0x1c, 0x20), 11, null, 11); + t.add_list(EXECUTABLES, 10, null, 10); + t.add(0x7f, 10, null, 10); + t.add_list(r(0x1c, 0x20), 10, null, 10); + t.add_list(r(0x30, 0x3a), 10, 8, 10); + t.add(0x3b, 10, 8, 10); + t.add_list([0x3a, 0x3c, 0x3d, 0x3e, 0x3f], 10, 0, 11); + t.add_list(r(0x20, 0x30), 10, 9, 12); + t.add_list(EXECUTABLES, 12, null, 12); + t.add(0x7f, 12, null, 12); + t.add_list(r(0x1c, 0x20), 12, null, 12); + t.add_list(r(0x20, 0x30), 12, 9, 12); + t.add_list(r(0x30, 0x40), 12, 0, 11); + t.add_list(r(0x40, 0x7f), 12, 12, 13); + t.add_list(r(0x40, 0x7f), 10, 12, 13); + t.add_list(r(0x40, 0x7f), 9, 12, 13); + t.add_list(EXECUTABLES, 13, 13, 13); + t.add_list(PRINTABLES, 13, 13, 13); + t.add(0x7f, 13, null, 13); + t.add_list([0x1b, 0x9c], 13, 14, 0); + + return t; +})(); + +export class AnsiParser { + public initialState: number; + public currentState: number; + public transitions: TransitionTable; + public osc: string; + public params: number[]; + public collected: string; + public term: any; + constructor(terminal: IParserTerminal) { + this.initialState = 0; + this.currentState = this.initialState | 0; + this.transitions = new TransitionTable(4095); + this.transitions.table.set(TRANSITION_TABLE.table); + this.osc = ''; + this.params = [0]; + this.collected = ''; + this.term = terminal || {}; + let instructions = ['inst_p', 'inst_o', 'inst_x', 'inst_c', + 'inst_e', 'inst_H', 'inst_P', 'inst_U', 'inst_E']; + for (let i = 0; i < instructions.length; ++i) { + if (!(instructions[i] in this.term)) { + this.term[instructions[i]] = function(): void {}; + } + } + } + reset(): void { + this.currentState = this.initialState; + this.osc = ''; + this.params = [0]; + this.collected = ''; + } + parse(s: string): void { + let code = 0; + let transition = 0; + let error = false; + let currentState = this.currentState; + + // local buffers + let printed = -1; + let dcs = -1; + let osc = this.osc; + let collected = this.collected; + let params = this.params; + let table: Uint8Array = this.transitions.table; + + // process input string + let l = s.length; + for (let i = 0; i < l; ++i) { + code = s.charCodeAt(i); + // shortcut for most chars (print action) + if (currentState === 0 && (code > 0x1f && code < 0x80)) { + printed = (~printed) ? printed : i; + continue; + } + if (currentState === 4) { + if (code === 0x3b) { + params.push(0); + continue; + } + if (code > 0x2f && code < 0x39) { + params[params.length - 1] = params[params.length - 1] * 10 + code - 48; + continue; + } + } + transition = ((code < 0xa0) ? (table[currentState << 8 | code]) : 16); + switch (transition >> 4) { + case 2: // print + printed = (~printed) ? printed : i; + break; + case 3: // execute + if (printed + 1) { + this.term.inst_p(s, printed, i); + printed = -1; + } + this.term.inst_x(String.fromCharCode(code)); + break; + case 0: // ignore + // handle leftover print and dcs chars + if (printed + 1) { + this.term.inst_p(s, printed, i); + printed = -1; + } else if (dcs + 1) { + this.term.inst_P(s.substring(dcs, i)); + dcs = -1; + } + break; + case 1: // error + // handle unicode chars in write buffers w'o state change + if (code > 0x9f) { + switch (currentState) { + case 0: // GROUND -> add char to print string + printed = (~printed) ? printed : i; + break; + case 8: // OSC_STRING -> add char to osc string + osc += String.fromCharCode(code); + transition |= 8; + break; + case 6: // CSI_IGNORE -> ignore char + transition |= 6; + break; + case 11: // DCS_IGNORE -> ignore char + transition |= 11; + break; + case 13: // DCS_PASSTHROUGH -> add char to dcs + if (!(~dcs)) dcs = i | 0; + transition |= 13; + break; + default: // real error + error = true; + } + } else { // real error + error = true; + } + if (error) { + if (this.term.inst_E( + { + pos: i, // position in parse string + character: String.fromCharCode(code), // wrong character + state: currentState, // in state + print: printed, // print buffer + dcs: dcs, // dcs buffer + osc: osc, // osc buffer + collect: collected, // collect buffer + params: params // params buffer + })) { + return; + } + error = false; + } + break; + case 7: // csi_dispatch + this.term.inst_c(collected, params, String.fromCharCode(code)); + break; + case 8: // param + if (code === 0x3b) params.push(0); + else params[params.length - 1] = params[params.length - 1] * 10 + code - 48; + break; + case 9: // collect + collected += String.fromCharCode(code); + break; + case 10: // esc_dispatch + this.term.inst_e(collected, String.fromCharCode(code)); + break; + case 11: // clear + if (~printed) { + this.term.inst_p(s, printed, i); + printed = -1; + } + osc = ''; + params = [0]; + collected = ''; + dcs = -1; + break; + case 12: // dcs_hook + this.term.inst_H(collected, params, String.fromCharCode(code)); + break; + case 13: // dcs_put + if (!(~dcs)) dcs = i; + break; + case 14: // dcs_unhook + if (~dcs) this.term.inst_P(s.substring(dcs, i)); + this.term.inst_U(); + if (code === 0x1b) transition |= 1; + osc = ''; + params = [0]; + collected = ''; + dcs = -1; + break; + case 4: // osc_start + if (~printed) { + this.term.inst_p(s, printed, i); + printed = -1; + } + osc = ''; + break; + case 5: // osc_put + osc += s.charAt(i); + break; + case 6: // osc_end + if (osc && code !== 0x18 && code !== 0x1a) this.term.inst_o(osc); + if (code === 0x1b) transition |= 1; + osc = ''; + params = [0]; + collected = ''; + dcs = -1; + break; + } + currentState = transition & 15; + } + + // push leftover pushable buffers to terminal + if (!currentState && (printed + 1)) { + this.term.inst_p(s, printed, s.length); + } else if (currentState === 13 && (dcs + 1)) { + this.term.inst_P(s.substring(dcs)); + } + + // save non pushable buffers + this.osc = osc; + this.collected = collected; + this.params = params; + + // save state + this.currentState = currentState; + } +} + + + + + +import { IInputHandler, IInputHandlingTerminal } from './Types'; +import { CHARSETS, DEFAULT_CHARSET } from './Charsets'; +import { C0 } from './EscapeSequences'; + +// glue code between AnsiParser and Terminal +export class ParserTerminal implements IParserTerminal { + private _parser: AnsiParser; + private _terminal: any; + private _inputHandler: IInputHandler; + + constructor(_terminal: any, _inputHandler: IInputHandler) { + this._parser = new AnsiParser(this); + this._terminal = _terminal; + this._inputHandler = _inputHandler; + } + + write(data: string): void { + const cursorStartX = this._terminal.buffer.x; + const cursorStartY = this._terminal.buffer.y; + if (this._terminal.debug) { + this._terminal.log('data: ' + data); + } + // apply leftover surrogate high from last write + if (this._terminal.surrogate_high) { + data = this._terminal.surrogate_high + data; + this._terminal.surrogate_high = ''; + } + + this._parser.parse(data); + + if (this._terminal.buffer.x !== cursorStartX || this._terminal.buffer.y !== cursorStartY) { + this._terminal.emit('cursormove'); + } + } + + inst_p(data: string, start: number, end: number): void { + // const l = data.length; + let ch; + let code; + let low; + for (let i = start; i < end; ++i) { + ch = data.charAt(i); + code = data.charCodeAt(i); + if (0xD800 <= code && code <= 0xDBFF) { + // we got a surrogate high + // get surrogate low (next 2 bytes) + low = data.charCodeAt(i + 1); + if (isNaN(low)) { + // end of data stream, save surrogate high + this._terminal.surrogate_high = ch; + continue; + } + code = ((code - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000; + ch += data.charAt(i + 1); + } + // surrogate low - already handled above + if (0xDC00 <= code && code <= 0xDFFF) { + continue; + } + this._inputHandler.addChar(ch, code); + } + } + + inst_o(data: string): void { + let params = data.split(';'); + switch (parseInt(params[0])) { + case 0: + case 1: + case 2: + if (params[1]) { + this._terminal.title = params[1]; + this._terminal.handleTitle(this._terminal.title); + } + break; + case 3: + // set X property + break; + case 4: + case 5: + // change dynamic colors + break; + case 10: + case 11: + case 12: + case 13: + case 14: + case 15: + case 16: + case 17: + case 18: + case 19: + // change dynamic ui colors + break; + case 46: + // change log file + break; + case 50: + // dynamic font + break; + case 51: + // emacs shell + break; + case 52: + // manipulate selection data + break; + case 104: + case 105: + case 110: + case 111: + case 112: + case 113: + case 114: + case 115: + case 116: + case 117: + case 118: + // reset colors + break; + } + } + + inst_x(flag: string): void { + switch (flag) { + case C0.BEL: return this._inputHandler.bell(); + case C0.LF: return this._inputHandler.lineFeed(); + case C0.VT: return this._inputHandler.lineFeed(); + case C0.FF: return this._inputHandler.lineFeed(); + case C0.CR: return this._inputHandler.carriageReturn(); + case C0.BS: return this._inputHandler.backspace(); + case C0.HT: return this._inputHandler.tab(); + case C0.SO: return this._inputHandler.shiftOut(); + case C0.SI: return this._inputHandler.shiftIn(); + default: + this._inputHandler.addChar(flag, flag.charCodeAt(0)); + } + this._terminal.error('Unknown EXEC flag: %s.', flag); + } + + inst_c(collected: string, params: number[], flag: string): void { + this._terminal.prefix = collected; + switch (flag) { + case '@': return this._inputHandler.insertChars(params); + case 'A': return this._inputHandler.cursorUp(params); + case 'B': return this._inputHandler.cursorDown(params); + case 'C': return this._inputHandler.cursorForward(params); + case 'D': return this._inputHandler.cursorBackward(params); + case 'E': return this._inputHandler.cursorNextLine(params); + case 'F': return this._inputHandler.cursorPrecedingLine(params); + case 'G': return this._inputHandler.cursorCharAbsolute(params); + case 'H': return this._inputHandler.cursorPosition(params); + case 'I': return this._inputHandler.cursorForwardTab(params); + case 'J': return this._inputHandler.eraseInDisplay(params); + case 'K': return this._inputHandler.eraseInLine(params); + case 'L': return this._inputHandler.insertLines(params); + case 'M': return this._inputHandler.deleteLines(params); + case 'P': return this._inputHandler.deleteChars(params); + case 'S': return this._inputHandler.scrollUp(params); + case 'T': + if (params.length < 2 && !collected) { + return this._inputHandler.scrollDown(params); + } + break; + case 'X': return this._inputHandler.eraseChars(params); + case 'Z': return this._inputHandler.cursorBackwardTab(params); + case '`': return this._inputHandler.charPosAbsolute(params); + case 'a': return this._inputHandler.HPositionRelative(params); + case 'b': return this._inputHandler.repeatPrecedingCharacter(params); + case 'c': return this._inputHandler.sendDeviceAttributes(params); + case 'd': return this._inputHandler.linePosAbsolute(params); + case 'e': return this._inputHandler.VPositionRelative(params); + case 'f': return this._inputHandler.HVPosition(params); + case 'g': return this._inputHandler.tabClear(params); + case 'h': return this._inputHandler.setMode(params); + case 'l': return this._inputHandler.resetMode(params); + case 'm': return this._inputHandler.charAttributes(params); + case 'n': return this._inputHandler.deviceStatus(params); + case 'p': + if (collected === '!') { + return this._inputHandler.softReset(params); + } + break; + case 'q': + if (collected === ' ') { + return this._inputHandler.setCursorStyle(params); + } + break; + case 'r': return this._inputHandler.setScrollRegion(params); + case 's': return this._inputHandler.saveCursor(params); + case 'u': return this._inputHandler.restoreCursor(params); + } + this._terminal.error('Unknown CSI code: %s %s %s.', collected, params, flag); + } + + inst_e(collected: string, flag: string): void { + let cs; + + switch (collected) { + case '': + switch (flag) { + // case '6': // Back Index (DECBI), VT420 and up - not supported + case '7': // Save Cursor (DECSC) + this._inputHandler.saveCursor(); + return; + case '8': // Restore Cursor (DECRC) + this._inputHandler.restoreCursor(); + return; + // case '9': // Forward Index (DECFI), VT420 and up - not supported + case 'D': // Index (IND is 0x84) + this._terminal.index(); + return; + case 'E': // Next Line (NEL is 0x85) + this._terminal.buffer.x = 0; + this._terminal.index(); + return; + case 'H': // ESC H Tab Set (HTS is 0x88) + (this._terminal).tabSet(); + return; + case 'M': // Reverse Index (RI is 0x8d) + this._terminal.reverseIndex(); + return; + case 'N': // Single Shift Select of G2 Character Set ( SS2 is 0x8e) - Is this supported? + case 'O': // Single Shift Select of G3 Character Set ( SS3 is 0x8f) + return; + // case 'P': // Device Control String (DCS is 0x90) - covered by parser + // case 'V': // Start of Guarded Area (SPA is 0x96) - not supported + // case 'W': // End of Guarded Area (EPA is 0x97) - not supported + // case 'X': // Start of String (SOS is 0x98) - covered by parser (unsupported) + // case 'Z': // Return Terminal ID (DECID is 0x9a). Obsolete form of CSI c (DA). - not supported + // case '[': // Control Sequence Introducer (CSI is 0x9b) - covered by parser + // case '\': // String Terminator (ST is 0x9c) - covered by parser + // case ']': // Operating System Command (OSC is 0x9d) - covered by parser + // case '^': // Privacy Message (PM is 0x9e) - covered by parser (unsupported) + // case '_': // Application Program Command (APC is 0x9f) - covered by parser (unsupported) + case '=': // Application Keypad (DECKPAM) + this._terminal.log('Serial port requested application keypad.'); + this._terminal.applicationKeypad = true; + if (this._terminal.viewport) { + this._terminal.viewport.syncScrollArea(); + } + return; + case '>': // Normal Keypad (DECKPNM) + this._terminal.log('Switching back to normal keypad.'); + this._terminal.applicationKeypad = false; + if (this._terminal.viewport) { + this._terminal.viewport.syncScrollArea(); + } + return; + // case 'F': // Cursor to lower left corner of screen + case 'c': // Full Reset (RIS) http://vt100.net/docs/vt220-rm/chapter4.html + this._terminal.reset(); + return; + // case 'l': // Memory Lock (per HP terminals). Locks memory above the cursor. + // case 'm': // Memory Unlock (per HP terminals). + case 'n': // Invoke the G2 Character Set as GL (LS2). + this._terminal.setgLevel(2); + return; + case 'o': // Invoke the G3 Character Set as GL (LS3). + this._terminal.setgLevel(3); + return; + case '|': // Invoke the G3 Character Set as GR (LS3R). + this._terminal.setgLevel(3); + return; + case '}': // Invoke the G2 Character Set as GR (LS2R). + this._terminal.setgLevel(2); + return; + case '~': // Invoke the G1 Character Set as GR (LS1R). + this._terminal.setgLevel(1); + return; + } + // case ' ': + // switch (flag) { + // case 'F': // (SP) 7-bit controls (S7C1T) + // case 'G': // (SP) 8-bit controls (S8C1T) + // case 'L': // (SP) Set ANSI conformance level 1 (dpANS X3.134.1) + // case 'M': // (SP) Set ANSI conformance level 2 (dpANS X3.134.1) + // case 'N': // (SP) Set ANSI conformance level 3 (dpANS X3.134.1) + // } + + // case '#': + // switch (flag) { + // case '3': // DEC double-height line, top half (DECDHL) + // case '4': // DEC double-height line, bottom half (DECDHL) + // case '5': // DEC single-width line (DECSWL) + // case '6': // DEC double-width line (DECDWL) + // case '8': // DEC Screen Alignment Test (DECALN) + // } + + case '%': + // switch (flag) { + // case '@': // (%) Select default character set. That is ISO 8859-1 (ISO 2022) + // case 'G': // (%) Select UTF-8 character set (ISO 2022) + // } + this._terminal.setgLevel(0); + this._terminal.setgCharset(0, DEFAULT_CHARSET); // US (default) + return; + + // load character sets + case '(': // G0 (VT100) + cs = CHARSETS[flag]; + if (!cs) cs = DEFAULT_CHARSET; + this._terminal.setgCharset(0, cs); + return; + case ')': // G1 (VT100) + cs = CHARSETS[flag]; + if (!cs) cs = DEFAULT_CHARSET; + this._terminal.setgCharset(1, cs); + return; + case '*': // G2 (VT220) + cs = CHARSETS[flag]; + if (!cs) cs = DEFAULT_CHARSET; + this._terminal.setgCharset(2, cs); + return; + case '+': // G3 (VT220) + cs = CHARSETS[flag]; + if (!cs) cs = DEFAULT_CHARSET; + this._terminal.setgCharset(3, cs); + return; + case '-': // G1 (VT300) + cs = CHARSETS[flag]; + if (!cs) cs = DEFAULT_CHARSET; + this._terminal.setgCharset(1, cs); + return; + case '.': // G2 (VT300) + if (!cs) cs = DEFAULT_CHARSET; + this._terminal.setgCharset(2, cs); + return; + case '/': // G3 (VT300) + // not supported - how to deal with this? (original code is not reachable) + return; + default: + this._terminal.error('Unknown ESC control: %s %s.', collected, flag); + } + } + + inst_H(collected: string, params: number[], flag: string): void { + // TODO + } + + inst_P(dcs: string): void { + // TODO + } + + inst_U(): void { + // TODO + } + + inst_E(): void { + // TODO + } +} diff --git a/src/InputHandler.ts b/src/InputHandler.ts index acf7af1f..3d29f7b4 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -22,11 +22,14 @@ export class InputHandler implements IInputHandler { constructor(private _terminal: IInputHandlingTerminal) { } public addChar(char: string, code: number): void { - if (char >= ' ') { + // if (char >= ' ') { // calculate print space // expensive call, therefore we save width in line buffer const chWidth = wcwidth(code); + // localize buffer + let buffer = this._terminal.buffer; + if (this._terminal.charset && this._terminal.charset[char]) { char = this._terminal.charset[char]; } @@ -35,42 +38,42 @@ export class InputHandler implements IInputHandler { this._terminal.emit('a11y.char', char); } - let row = this._terminal.buffer.y + this._terminal.buffer.ybase; + let row = buffer.y + buffer.ybase; // insert combining char in last cell // FIXME: needs handling after cursor jumps - if (!chWidth && this._terminal.buffer.x) { + if (!chWidth && buffer.x) { // dont overflow left - if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1]) { - if (!this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) { + if (buffer.lines.get(row)[buffer.x - 1]) { + if (!buffer.lines.get(row)[buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) { // found empty cell after fullwidth, need to go 2 cells back - if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2]) { - this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char; - this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2][3] = char.charCodeAt(0); + if (buffer.lines.get(row)[buffer.x - 2]) { + buffer.lines.get(row)[buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char; + buffer.lines.get(row)[buffer.x - 2][3] = char.charCodeAt(0); } } else { - this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char; - this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][3] = char.charCodeAt(0); + buffer.lines.get(row)[buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char; + buffer.lines.get(row)[buffer.x - 1][3] = char.charCodeAt(0); } - this._terminal.updateRange(this._terminal.buffer.y); + this._terminal.updateRange(buffer.y); } return; } // goto next line if ch would overflow // TODO: needs a global min terminal width of 2 - if (this._terminal.buffer.x + chWidth - 1 >= this._terminal.cols) { + if (buffer.x + chWidth - 1 >= this._terminal.cols) { // autowrap - DECAWM if (this._terminal.wraparoundMode) { - this._terminal.buffer.x = 0; - this._terminal.buffer.y++; - if (this._terminal.buffer.y > this._terminal.buffer.scrollBottom) { - this._terminal.buffer.y--; + buffer.x = 0; + buffer.y++; + if (buffer.y > buffer.scrollBottom) { + buffer.y--; this._terminal.scroll(true); } else { // The line already exists (eg. the initial viewport), mark it as a // wrapped line - (this._terminal.buffer.lines.get(this._terminal.buffer.y)).isWrapped = true; + (buffer.lines.get(buffer.y)).isWrapped = true; } } else { if (chWidth === 2) { // FIXME: check for xterm behavior @@ -78,7 +81,7 @@ export class InputHandler implements IInputHandler { } } } - row = this._terminal.buffer.y + this._terminal.buffer.ybase; + row = buffer.y + buffer.ybase; // insert mode: move characters to right if (this._terminal.insertMode) { @@ -86,28 +89,28 @@ export class InputHandler implements IInputHandler { for (let moves = 0; moves < chWidth; ++moves) { // remove last cell, if it's width is 0 // we have to adjust the second last cell as well - const removed = this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).pop(); + const removed = buffer.lines.get(buffer.y + buffer.ybase).pop(); if (removed[CHAR_DATA_WIDTH_INDEX] === 0 - && this._terminal.buffer.lines.get(row)[this._terminal.cols - 2] - && this._terminal.buffer.lines.get(row)[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) { - this._terminal.buffer.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]; + && buffer.lines.get(row)[this._terminal.cols - 2] + && buffer.lines.get(row)[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) { + buffer.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]; } // insert empty cell at cursor - this._terminal.buffer.lines.get(row).splice(this._terminal.buffer.x, 0, [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]); + buffer.lines.get(row).splice(buffer.x, 0, [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]); } } - this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, char, chWidth, char.charCodeAt(0)]; - this._terminal.buffer.x++; - this._terminal.updateRange(this._terminal.buffer.y); + buffer.lines.get(row)[buffer.x] = [this._terminal.curAttr, char, chWidth, char.charCodeAt(0)]; + buffer.x++; + this._terminal.updateRange(buffer.y); // fullwidth char - set next cell width to zero and advance cursor if (chWidth === 2) { - this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, '', 0, undefined]; - this._terminal.buffer.x++; + buffer.lines.get(row)[buffer.x] = [this._terminal.curAttr, '', 0, undefined]; + buffer.x++; } - } + // } } /** diff --git a/src/Terminal.ts b/src/Terminal.ts index ba1fff3d..44c90643 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -32,7 +32,7 @@ import { Viewport } from './Viewport'; import { rightClickHandler, moveTextAreaUnderMouseCursor, pasteHandler, copyHandler } from './handlers/Clipboard'; import { C0 } from './EscapeSequences'; import { InputHandler } from './InputHandler'; -import { Parser } from './Parser'; +// import { Parser } from './Parser'; import { Renderer } from './renderer/Renderer'; import { Linkifier } from './Linkifier'; import { SelectionManager } from './SelectionManager'; @@ -49,6 +49,7 @@ import { AccessibilityManager } from './AccessibilityManager'; import { ScreenDprMonitor } from './utils/ScreenDprMonitor'; import { ITheme, ILocalizableStrings, IMarker, IDisposable } from 'xterm'; import { removeTerminalFromCache } from './renderer/atlas/CharAtlas'; +import { ParserTerminal } from './EscapeSequenceParser'; // reg + shift key mappings for digits and special chars const KEYCODE_KEY_MAPPINGS = { @@ -216,7 +217,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II private _inputHandler: InputHandler; public soundManager: SoundManager; - private _parser: Parser; + // private _parser: Parser; + private _newParser: ParserTerminal; public renderer: IRenderer; public selectionManager: SelectionManager; public linkifier: ILinkifier; @@ -331,7 +333,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this._userScrolling = false; this._inputHandler = new InputHandler(this); - this._parser = new Parser(this._inputHandler, this); + // this._parser = new Parser(this._inputHandler, this); + this._newParser = new ParserTerminal(this, this._inputHandler); // Reuse renderer if the Terminal is being recreated via a reset call. this.renderer = this.renderer || null; this.selectionManager = this.selectionManager || null; @@ -1315,8 +1318,10 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // middle of parsing escape sequence in two chunks. For some reason the // state of the parser resets to 0 after exiting parser.parse. This change // just sets the state back based on the correct return statement. - const state = this._parser.parse(data); - this._parser.setState(state); + + // const state = this._parser.parse(data); + this._newParser.write(data); + // this._parser.setState(state); this.updateRange(this.buffer.y); this.refresh(this._refreshStart, this._refreshEnd); From 20760d09e43659e7231bd674b5bcb9104e0d442b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 22 Apr 2018 04:34:42 +0200 Subject: [PATCH 3/3] utilize more typescript features --- src/EscapeSequenceParser.test.ts | 1937 +++++++++++++++--------------- src/EscapeSequenceParser.ts | 575 +++++---- src/Terminal.ts | 4 +- 3 files changed, 1302 insertions(+), 1214 deletions(-) diff --git a/src/EscapeSequenceParser.test.ts b/src/EscapeSequenceParser.test.ts index eefa7603..296ac34d 100644 --- a/src/EscapeSequenceParser.test.ts +++ b/src/EscapeSequenceParser.test.ts @@ -1,4 +1,4 @@ -import { AnsiParser, IParserTerminal } from './EscapeSequenceParser'; +import { EscapeSequenceParser, IParserTerminal, STATE } from './EscapeSequenceParser'; import * as chai from 'chai'; function r(a: number, b: number): string[] { @@ -24,1019 +24,1040 @@ let testTerminal: ITestTerminal = { compare: function (value: any): void { chai.expect(this.calls.slice()).eql(value); // weird bug w'o slicing here }, - inst_p: function (s: string, start: number, end: number): void { - this.calls.push(['print', s.substring(start, end)]); + actionPrint: function (data: string, start: number, end: number): void { + this.calls.push(['print', data.substring(start, end)]); }, - inst_o: function (s: string): void { + actionOSC: function (s: string): void { this.calls.push(['osc', s]); }, - inst_x: function (flag: string): void { + actionExecute: function (flag: string): void { this.calls.push(['exe', flag]); }, - inst_c: function (collected: string, params: number[], flag: string): void { + actionCSI: function (collected: string, params: number[], flag: string): void { this.calls.push(['csi', collected, params, flag]); }, - inst_e: function (collected: string, flag: string): void { + actionESC: function (collected: string, flag: string): void { this.calls.push(['esc', collected, flag]); }, - inst_H: function (collected: string, params: number[], flag: string): void { + actionDCSHook: function (collected: string, params: number[], flag: string): void { this.calls.push(['dcs hook', collected, params, flag]); }, - inst_P: function (dcs: string): void { - this.calls.push(['dcs put', dcs]); + actionDCSPrint: function (data: string, start: number, end: number): void { + this.calls.push(['dcs put', data.substring(start, end)]); }, - inst_U: function (): void { + actionDCSUnhook: function (): void { this.calls.push(['dcs unhook']); } }; -let parser = new AnsiParser(testTerminal); +let states: number[] = [ + STATE.GROUND, + STATE.ESCAPE, + STATE.ESCAPE_INTERMEDIATE, + STATE.CSI_ENTRY, + STATE.CSI_PARAM, + STATE.CSI_INTERMEDIATE, + STATE.CSI_IGNORE, + STATE.SOS_PM_APC_STRING, + STATE.OSC_STRING, + STATE.DCS_ENTRY, + STATE.DCS_PARAM, + STATE.DCS_IGNORE, + STATE.DCS_INTERMEDIATE, + STATE.DCS_PASSTHROUGH +]; +let state: any; -describe('Parser init and methods', function(): void { - it('parser init', function (): void { - let p = new AnsiParser({}); - chai.expect(p.term).a('object'); - chai.expect(p.term.inst_p).a('function'); - chai.expect(p.term.inst_o).a('function'); - chai.expect(p.term.inst_x).a('function'); - chai.expect(p.term.inst_c).a('function'); - chai.expect(p.term.inst_e).a('function'); - chai.expect(p.term.inst_H).a('function'); - chai.expect(p.term.inst_P).a('function'); - chai.expect(p.term.inst_U).a('function'); - p.parse('\x1b[31mHello World!'); - }); - it('terminal callbacks', function (): void { - chai.expect(parser.term).equal(testTerminal); - chai.expect(parser.term.inst_p).equal(testTerminal.inst_p); - chai.expect(parser.term.inst_o).equal(testTerminal.inst_o); - chai.expect(parser.term.inst_x).equal(testTerminal.inst_x); - chai.expect(parser.term.inst_c).equal(testTerminal.inst_c); - chai.expect(parser.term.inst_e).equal(testTerminal.inst_e); - chai.expect(parser.term.inst_H).equal(testTerminal.inst_H); - chai.expect(parser.term.inst_P).equal(testTerminal.inst_P); - chai.expect(parser.term.inst_U).equal(testTerminal.inst_U); - }); - it('inital states', function (): void { - chai.expect(parser.initialState).equal(0); - chai.expect(parser.currentState).equal(0); - chai.expect(parser.osc).equal(''); - chai.expect(parser.params).eql([0]); - chai.expect(parser.collected).equal(''); - }); - it('reset states', function (): void { - parser.currentState = 124; - parser.osc = '#'; - parser.params = [123]; - parser.collected = '#'; +let parser = new EscapeSequenceParser(testTerminal); - parser.reset(); - chai.expect(parser.currentState).equal(0); - chai.expect(parser.osc).equal(''); - chai.expect(parser.params).eql([0]); - chai.expect(parser.collected).equal(''); - }); -}); +describe('EscapeSequenceParser', function(): void { -describe('state transitions and actions', function(): void { - it('state GROUND execute action', function (): void { - parser.reset(); - testTerminal.clear(); - let exes = r(0x00, 0x18); - exes.concat(['\x19']); - exes.concat(r(0x1c, 0x20)); - for (let i = 0; i < exes.length; ++i) { - parser.currentState = 0; - parser.parse(exes[i]); + describe('Parser init and methods', function(): void { + it('parser init', function (): void { + let p = new EscapeSequenceParser({}); + chai.expect(p.term).a('object'); + chai.expect(p.term.actionPrint).a('function'); + chai.expect(p.term.actionOSC).a('function'); + chai.expect(p.term.actionExecute).a('function'); + chai.expect(p.term.actionCSI).a('function'); + chai.expect(p.term.actionESC).a('function'); + chai.expect(p.term.actionDCSHook).a('function'); + chai.expect(p.term.actionDCSPrint).a('function'); + chai.expect(p.term.actionDCSUnhook).a('function'); + p.parse('\x1b[31mHello World!'); + }); + it('terminal callbacks', function (): void { + chai.expect(parser.term).equal(testTerminal); + chai.expect(parser.term.actionPrint).equal(testTerminal.actionPrint); + chai.expect(parser.term.actionOSC).equal(testTerminal.actionOSC); + chai.expect(parser.term.actionExecute).equal(testTerminal.actionExecute); + chai.expect(parser.term.actionCSI).equal(testTerminal.actionCSI); + chai.expect(parser.term.actionESC).equal(testTerminal.actionESC); + chai.expect(parser.term.actionDCSHook).equal(testTerminal.actionDCSHook); + chai.expect(parser.term.actionDCSPrint).equal(testTerminal.actionDCSPrint); + chai.expect(parser.term.actionDCSUnhook).equal(testTerminal.actionDCSUnhook); + }); + it('inital states', function (): void { + chai.expect(parser.initialState).equal(0); chai.expect(parser.currentState).equal(0); - testTerminal.compare([['exe', exes[i]]]); - parser.reset(); - testTerminal.clear(); - } - }); - it('state GROUND print action', function (): void { - parser.reset(); - testTerminal.clear(); - let printables = r(0x20, 0x7f); // NOTE: DEL excluded - for (let i = 0; i < printables.length; ++i) { - parser.currentState = 0; - parser.parse(printables[i]); - chai.expect(parser.currentState).equal(0); - testTerminal.compare([['print', printables[i]]]); - parser.reset(); - testTerminal.clear(); - } - }); - it('trans ANYWHERE --> GROUND with actions', function (): void { - let exes = [ - '\x18', '\x1a', - '\x80', '\x81', '\x82', '\x83', '\x84', '\x85', '\x86', '\x87', '\x88', - '\x89', '\x8a', '\x8b', '\x8c', '\x8d', '\x8e', '\x8f', - '\x91', '\x92', '\x93', '\x94', '\x95', '\x96', '\x97', '\x99', '\x9a' - ]; - let exceptions = { - 8: {'\x18': [], '\x1a': []} // simply abort osc state - }; - parser.reset(); - testTerminal.clear(); - for (let state = 0; state < 14; ++state) { - for (let i = 0; i < exes.length; ++i) { - parser.currentState = state; - parser.parse(exes[i]); - chai.expect(parser.currentState).equal(0); - testTerminal.compare(((exceptions[state]) ? exceptions[state][exes[i]] : 0) || [['exe', exes[i]]]); - parser.reset(); - testTerminal.clear(); - } - parser.parse('\x9c'); - chai.expect(parser.currentState).equal(0); - testTerminal.compare([]); - parser.reset(); - testTerminal.clear(); - } - }); - it('trans ANYWHERE --> ESCAPE with clear', function (): void { - parser.reset(); - for (let state = 0; state < 14; ++state) { - parser.currentState = state; - parser.osc = '#'; - parser.params = [23]; - parser.collected = '#'; - parser.parse('\x1b'); - chai.expect(parser.currentState).equal(1); chai.expect(parser.osc).equal(''); chai.expect(parser.params).eql([0]); chai.expect(parser.collected).equal(''); - parser.reset(); - } - }); - it('state ESCAPE execute rules', function (): void { - parser.reset(); - testTerminal.clear(); - let exes = r(0x00, 0x18); - exes.concat(['\x19']); - exes.concat(r(0x1c, 0x20)); - for (let i = 0; i < exes.length; ++i) { - parser.currentState = 1; - parser.parse(exes[i]); - chai.expect(parser.currentState).equal(1); - testTerminal.compare([['exe', exes[i]]]); - parser.reset(); - testTerminal.clear(); - } - }); - it('state ESCAPE ignore', function (): void { - parser.reset(); - testTerminal.clear(); - parser.currentState = 1; - parser.parse('\x7f'); - chai.expect(parser.currentState).equal(1); - testTerminal.compare([]); - parser.reset(); - testTerminal.clear(); - }); - it('trans ESCAPE --> GROUND with ecs_dispatch action', function (): void { - parser.reset(); - testTerminal.clear(); - let dispatches = r(0x30, 0x50); - dispatches.concat(r(0x51, 0x58)); - dispatches.concat(['\x59', '\x5a', '\x5c']); - dispatches.concat(r(0x60, 0x7f)); - for (let i = 0; i < dispatches.length; ++i) { - parser.currentState = 1; - parser.parse(dispatches[i]); - chai.expect(parser.currentState).equal(0); - testTerminal.compare([['esc', '', dispatches[i]]]); - parser.reset(); - testTerminal.clear(); - } - }); - it('trans ESCAPE --> ESCAPE_INTERMEDIATE with collect action', function (): void { - parser.reset(); - let collect = r(0x20, 0x30); - for (let i = 0; i < collect.length; ++i) { - parser.currentState = 1; - parser.parse(collect[i]); - chai.expect(parser.currentState).equal(2); - chai.expect(parser.collected).equal(collect[i]); - parser.reset(); - } - }); - it('state ESCAPE_INTERMEDIATE execute rules', function (): void { - parser.reset(); - testTerminal.clear(); - let exes = r(0x00, 0x18); - exes.concat(['\x19']); - exes.concat(r(0x1c, 0x20)); - for (let i = 0; i < exes.length; ++i) { - parser.currentState = 2; - parser.parse(exes[i]); - chai.expect(parser.currentState).equal(2); - testTerminal.compare([['exe', exes[i]]]); - parser.reset(); - testTerminal.clear(); - } - }); - it('state ESCAPE_INTERMEDIATE ignore', function (): void { - parser.reset(); - testTerminal.clear(); - parser.currentState = 2; - parser.parse('\x7f'); - chai.expect(parser.currentState).equal(2); - testTerminal.compare([]); - parser.reset(); - testTerminal.clear(); - }); - it('state ESCAPE_INTERMEDIATE collect action', function (): void { - parser.reset(); - let collect = r(0x20, 0x30); - for (let i = 0; i < collect.length; ++i) { - parser.currentState = 2; - parser.parse(collect[i]); - chai.expect(parser.currentState).equal(2); - chai.expect(parser.collected).equal(collect[i]); - parser.reset(); - } - }); - it('trans ESCAPE_INTERMEDIATE --> GROUND with esc_dispatch action', function (): void { - parser.reset(); - testTerminal.clear(); - let collect = r(0x30, 0x7f); - for (let i = 0; i < collect.length; ++i) { - parser.currentState = 2; - parser.parse(collect[i]); - chai.expect(parser.currentState).equal(0); - testTerminal.compare([['esc', '', collect[i]]]); - parser.reset(); - testTerminal.clear(); - } - }); - it('trans ANYWHERE/ESCAPE --> CSI_ENTRY with clear', function (): void { - parser.reset(); - // C0 - parser.currentState = 1; - parser.osc = '#'; - parser.params = [123]; - parser.collected = '#'; - parser.parse('['); - chai.expect(parser.currentState).equal(3); - chai.expect(parser.osc).equal(''); - chai.expect(parser.params).eql([0]); - chai.expect(parser.collected).equal(''); - parser.reset(); - // C1 - for (let state = 0; state < 14; ++state) { - parser.currentState = state; + }); + it('reset states', function (): void { + parser.currentState = 124; parser.osc = '#'; parser.params = [123]; parser.collected = '#'; - parser.parse('\x9b'); - chai.expect(parser.currentState).equal(3); + + parser.reset(); + chai.expect(parser.currentState).equal(STATE.GROUND); + chai.expect(parser.osc).equal(''); + chai.expect(parser.params).eql([0]); + chai.expect(parser.collected).equal(''); + }); + }); + + describe('state transitions and actions', function(): void { + it('state GROUND execute action', function (): void { + parser.reset(); + testTerminal.clear(); + let exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = STATE.GROUND; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(STATE.GROUND); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state GROUND print action', function (): void { + parser.reset(); + testTerminal.clear(); + let printables = r(0x20, 0x7f); // NOTE: DEL excluded + for (let i = 0; i < printables.length; ++i) { + parser.currentState = STATE.GROUND; + parser.parse(printables[i]); + chai.expect(parser.currentState).equal(STATE.GROUND); + testTerminal.compare([['print', printables[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans ANYWHERE --> GROUND with actions', function (): void { + let exes = [ + '\x18', '\x1a', + '\x80', '\x81', '\x82', '\x83', '\x84', '\x85', '\x86', '\x87', '\x88', + '\x89', '\x8a', '\x8b', '\x8c', '\x8d', '\x8e', '\x8f', + '\x91', '\x92', '\x93', '\x94', '\x95', '\x96', '\x97', '\x99', '\x9a' + ]; + let exceptions = { + 8: {'\x18': [], '\x1a': []} // simply abort osc state + }; + parser.reset(); + testTerminal.clear(); + for (state in states) { + for (let i = 0; i < exes.length; ++i) { + parser.currentState = state; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(STATE.GROUND); + testTerminal.compare(((exceptions[state]) ? exceptions[state][exes[i]] : 0) || [['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + parser.parse('\x9c'); + chai.expect(parser.currentState).equal(STATE.GROUND); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans ANYWHERE --> ESCAPE with clear', function (): void { + parser.reset(); + for (state in states) { + parser.currentState = state; + parser.osc = '#'; + parser.params = [23]; + parser.collected = '#'; + parser.parse('\x1b'); + chai.expect(parser.currentState).equal(STATE.ESCAPE); + chai.expect(parser.osc).equal(''); + chai.expect(parser.params).eql([0]); + chai.expect(parser.collected).equal(''); + parser.reset(); + } + }); + it('state ESCAPE execute rules', function (): void { + parser.reset(); + testTerminal.clear(); + let exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = STATE.ESCAPE; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(STATE.ESCAPE); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state ESCAPE ignore', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = STATE.ESCAPE; + parser.parse('\x7f'); + chai.expect(parser.currentState).equal(STATE.ESCAPE); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('trans ESCAPE --> GROUND with ecs_dispatch action', function (): void { + parser.reset(); + testTerminal.clear(); + let dispatches = r(0x30, 0x50); + dispatches.concat(r(0x51, 0x58)); + dispatches.concat(['\x59', '\x5a', '\x5c']); + dispatches.concat(r(0x60, 0x7f)); + for (let i = 0; i < dispatches.length; ++i) { + parser.currentState = STATE.ESCAPE; + parser.parse(dispatches[i]); + chai.expect(parser.currentState).equal(STATE.GROUND); + testTerminal.compare([['esc', '', dispatches[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans ESCAPE --> ESCAPE_INTERMEDIATE with collect action', function (): void { + parser.reset(); + let collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = STATE.ESCAPE; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(STATE.ESCAPE_INTERMEDIATE); + chai.expect(parser.collected).equal(collect[i]); + parser.reset(); + } + }); + it('state ESCAPE_INTERMEDIATE execute rules', function (): void { + parser.reset(); + testTerminal.clear(); + let exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = STATE.ESCAPE_INTERMEDIATE; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(STATE.ESCAPE_INTERMEDIATE); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state ESCAPE_INTERMEDIATE ignore', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = STATE.ESCAPE_INTERMEDIATE; + parser.parse('\x7f'); + chai.expect(parser.currentState).equal(STATE.ESCAPE_INTERMEDIATE); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('state ESCAPE_INTERMEDIATE collect action', function (): void { + parser.reset(); + let collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = STATE.ESCAPE_INTERMEDIATE; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(STATE.ESCAPE_INTERMEDIATE); + chai.expect(parser.collected).equal(collect[i]); + parser.reset(); + } + }); + it('trans ESCAPE_INTERMEDIATE --> GROUND with esc_dispatch action', function (): void { + parser.reset(); + testTerminal.clear(); + let collect = r(0x30, 0x7f); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = STATE.ESCAPE_INTERMEDIATE; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(STATE.GROUND); + testTerminal.compare([['esc', '', collect[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans ANYWHERE/ESCAPE --> CSI_ENTRY with clear', function (): void { + parser.reset(); + // C0 + parser.currentState = STATE.ESCAPE; + parser.osc = '#'; + parser.params = [123]; + parser.collected = '#'; + parser.parse('['); + chai.expect(parser.currentState).equal(STATE.CSI_ENTRY); chai.expect(parser.osc).equal(''); chai.expect(parser.params).eql([0]); chai.expect(parser.collected).equal(''); parser.reset(); - } - }); - it('state CSI_ENTRY execute rules', function (): void { - parser.reset(); - testTerminal.clear(); - let exes = r(0x00, 0x18); - exes.concat(['\x19']); - exes.concat(r(0x1c, 0x20)); - for (let i = 0; i < exes.length; ++i) { - parser.currentState = 3; - parser.parse(exes[i]); - chai.expect(parser.currentState).equal(3); - testTerminal.compare([['exe', exes[i]]]); - parser.reset(); - testTerminal.clear(); - } - }); - it('state CSI_ENTRY ignore', function (): void { - parser.reset(); - testTerminal.clear(); - parser.currentState = 3; - parser.parse('\x7f'); - chai.expect(parser.currentState).equal(3); - testTerminal.compare([]); - parser.reset(); - testTerminal.clear(); - }); - it('trans CSI_ENTRY --> GROUND with csi_dispatch action', function (): void { - parser.reset(); - let dispatches = r(0x40, 0x7f); - for (let i = 0; i < dispatches.length; ++i) { - parser.currentState = 3; - parser.parse(dispatches[i]); - chai.expect(parser.currentState).equal(0); - testTerminal.compare([['csi', '', [0], dispatches[i]]]); - parser.reset(); - testTerminal.clear(); - } - }); - it('trans CSI_ENTRY --> CSI_PARAM with param/collect actions', function (): void { - parser.reset(); - let params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; - let collect = ['\x3c', '\x3d', '\x3e', '\x3f']; - for (let i = 0; i < params.length; ++i) { - parser.currentState = 3; - parser.parse(params[i]); - chai.expect(parser.currentState).equal(4); - chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); - parser.reset(); - } - // ';' - parser.currentState = 3; - parser.parse('\x3b'); - chai.expect(parser.currentState).equal(4); - chai.expect(parser.params).eql([0, 0]); - parser.reset(); - for (let i = 0; i < collect.length; ++i) { - parser.currentState = 3; - parser.parse(collect[i]); - chai.expect(parser.currentState).equal(4); - chai.expect(parser.collected).equal(collect[i]); - parser.reset(); - } - }); - it('state CSI_PARAM execute rules', function (): void { - parser.reset(); - testTerminal.clear(); - let exes = r(0x00, 0x18); - exes.concat(['\x19']); - exes.concat(r(0x1c, 0x20)); - for (let i = 0; i < exes.length; ++i) { - parser.currentState = 4; - parser.parse(exes[i]); - chai.expect(parser.currentState).equal(4); - testTerminal.compare([['exe', exes[i]]]); - parser.reset(); - testTerminal.clear(); - } - }); - it('state CSI_PARAM param action', function (): void { - parser.reset(); - let params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; - for (let i = 0; i < params.length; ++i) { - parser.currentState = 4; - parser.parse(params[i]); - chai.expect(parser.currentState).equal(4); - chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); - parser.reset(); - } - parser.currentState = 4; - parser.parse('\x3b'); - chai.expect(parser.currentState).equal(4); - chai.expect(parser.params).eql([0, 0]); - parser.reset(); - }); - it('state CSI_PARAM ignore', function (): void { - parser.reset(); - testTerminal.clear(); - parser.currentState = 4; - parser.parse('\x7f'); - chai.expect(parser.currentState).equal(4); - testTerminal.compare([]); - parser.reset(); - testTerminal.clear(); - }); - it('trans CSI_PARAM --> GROUND with csi_dispatch action', function (): void { - parser.reset(); - let dispatches = r(0x40, 0x7f); - for (let i = 0; i < dispatches.length; ++i) { - parser.currentState = 4; - parser.params = [0, 1]; - parser.parse(dispatches[i]); - chai.expect(parser.currentState).equal(0); - testTerminal.compare([['csi', '', [0, 1], dispatches[i]]]); - parser.reset(); - testTerminal.clear(); - } - }); - it('trans CSI_ENTRY --> CSI_INTERMEDIATE with collect action', function (): void { - parser.reset(); - let collect = r(0x20, 0x30); - for (let i = 0; i < collect.length; ++i) { - parser.currentState = 3; - parser.parse(collect[i]); - chai.expect(parser.currentState).equal(5); - chai.expect(parser.collected).equal(collect[i]); - parser.reset(); - } - }); - it('trans CSI_PARAM --> CSI_INTERMEDIATE with collect action', function (): void { - parser.reset(); - let collect = r(0x20, 0x30); - for (let i = 0; i < collect.length; ++i) { - parser.currentState = 4; - parser.parse(collect[i]); - chai.expect(parser.currentState).equal(5); - chai.expect(parser.collected).equal(collect[i]); - parser.reset(); - } - }); - it('state CSI_INTERMEDIATE execute rules', function (): void { - parser.reset(); - testTerminal.clear(); - let exes = r(0x00, 0x18); - exes.concat(['\x19']); - exes.concat(r(0x1c, 0x20)); - for (let i = 0; i < exes.length; ++i) { - parser.currentState = 5; - parser.parse(exes[i]); - chai.expect(parser.currentState).equal(5); - testTerminal.compare([['exe', exes[i]]]); - parser.reset(); - testTerminal.clear(); - } - }); - it('state CSI_INTERMEDIATE collect', function (): void { - parser.reset(); - let collect = r(0x20, 0x30); - for (let i = 0; i < collect.length; ++i) { - parser.currentState = 5; - parser.parse(collect[i]); - chai.expect(parser.currentState).equal(5); - chai.expect(parser.collected).equal(collect[i]); - parser.reset(); - } - }); - it('state CSI_INTERMEDIATE ignore', function (): void { - parser.reset(); - testTerminal.clear(); - parser.currentState = 5; - parser.parse('\x7f'); - chai.expect(parser.currentState).equal(5); - testTerminal.compare([]); - parser.reset(); - testTerminal.clear(); - }); - it('trans CSI_INTERMEDIATE --> GROUND with csi_dispatch action', function (): void { - parser.reset(); - let dispatches = r(0x40, 0x7f); - for (let i = 0; i < dispatches.length; ++i) { - parser.currentState = 5; - parser.params = [0, 1]; - parser.parse(dispatches[i]); - chai.expect(parser.currentState).equal(0); - testTerminal.compare([['csi', '', [0, 1], dispatches[i]]]); - parser.reset(); - testTerminal.clear(); - } - }); - it('trans CSI_ENTRY --> CSI_IGNORE', function (): void { - parser.reset(); - parser.currentState = 3; - parser.parse('\x3a'); - chai.expect(parser.currentState).equal(6); - parser.reset(); - }); - it('trans CSI_PARAM --> CSI_IGNORE', function (): void { - parser.reset(); - let chars = ['\x3a', '\x3c', '\x3d', '\x3e', '\x3f']; - for (let i = 0; i < chars.length; ++i) { - parser.currentState = 4; - parser.parse('\x3b' + chars[i]); - chai.expect(parser.currentState).equal(6); - chai.expect(parser.params).eql([0, 0]); - parser.reset(); - } - }); - it('trans CSI_INTERMEDIATE --> CSI_IGNORE', function (): void { - parser.reset(); - let chars = r(0x30, 0x40); - for (let i = 0; i < chars.length; ++i) { - parser.currentState = 5; - parser.parse(chars[i]); - chai.expect(parser.currentState).equal(6); - chai.expect(parser.params).eql([0]); - parser.reset(); - } - }); - it('state CSI_IGNORE execute rules', function (): void { - parser.reset(); - testTerminal.clear(); - let exes = r(0x00, 0x18); - exes.concat(['\x19']); - exes.concat(r(0x1c, 0x20)); - for (let i = 0; i < exes.length; ++i) { - parser.currentState = 6; - parser.parse(exes[i]); - chai.expect(parser.currentState).equal(6); - testTerminal.compare([['exe', exes[i]]]); - parser.reset(); - testTerminal.clear(); - } - }); - it('state CSI_IGNORE ignore', function (): void { - parser.reset(); - testTerminal.clear(); - let ignored = r(0x20, 0x40); - ignored.concat(['\x7f']); - for (let i = 0; i < ignored.length; ++i) { - parser.currentState = 6; - parser.parse(ignored[i]); - chai.expect(parser.currentState).equal(6); - testTerminal.compare([]); - parser.reset(); - testTerminal.clear(); - } - }); - it('trans CSI_IGNORE --> GROUND', function (): void { - parser.reset(); - let dispatches = r(0x40, 0x7f); - for (let i = 0; i < dispatches.length; ++i) { - parser.currentState = 6; - parser.params = [0, 1]; - parser.parse(dispatches[i]); - chai.expect(parser.currentState).equal(0); - testTerminal.compare([]); - parser.reset(); - testTerminal.clear(); - } - }); - it('trans ANYWHERE/ESCAPE --> SOS_PM_APC_STRING', function (): void { - parser.reset(); - // C0 - let initializers = ['\x58', '\x5e', '\x5f']; - for (let i = 0; i < initializers.length; ++i) { - parser.parse('\x1b' + initializers[i]); - chai.expect(parser.currentState).equal(7); - parser.reset(); - } - // C1 - for (let state = 0; state < 14; ++state) { - parser.currentState = state; - initializers = ['\x98', '\x9e', '\x9f']; - for (let i = 0; i < initializers.length; ++i) { - parser.parse(initializers[i]); - chai.expect(parser.currentState).equal(7); + // C1 + for (state in states) { + parser.currentState = state; + parser.osc = '#'; + parser.params = [123]; + parser.collected = '#'; + parser.parse('\x9b'); + chai.expect(parser.currentState).equal(STATE.CSI_ENTRY); + chai.expect(parser.osc).equal(''); + chai.expect(parser.params).eql([0]); + chai.expect(parser.collected).equal(''); parser.reset(); } - } - }); - it('state SOS_PM_APC_STRING ignore rules', function (): void { - parser.reset(); - let ignored = r(0x00, 0x18); - ignored.concat(['\x19']); - ignored.concat(r(0x1c, 0x20)); - ignored.concat(r(0x20, 0x80)); - for (let i = 0; i < ignored.length; ++i) { - parser.currentState = 7; - parser.parse(ignored[i]); - chai.expect(parser.currentState).equal(7); + }); + it('state CSI_ENTRY execute rules', function (): void { parser.reset(); - } - }); - it('trans ANYWHERE/ESCAPE --> OSC_STRING', function (): void { - parser.reset(); - // C0 - parser.parse('\x1b]'); - chai.expect(parser.currentState).equal(8); - parser.reset(); - // C1 - for (let state = 0; state < 14; ++state) { - parser.currentState = state; - parser.parse('\x9d'); - chai.expect(parser.currentState).equal(8); + testTerminal.clear(); + let exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = STATE.CSI_ENTRY; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(STATE.CSI_ENTRY); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state CSI_ENTRY ignore', function (): void { parser.reset(); - } - }); - it('state OSC_STRING ignore rules', function (): void { - parser.reset(); - let ignored = [ - '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', /*'\x07',*/ '\x08', - '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', - '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f']; - for (let i = 0; i < ignored.length; ++i) { - parser.currentState = 8; - parser.parse(ignored[i]); - chai.expect(parser.currentState).equal(8); - chai.expect(parser.osc).equal(''); + testTerminal.clear(); + parser.currentState = STATE.CSI_ENTRY; + parser.parse('\x7f'); + chai.expect(parser.currentState).equal(STATE.CSI_ENTRY); + testTerminal.compare([]); parser.reset(); - } - }); - it('state OSC_STRING put action', function (): void { - parser.reset(); - let puts = r(0x20, 0x80); - for (let i = 0; i < puts.length; ++i) { - parser.currentState = 8; - parser.parse(puts[i]); - chai.expect(parser.currentState).equal(8); - chai.expect(parser.osc).equal(puts[i]); + testTerminal.clear(); + }); + it('trans CSI_ENTRY --> GROUND with csi_dispatch action', function (): void { parser.reset(); - } - }); - it('state DCS_ENTRY', function (): void { - parser.reset(); - // C0 - parser.parse('\x1bP'); - chai.expect(parser.currentState).equal(9); - parser.reset(); - // C1 - for (let state = 0; state < 14; ++state) { - parser.currentState = state; - parser.parse('\x90'); - chai.expect(parser.currentState).equal(9); + let dispatches = r(0x40, 0x7f); + for (let i = 0; i < dispatches.length; ++i) { + parser.currentState = STATE.CSI_ENTRY; + parser.parse(dispatches[i]); + chai.expect(parser.currentState).equal(STATE.GROUND); + testTerminal.compare([['csi', '', [0], dispatches[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans CSI_ENTRY --> CSI_PARAM with param/collect actions', function (): void { parser.reset(); - } - }); - it('state DCS_ENTRY ignore rules', function (): void { - parser.reset(); - let ignored = [ - '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', - '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', - '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; - for (let i = 0; i < ignored.length; ++i) { - parser.currentState = 9; - parser.parse(ignored[i]); - chai.expect(parser.currentState).equal(9); - parser.reset(); - } - }); - it('state DCS_ENTRY --> DCS_PARAM with param/collect actions', function (): void { - parser.reset(); - let params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; - let collect = ['\x3c', '\x3d', '\x3e', '\x3f']; - for (let i = 0; i < params.length; ++i) { - parser.currentState = 9; - parser.parse(params[i]); - chai.expect(parser.currentState).equal(10); - chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); - parser.reset(); - } - parser.currentState = 9; - parser.parse('\x3b'); - chai.expect(parser.currentState).equal(10); - chai.expect(parser.params).eql([0, 0]); - parser.reset(); - for (let i = 0; i < collect.length; ++i) { - parser.currentState = 9; - parser.parse(collect[i]); - chai.expect(parser.currentState).equal(10); - chai.expect(parser.collected).equal(collect[i]); - parser.reset(); - } - }); - it('state DCS_PARAM ignore rules', function (): void { - parser.reset(); - let ignored = [ - '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', - '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', - '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; - for (let i = 0; i < ignored.length; ++i) { - parser.currentState = 10; - parser.parse(ignored[i]); - chai.expect(parser.currentState).equal(10); - parser.reset(); - } - }); - it('state DCS_PARAM param action', function (): void { - parser.reset(); - let params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; - for (let i = 0; i < params.length; ++i) { - parser.currentState = 10; - parser.parse(params[i]); - chai.expect(parser.currentState).equal(10); - chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); - parser.reset(); - } - parser.currentState = 10; - parser.parse('\x3b'); - chai.expect(parser.currentState).equal(10); - chai.expect(parser.params).eql([0, 0]); - parser.reset(); - }); - it('trans DCS_ENTRY --> DCS_IGNORE', function (): void { - parser.reset(); - parser.currentState = 9; - parser.parse('\x3a'); - chai.expect(parser.currentState).equal(11); - parser.reset(); - }); - it('trans DCS_PARAM --> DCS_IGNORE', function (): void { - parser.reset(); - let chars = ['\x3a', '\x3c', '\x3d', '\x3e', '\x3f']; - for (let i = 0; i < chars.length; ++i) { - parser.currentState = 10; - parser.parse('\x3b' + chars[i]); - chai.expect(parser.currentState).equal(11); + let params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; + let collect = ['\x3c', '\x3d', '\x3e', '\x3f']; + for (let i = 0; i < params.length; ++i) { + parser.currentState = STATE.CSI_ENTRY; + parser.parse(params[i]); + chai.expect(parser.currentState).equal(STATE.CSI_PARAM); + chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); + parser.reset(); + } + parser.currentState = STATE.CSI_ENTRY; + parser.parse('\x3b'); + chai.expect(parser.currentState).equal(STATE.CSI_PARAM); chai.expect(parser.params).eql([0, 0]); parser.reset(); - } - }); - it('trans DCS_INTERMEDIATE --> DCS_IGNORE', function (): void { - parser.reset(); - let chars = r(0x30, 0x40); - for (let i = 0; i < chars.length; ++i) { - parser.currentState = 12; - parser.parse(chars[i]); - chai.expect(parser.currentState).equal(11); - parser.reset(); - } - }); - it('state DCS_IGNORE ignore rules', function (): void { - parser.reset(); - let ignored = [ - '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', - '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', - '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; - ignored.concat(r(0x20, 0x80)); - for (let i = 0; i < ignored.length; ++i) { - parser.currentState = 11; - parser.parse(ignored[i]); - chai.expect(parser.currentState).equal(11); - parser.reset(); - } - }); - it('trans DCS_ENTRY --> DCS_INTERMEDIATE with collect action', function (): void { - parser.reset(); - let collect = r(0x20, 0x30); - for (let i = 0; i < collect.length; ++i) { - parser.currentState = 9; - parser.parse(collect[i]); - chai.expect(parser.currentState).equal(12); - chai.expect(parser.collected).equal(collect[i]); - parser.reset(); - } - }); - it('trans DCS_PARAM --> DCS_INTERMEDIATE with collect action', function (): void { - parser.reset(); - let collect = r(0x20, 0x30); - for (let i = 0; i < collect.length; ++i) { - parser.currentState = 10; - parser.parse(collect[i]); - chai.expect(parser.currentState).equal(12); - chai.expect(parser.collected).equal(collect[i]); - parser.reset(); - } - }); - it('state DCS_INTERMEDIATE ignore rules', function (): void { - parser.reset(); - let ignored = [ - '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', - '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', - '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; - for (let i = 0; i < ignored.length; ++i) { - parser.currentState = 12; - parser.parse(ignored[i]); - chai.expect(parser.currentState).equal(12); - parser.reset(); - } - }); - it('state DCS_INTERMEDIATE collect action', function (): void { - parser.reset(); - let collect = r(0x20, 0x30); - for (let i = 0; i < collect.length; ++i) { - parser.currentState = 12; - parser.parse(collect[i]); - chai.expect(parser.currentState).equal(12); - chai.expect(parser.collected).equal(collect[i]); - parser.reset(); - } - }); - it('trans DCS_INTERMEDIATE --> DCS_IGNORE', function (): void { - parser.reset(); - let chars = r(0x30, 0x40); - for (let i = 0; i < chars.length; ++i) { - parser.currentState = 12; - parser.parse('\x20' + chars[i]); - chai.expect(parser.currentState).equal(11); - chai.expect(parser.collected).equal('\x20'); - parser.reset(); - } - }); - it('trans DCS_ENTRY --> DCS_PASSTHROUGH with hook', function (): void { - parser.reset(); - testTerminal.clear(); - let collect = r(0x40, 0x7f); - for (let i = 0; i < collect.length; ++i) { - parser.currentState = 9; - parser.parse(collect[i]); - chai.expect(parser.currentState).equal(13); - testTerminal.compare([['dcs hook', '', [0], collect[i]]]); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = STATE.CSI_ENTRY; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(STATE.CSI_PARAM); + chai.expect(parser.collected).equal(collect[i]); + parser.reset(); + } + }); + it('state CSI_PARAM execute rules', function (): void { parser.reset(); testTerminal.clear(); - } - }); - it('trans DCS_PARAM --> DCS_PASSTHROUGH with hook', function (): void { - parser.reset(); - testTerminal.clear(); - let collect = r(0x40, 0x7f); - for (let i = 0; i < collect.length; ++i) { - parser.currentState = 10; - parser.parse(collect[i]); - chai.expect(parser.currentState).equal(13); - testTerminal.compare([['dcs hook', '', [0], collect[i]]]); + let exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = STATE.CSI_PARAM; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(STATE.CSI_PARAM); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state CSI_PARAM param action', function (): void { + parser.reset(); + let params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; + for (let i = 0; i < params.length; ++i) { + parser.currentState = STATE.CSI_PARAM; + parser.parse(params[i]); + chai.expect(parser.currentState).equal(STATE.CSI_PARAM); + chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); + parser.reset(); + } + parser.currentState = STATE.CSI_PARAM; + parser.parse('\x3b'); + chai.expect(parser.currentState).equal(STATE.CSI_PARAM); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + }); + it('state CSI_PARAM ignore', function (): void { parser.reset(); testTerminal.clear(); - } - }); - it('trans DCS_INTERMEDIATE --> DCS_PASSTHROUGH with hook', function (): void { - parser.reset(); - testTerminal.clear(); - let collect = r(0x40, 0x7f); - for (let i = 0; i < collect.length; ++i) { - parser.currentState = 12; - parser.parse(collect[i]); - chai.expect(parser.currentState).equal(13); - testTerminal.compare([['dcs hook', '', [0], collect[i]]]); + parser.currentState = STATE.CSI_PARAM; + parser.parse('\x7f'); + chai.expect(parser.currentState).equal(STATE.CSI_PARAM); + testTerminal.compare([]); parser.reset(); testTerminal.clear(); - } - }); - it('state DCS_PASSTHROUGH put action', function (): void { - parser.reset(); - testTerminal.clear(); - let puts = r(0x00, 0x18); - puts.concat(['\x19']); - puts.concat(r(0x1c, 0x20)); - puts.concat(r(0x20, 0x7f)); - for (let i = 0; i < puts.length; ++i) { - parser.currentState = 13; - parser.parse(puts[i]); - chai.expect(parser.currentState).equal(13); - testTerminal.compare([['dcs put', puts[i]]]); + }); + it('trans CSI_PARAM --> GROUND with csi_dispatch action', function (): void { + parser.reset(); + let dispatches = r(0x40, 0x7f); + for (let i = 0; i < dispatches.length; ++i) { + parser.currentState = STATE.CSI_PARAM; + parser.params = [0, 1]; + parser.parse(dispatches[i]); + chai.expect(parser.currentState).equal(STATE.GROUND); + testTerminal.compare([['csi', '', [0, 1], dispatches[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans CSI_ENTRY --> CSI_INTERMEDIATE with collect action', function (): void { + parser.reset(); + let collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = STATE.CSI_ENTRY; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(STATE.CSI_INTERMEDIATE); + chai.expect(parser.collected).equal(collect[i]); + parser.reset(); + } + }); + it('trans CSI_PARAM --> CSI_INTERMEDIATE with collect action', function (): void { + parser.reset(); + let collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = STATE.CSI_PARAM; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(STATE.CSI_INTERMEDIATE); + chai.expect(parser.collected).equal(collect[i]); + parser.reset(); + } + }); + it('state CSI_INTERMEDIATE execute rules', function (): void { parser.reset(); testTerminal.clear(); - } + let exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = STATE.CSI_INTERMEDIATE; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(STATE.CSI_INTERMEDIATE); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state CSI_INTERMEDIATE collect', function (): void { + parser.reset(); + let collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = STATE.CSI_INTERMEDIATE; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(STATE.CSI_INTERMEDIATE); + chai.expect(parser.collected).equal(collect[i]); + parser.reset(); + } + }); + it('state CSI_INTERMEDIATE ignore', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = STATE.CSI_INTERMEDIATE; + parser.parse('\x7f'); + chai.expect(parser.currentState).equal(STATE.CSI_INTERMEDIATE); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('trans CSI_INTERMEDIATE --> GROUND with csi_dispatch action', function (): void { + parser.reset(); + let dispatches = r(0x40, 0x7f); + for (let i = 0; i < dispatches.length; ++i) { + parser.currentState = STATE.CSI_INTERMEDIATE; + parser.params = [0, 1]; + parser.parse(dispatches[i]); + chai.expect(parser.currentState).equal(STATE.GROUND); + testTerminal.compare([['csi', '', [0, 1], dispatches[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans CSI_ENTRY --> CSI_IGNORE', function (): void { + parser.reset(); + parser.currentState = STATE.CSI_ENTRY; + parser.parse('\x3a'); + chai.expect(parser.currentState).equal(STATE.CSI_IGNORE); + parser.reset(); + }); + it('trans CSI_PARAM --> CSI_IGNORE', function (): void { + parser.reset(); + let chars = ['\x3a', '\x3c', '\x3d', '\x3e', '\x3f']; + for (let i = 0; i < chars.length; ++i) { + parser.currentState = STATE.CSI_PARAM; + parser.parse('\x3b' + chars[i]); + chai.expect(parser.currentState).equal(STATE.CSI_IGNORE); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + } + }); + it('trans CSI_INTERMEDIATE --> CSI_IGNORE', function (): void { + parser.reset(); + let chars = r(0x30, 0x40); + for (let i = 0; i < chars.length; ++i) { + parser.currentState = STATE.CSI_INTERMEDIATE; + parser.parse(chars[i]); + chai.expect(parser.currentState).equal(STATE.CSI_IGNORE); + chai.expect(parser.params).eql([0]); + parser.reset(); + } + }); + it('state CSI_IGNORE execute rules', function (): void { + parser.reset(); + testTerminal.clear(); + let exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = STATE.CSI_IGNORE; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(STATE.CSI_IGNORE); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state CSI_IGNORE ignore', function (): void { + parser.reset(); + testTerminal.clear(); + let ignored = r(0x20, 0x40); + ignored.concat(['\x7f']); + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = STATE.CSI_IGNORE; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(STATE.CSI_IGNORE); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans CSI_IGNORE --> GROUND', function (): void { + parser.reset(); + let dispatches = r(0x40, 0x7f); + for (let i = 0; i < dispatches.length; ++i) { + parser.currentState = STATE.CSI_IGNORE; + parser.params = [0, 1]; + parser.parse(dispatches[i]); + chai.expect(parser.currentState).equal(STATE.GROUND); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans ANYWHERE/ESCAPE --> SOS_PM_APC_STRING', function (): void { + parser.reset(); + // C0 + let initializers = ['\x58', '\x5e', '\x5f']; + for (let i = 0; i < initializers.length; ++i) { + parser.parse('\x1b' + initializers[i]); + chai.expect(parser.currentState).equal(STATE.SOS_PM_APC_STRING); + parser.reset(); + } + // C1 + for (state in states) { + parser.currentState = state; + initializers = ['\x98', '\x9e', '\x9f']; + for (let i = 0; i < initializers.length; ++i) { + parser.parse(initializers[i]); + chai.expect(parser.currentState).equal(STATE.SOS_PM_APC_STRING); + parser.reset(); + } + } + }); + it('state SOS_PM_APC_STRING ignore rules', function (): void { + parser.reset(); + let ignored = r(0x00, 0x18); + ignored.concat(['\x19']); + ignored.concat(r(0x1c, 0x20)); + ignored.concat(r(0x20, 0x80)); + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = STATE.SOS_PM_APC_STRING; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(STATE.SOS_PM_APC_STRING); + parser.reset(); + } + }); + it('trans ANYWHERE/ESCAPE --> OSC_STRING', function (): void { + parser.reset(); + // C0 + parser.parse('\x1b]'); + chai.expect(parser.currentState).equal(STATE.OSC_STRING); + parser.reset(); + // C1 + for (state in states) { + parser.currentState = state; + parser.parse('\x9d'); + chai.expect(parser.currentState).equal(STATE.OSC_STRING); + parser.reset(); + } + }); + it('state OSC_STRING ignore rules', function (): void { + parser.reset(); + let ignored = [ + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', /*'\x07',*/ '\x08', + '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', + '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f']; + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = STATE.OSC_STRING; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(STATE.OSC_STRING); + chai.expect(parser.osc).equal(''); + parser.reset(); + } + }); + it('state OSC_STRING put action', function (): void { + parser.reset(); + let puts = r(0x20, 0x80); + for (let i = 0; i < puts.length; ++i) { + parser.currentState = STATE.OSC_STRING; + parser.parse(puts[i]); + chai.expect(parser.currentState).equal(STATE.OSC_STRING); + chai.expect(parser.osc).equal(puts[i]); + parser.reset(); + } + }); + it('state DCS_ENTRY', function (): void { + parser.reset(); + // C0 + parser.parse('\x1bP'); + chai.expect(parser.currentState).equal(STATE.DCS_ENTRY); + parser.reset(); + // C1 + for (state in states) { + parser.currentState = state; + parser.parse('\x90'); + chai.expect(parser.currentState).equal(STATE.DCS_ENTRY); + parser.reset(); + } + }); + it('state DCS_ENTRY ignore rules', function (): void { + parser.reset(); + let ignored = [ + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', + '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', + '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = STATE.DCS_ENTRY; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(STATE.DCS_ENTRY); + parser.reset(); + } + }); + it('state DCS_ENTRY --> DCS_PARAM with param/collect actions', function (): void { + parser.reset(); + let params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; + let collect = ['\x3c', '\x3d', '\x3e', '\x3f']; + for (let i = 0; i < params.length; ++i) { + parser.currentState = STATE.DCS_ENTRY; + parser.parse(params[i]); + chai.expect(parser.currentState).equal(STATE.DCS_PARAM); + chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); + parser.reset(); + } + parser.currentState = STATE.DCS_ENTRY; + parser.parse('\x3b'); + chai.expect(parser.currentState).equal(STATE.DCS_PARAM); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = STATE.DCS_ENTRY; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(STATE.DCS_PARAM); + chai.expect(parser.collected).equal(collect[i]); + parser.reset(); + } + }); + it('state DCS_PARAM ignore rules', function (): void { + parser.reset(); + let ignored = [ + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', + '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', + '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = STATE.DCS_PARAM; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(STATE.DCS_PARAM); + parser.reset(); + } + }); + it('state DCS_PARAM param action', function (): void { + parser.reset(); + let params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; + for (let i = 0; i < params.length; ++i) { + parser.currentState = STATE.DCS_PARAM; + parser.parse(params[i]); + chai.expect(parser.currentState).equal(STATE.DCS_PARAM); + chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); + parser.reset(); + } + parser.currentState = STATE.DCS_PARAM; + parser.parse('\x3b'); + chai.expect(parser.currentState).equal(STATE.DCS_PARAM); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + }); + it('trans DCS_ENTRY --> DCS_IGNORE', function (): void { + parser.reset(); + parser.currentState = STATE.DCS_ENTRY; + parser.parse('\x3a'); + chai.expect(parser.currentState).equal(STATE.DCS_IGNORE); + parser.reset(); + }); + it('trans DCS_PARAM --> DCS_IGNORE', function (): void { + parser.reset(); + let chars = ['\x3a', '\x3c', '\x3d', '\x3e', '\x3f']; + for (let i = 0; i < chars.length; ++i) { + parser.currentState = STATE.DCS_PARAM; + parser.parse('\x3b' + chars[i]); + chai.expect(parser.currentState).equal(STATE.DCS_IGNORE); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + } + }); + it('trans DCS_INTERMEDIATE --> DCS_IGNORE', function (): void { + parser.reset(); + let chars = r(0x30, 0x40); + for (let i = 0; i < chars.length; ++i) { + parser.currentState = STATE.DCS_INTERMEDIATE; + parser.parse(chars[i]); + chai.expect(parser.currentState).equal(STATE.DCS_IGNORE); + parser.reset(); + } + }); + it('state DCS_IGNORE ignore rules', function (): void { + parser.reset(); + let ignored = [ + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', + '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', + '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; + ignored.concat(r(0x20, 0x80)); + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = STATE.DCS_IGNORE; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(STATE.DCS_IGNORE); + parser.reset(); + } + }); + it('trans DCS_ENTRY --> DCS_INTERMEDIATE with collect action', function (): void { + parser.reset(); + let collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = STATE.DCS_ENTRY; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(STATE.DCS_INTERMEDIATE); + chai.expect(parser.collected).equal(collect[i]); + parser.reset(); + } + }); + it('trans DCS_PARAM --> DCS_INTERMEDIATE with collect action', function (): void { + parser.reset(); + let collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = STATE.DCS_PARAM; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(STATE.DCS_INTERMEDIATE); + chai.expect(parser.collected).equal(collect[i]); + parser.reset(); + } + }); + it('state DCS_INTERMEDIATE ignore rules', function (): void { + parser.reset(); + let ignored = [ + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', + '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', + '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = STATE.DCS_INTERMEDIATE; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(STATE.DCS_INTERMEDIATE); + parser.reset(); + } + }); + it('state DCS_INTERMEDIATE collect action', function (): void { + parser.reset(); + let collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = STATE.DCS_INTERMEDIATE; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(STATE.DCS_INTERMEDIATE); + chai.expect(parser.collected).equal(collect[i]); + parser.reset(); + } + }); + it('trans DCS_INTERMEDIATE --> DCS_IGNORE', function (): void { + parser.reset(); + let chars = r(0x30, 0x40); + for (let i = 0; i < chars.length; ++i) { + parser.currentState = STATE.DCS_INTERMEDIATE; + parser.parse('\x20' + chars[i]); + chai.expect(parser.currentState).equal(STATE.DCS_IGNORE); + chai.expect(parser.collected).equal('\x20'); + parser.reset(); + } + }); + it('trans DCS_ENTRY --> DCS_PASSTHROUGH with hook', function (): void { + parser.reset(); + testTerminal.clear(); + let collect = r(0x40, 0x7f); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = STATE.DCS_ENTRY; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(STATE.DCS_PASSTHROUGH); + testTerminal.compare([['dcs hook', '', [0], collect[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans DCS_PARAM --> DCS_PASSTHROUGH with hook', function (): void { + parser.reset(); + testTerminal.clear(); + let collect = r(0x40, 0x7f); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = STATE.DCS_PARAM; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(STATE.DCS_PASSTHROUGH); + testTerminal.compare([['dcs hook', '', [0], collect[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans DCS_INTERMEDIATE --> DCS_PASSTHROUGH with hook', function (): void { + parser.reset(); + testTerminal.clear(); + let collect = r(0x40, 0x7f); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = STATE.DCS_INTERMEDIATE; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(STATE.DCS_PASSTHROUGH); + testTerminal.compare([['dcs hook', '', [0], collect[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state DCS_PASSTHROUGH put action', function (): void { + parser.reset(); + testTerminal.clear(); + let puts = r(0x00, 0x18); + puts.concat(['\x19']); + puts.concat(r(0x1c, 0x20)); + puts.concat(r(0x20, 0x7f)); + for (let i = 0; i < puts.length; ++i) { + parser.currentState = STATE.DCS_PASSTHROUGH; + parser.parse(puts[i]); + chai.expect(parser.currentState).equal(STATE.DCS_PASSTHROUGH); + testTerminal.compare([['dcs put', puts[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state DCS_PASSTHROUGH ignore', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = STATE.DCS_PASSTHROUGH; + parser.parse('\x7f'); + chai.expect(parser.currentState).equal(STATE.DCS_PASSTHROUGH); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); }); - it('state DCS_PASSTHROUGH ignore', function (): void { - parser.reset(); - testTerminal.clear(); - parser.currentState = 13; - parser.parse('\x7f'); - chai.expect(parser.currentState).equal(13); - testTerminal.compare([]); - parser.reset(); - testTerminal.clear(); - }); -}); -function test(s: string, value: any, noReset: any): void { - if (!noReset) { - parser.reset(); - testTerminal.clear(); + function test(s: string, value: any, noReset: any): void { + if (!noReset) { + parser.reset(); + testTerminal.clear(); + } + parser.parse(s); + testTerminal.compare(value); } - parser.parse(s); - testTerminal.compare(value); -} -describe('escape sequence examples', function(): void { - it('CSI with print and execute', function (): void { - test('\x1b[<31;5mHello World! öäü€\nabc', - [ - ['csi', '<', [31, 5], 'm'], - ['print', 'Hello World! öäü€'], - ['exe', '\n'], - ['print', 'abc'] + describe('escape sequence examples', function(): void { + it('CSI with print and execute', function (): void { + test('\x1b[<31;5mHello World! öäü€\nabc', + [ + ['csi', '<', [31, 5], 'm'], + ['print', 'Hello World! öäü€'], + ['exe', '\n'], + ['print', 'abc'] + ], null); + }); + it('OSC', function (): void { + test('\x1b]0;abc123€öäü\x07', [ + ['osc', '0;abc123€öäü'] ], null); + }); + it('single DCS', function (): void { + test('\x1bP1;2;3+$abc;de\x9c', [ + ['dcs hook', '+$', [1, 2, 3], 'a'], + ['dcs put', 'bc;de'], + ['dcs unhook'] + ], null); + }); + it('multi DCS', function (): void { + test('\x1bP1;2;3+$abc;de', [ + ['dcs hook', '+$', [1, 2, 3], 'a'], + ['dcs put', 'bc;de'] + ], null); + testTerminal.clear(); + test('abc\x9c', [ + ['dcs put', 'abc'], + ['dcs unhook'] + ], true); + }); + it('print + DCS(C1)', function (): void { + test('abc\x901;2;3+$abc;de\x9c', [ + ['print', 'abc'], + ['dcs hook', '+$', [1, 2, 3], 'a'], + ['dcs put', 'bc;de'], + ['dcs unhook'] + ], null); + }); + it('print + PM(C1) + print', function (): void { + test('abc\x98123tzf\x9cdefg', [ + ['print', 'abc'], + ['print', 'defg'] + ], null); + }); + it('print + OSC(C1) + print', function (): void { + test('abc\x9d123tzf\x9cdefg', [ + ['print', 'abc'], + ['osc', '123tzf'], + ['print', 'defg'] + ], null); + }); + it('error recovery', function (): void { + test('\x1b[1€abcdefg\x9b<;c', [ + ['print', 'abcdefg'], + ['csi', '<', [0, 0], 'c'] + ], null); + }); }); - it('OSC', function (): void { - test('\x1b]0;abc123€öäü\x07', [ - ['osc', '0;abc123€öäü'] - ], null); + + describe('coverage tests', function(): void { + it('CSI_IGNORE error', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = STATE.CSI_IGNORE; + parser.parse('€öäü'); + chai.expect(parser.currentState).equal(STATE.CSI_IGNORE); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('DCS_IGNORE error', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = STATE.DCS_IGNORE; + parser.parse('€öäü'); + chai.expect(parser.currentState).equal(STATE.DCS_IGNORE); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('DCS_PASSTHROUGH error', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = STATE.DCS_PASSTHROUGH; + parser.parse('€öäü'); + chai.expect(parser.currentState).equal(STATE.DCS_PASSTHROUGH); + testTerminal.compare([['dcs put', '€öäü']]); + parser.reset(); + testTerminal.clear(); + }); + it('error else of if (code > 159)', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = STATE.GROUND; + parser.parse('\x1e'); + chai.expect(parser.currentState).equal(STATE.GROUND); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); }); - it('single DCS', function (): void { - test('\x1bP1;2;3+$abc;de\x9c', [ - ['dcs hook', '+$', [1, 2, 3], 'a'], - ['dcs put', 'bc;de'], - ['dcs unhook'] - ], null); - }); - it('multi DCS', function (): void { - test('\x1bP1;2;3+$abc;de', [ - ['dcs hook', '+$', [1, 2, 3], 'a'], - ['dcs put', 'bc;de'] - ], null); - testTerminal.clear(); - test('abc\x9c', [ - ['dcs put', 'abc'], - ['dcs unhook'] - ], true); - }); - it('print + DCS(C1)', function (): void { - test('abc\x901;2;3+$abc;de\x9c', [ - ['print', 'abc'], - ['dcs hook', '+$', [1, 2, 3], 'a'], - ['dcs put', 'bc;de'], - ['dcs unhook'] - ], null); - }); - it('print + PM(C1) + print', function (): void { - test('abc\x98123tzf\x9cdefg', [ - ['print', 'abc'], - ['print', 'defg'] - ], null); - }); - it('print + OSC(C1) + print', function (): void { - test('abc\x9d123tzf\x9cdefg', [ - ['print', 'abc'], - ['osc', '123tzf'], - ['print', 'defg'] - ], null); - }); - it('error recovery', function (): void { - test('\x1b[1€abcdefg\x9b<;c', [ - ['print', 'abcdefg'], - ['csi', '<', [0, 0], 'c'] - ], null); - }); -}); -describe('coverage tests', function(): void { - it('CSI_IGNORE error', function (): void { - parser.reset(); - testTerminal.clear(); - parser.currentState = 6; - parser.parse('€öäü'); - chai.expect(parser.currentState).equal(6); - testTerminal.compare([]); - parser.reset(); - testTerminal.clear(); - }); - it('DCS_IGNORE error', function (): void { - parser.reset(); - testTerminal.clear(); - parser.currentState = 11; - parser.parse('€öäü'); - chai.expect(parser.currentState).equal(11); - testTerminal.compare([]); - parser.reset(); - testTerminal.clear(); - }); - it('DCS_PASSTHROUGH error', function (): void { - parser.reset(); - testTerminal.clear(); - parser.currentState = 13; - parser.parse('€öäü'); - chai.expect(parser.currentState).equal(13); - testTerminal.compare([['dcs put', '€öäü']]); - parser.reset(); - testTerminal.clear(); - }); - it('error else of if (code > 159)', function (): void { - parser.reset(); - testTerminal.clear(); - parser.currentState = 0; - parser.parse('\x1e'); - chai.expect(parser.currentState).equal(0); - testTerminal.compare([]); - parser.reset(); - testTerminal.clear(); - }); -}); + let errorTerminal1 = function(): void {}; + errorTerminal1.prototype = testTerminal; + let errTerminal1 = new errorTerminal1(); + errTerminal1.actionError = function(e: any): void { + this.calls.push(['error', e]); + }; + let errParser1 = new EscapeSequenceParser(errTerminal1); -let errorTerminal1 = function(): void {}; -errorTerminal1.prototype = testTerminal; -let errTerminal1 = new errorTerminal1(); -errTerminal1.inst_E = function(e: any): void { - this.calls.push(['error', e]); -}; -let errParser1 = new AnsiParser(errTerminal1); + let errorTerminal2 = function(): void {}; + errorTerminal2.prototype = testTerminal; + let errTerminal2 = new errorTerminal2(); + errTerminal2.actionError = function(e: any): any { + this.calls.push(['error', e]); + return true; // --> abort parsing + }; + let errParser2 = new EscapeSequenceParser(errTerminal2); -let errorTerminal2 = function(): void {}; -errorTerminal2.prototype = testTerminal; -let errTerminal2 = new errorTerminal2(); -errTerminal2.inst_E = function(e: any): any { - this.calls.push(['error', e]); - return true; // --> abort parsing -}; -let errParser2 = new AnsiParser(errTerminal2); - -describe('error tests', function(): void { - it('CSI_PARAM unicode error - inst_E output w/o abort', function (): void { - errParser1.parse('\x1b[<31;5€normal print'); - errTerminal1.compare([ - ['error', { - pos: 7, - character: '€', - state: 4, - print: -1, - dcs: -1, - osc: '', - collect: '<', - params: [31, 5]}], - ['print', 'normal print'] - ]); - parser.reset(); - testTerminal.clear(); - }); - it('CSI_PARAM unicode error - inst_E output with abort', function (): void { - errParser2.parse('\x1b[<31;5€no print'); - errTerminal2.compare([ - ['error', { - pos: 7, - character: '€', - state: 4, - print: -1, - dcs: -1, - osc: '', - collect: '<', - params: [31, 5]}] - ]); - parser.reset(); - testTerminal.clear(); + describe('error tests', function(): void { + it('CSI_PARAM unicode error - actionError output w/o abort', function (): void { + errParser1.parse('\x1b[<31;5€normal print'); + errTerminal1.compare([ + ['error', { + pos: 7, + code: '€'.charCodeAt(0), + state: 4, + print: -1, + dcs: -1, + osc: '', + collect: '<', + params: [31, 5]}], + ['print', 'normal print'] + ]); + parser.reset(); + testTerminal.clear(); + }); + it('CSI_PARAM unicode error - actionError output with abort', function (): void { + errParser2.parse('\x1b[<31;5€no print'); + errTerminal2.compare([ + ['error', { + pos: 7, + code: '€'.charCodeAt(0), + state: 4, + print: -1, + dcs: -1, + osc: '', + collect: '<', + params: [31, 5]}] + ]); + parser.reset(); + testTerminal.clear(); + }); }); + }); diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index 6facddea..072ab882 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -1,17 +1,62 @@ +import { IInputHandler, IInputHandlingTerminal } from './Types'; +import { CHARSETS, DEFAULT_CHARSET } from './Charsets'; +import { C0 } from './EscapeSequences'; + + +// terminal interface for the escape sequence parser export interface IParserTerminal { - inst_p?: (s: string, start: number, end: number) => void; - inst_o?: (s: string) => void; - inst_x?: (flag: string) => void; - inst_c?: (collected: string, params: number[], flag: string) => void; - inst_e?: (collected: string, flag: string) => void; - inst_H?: (collected: string, params: number[], flag: string) => void; - inst_P?: (dcs: string) => void; - inst_U?: () => void; - inst_E?: () => void; // TODO: real signature + actionPrint?: (data: string, start: number, end: number) => void; + actionOSC?: (data: string) => void; + actionExecute?: (flag: string) => void; + actionCSI?: (collected: string, params: number[], flag: string) => void; + actionESC?: (collected: string, flag: string) => void; + actionDCSHook?: (collected: string, params: number[], flag: string) => void; + actionDCSPrint?: (data: string, start: number, end: number) => void; + actionDCSUnhook?: () => void; + actionError?: () => void; // FIXME: real signature and error handling } -export function r(a: number, b: number): number[] { +// FSM states +export const enum STATE { + GROUND = 0, + ESCAPE, + ESCAPE_INTERMEDIATE, + CSI_ENTRY, + CSI_PARAM, + CSI_INTERMEDIATE, + CSI_IGNORE, + SOS_PM_APC_STRING, + OSC_STRING, + DCS_ENTRY, + DCS_PARAM, + DCS_IGNORE, + DCS_INTERMEDIATE, + DCS_PASSTHROUGH +} + +// FSM actions +export const enum ACTION { + ignore = 0, + error, + print, + execute, + osc_start, + osc_put, + osc_end, + csi_dispatch, + param, + collect, + esc_dispatch, + clear, + dcs_hook, + dcs_put, + dcs_unhook +} + + +// number range macro +function r(a: number, b: number): number[] { let c = b - a; let arr = new Array(c); while (c--) { @@ -21,15 +66,20 @@ export function r(a: number, b: number): number[] { } +// transition table of the FSM +// TODO: fallback to array export class TransitionTable { public table: Uint8Array; + constructor(length: number) { this.table = new Uint8Array(length); } + add(inp: number, state: number, action: number | null, next: number | null): void { this.table[state << 8 | inp] = ((action | 0) << 4) | ((next === undefined) ? state : next); } - add_list(inps: number[], state: number, action: number | null, next: number | null): void { + + addMany(inps: number[], state: number, action: number | null, next: number | null): void { for (let i = 0; i < inps.length; i++) { this.add(inps[i], state, action, next); } @@ -37,125 +87,139 @@ export class TransitionTable { } +// default definitions of printable and executable characters let PRINTABLES = r(0x20, 0x7f); let EXECUTABLES = r(0x00, 0x18); EXECUTABLES.push(0x19); EXECUTABLES.concat(r(0x1c, 0x20)); +// default transition of the FSM is [error, GROUND] +let DEFAULT_TRANSITION = ACTION.error << 4 | STATE.GROUND; -export const TRANSITION_TABLE = (function (): TransitionTable { - let t: TransitionTable = new TransitionTable(4095); +// default DEC/ANSI compatible state transition table +// as defined by https://vt100.net/emu/dec_ansi_parser +export const VT500_TRANSITION_TABLE = (function (): TransitionTable { + let table: TransitionTable = new TransitionTable(4095); + + let states: number[] = r(STATE.GROUND, STATE.DCS_PASSTHROUGH + 1); + let state: any; // table with default transition [any] --> [error, GROUND] - for (let state = 0; state < 14; ++state) { + for (state in states) { + // table lookup is capped at 0xa0 in parse + // any higher will be treated by the error action for (let code = 0; code < 160; ++code) { - t[state << 8 | code] = 16; + table[state << 8 | code] = DEFAULT_TRANSITION; } } // apply transitions // printables - t.add_list(PRINTABLES, 0, 2, 0); + table.addMany(PRINTABLES, STATE.GROUND, ACTION.print, STATE.GROUND); // global anywhere rules - for (let state = 0; state < 14; ++state) { - t.add_list([0x18, 0x1a, 0x99, 0x9a], state, 3, 0); - t.add_list(r(0x80, 0x90), state, 3, 0); - t.add_list(r(0x90, 0x98), state, 3, 0); - t.add(0x9c, state, 0, 0); // ST as terminator - t.add(0x1b, state, 11, 1); // ESC - t.add(0x9d, state, 4, 8); // OSC - t.add_list([0x98, 0x9e, 0x9f], state, 0, 7); - t.add(0x9b, state, 11, 3); // CSI - t.add(0x90, state, 11, 9); // DCS + for (state in states) { + table.addMany([0x18, 0x1a, 0x99, 0x9a], state, ACTION.execute, STATE.GROUND); + table.addMany(r(0x80, 0x90), state, ACTION.execute, STATE.GROUND); + table.addMany(r(0x90, 0x98), state, ACTION.execute, STATE.GROUND); + table.add(0x9c, state, ACTION.ignore, STATE.GROUND); // ST as terminator + table.add(0x1b, state, ACTION.clear, STATE.ESCAPE); // ESC + table.add(0x9d, state, ACTION.osc_start, STATE.OSC_STRING); // OSC + table.addMany([0x98, 0x9e, 0x9f], state, ACTION.ignore, STATE.SOS_PM_APC_STRING); + table.add(0x9b, state, ACTION.clear, STATE.CSI_ENTRY); // CSI + table.add(0x90, state, ACTION.clear, STATE.DCS_ENTRY); // DCS } // rules for executables and 7f - t.add_list(EXECUTABLES, 0, 3, 0); - t.add_list(EXECUTABLES, 1, 3, 1); - t.add(0x7f, 1, null, 1); - t.add_list(EXECUTABLES, 8, null, 8); - t.add_list(EXECUTABLES, 3, 3, 3); - t.add(0x7f, 3, null, 3); - t.add_list(EXECUTABLES, 4, 3, 4); - t.add(0x7f, 4, null, 4); - t.add_list(EXECUTABLES, 6, 3, 6); - t.add_list(EXECUTABLES, 5, 3, 5); - t.add(0x7f, 5, null, 5); - t.add_list(EXECUTABLES, 2, 3, 2); - t.add(0x7f, 2, null, 2); + table.addMany(EXECUTABLES, STATE.GROUND, ACTION.execute, STATE.GROUND); + table.addMany(EXECUTABLES, STATE.ESCAPE, ACTION.execute, STATE.ESCAPE); + table.add(0x7f, STATE.ESCAPE, ACTION.ignore, STATE.ESCAPE); + table.addMany(EXECUTABLES, STATE.OSC_STRING, ACTION.ignore, STATE.OSC_STRING); + table.addMany(EXECUTABLES, STATE.CSI_ENTRY, ACTION.execute, STATE.CSI_ENTRY); + table.add(0x7f, STATE.CSI_ENTRY, ACTION.ignore, STATE.CSI_ENTRY); + table.addMany(EXECUTABLES, STATE.CSI_PARAM, ACTION.execute, STATE.CSI_PARAM); + table.add(0x7f, STATE.CSI_PARAM, ACTION.ignore, STATE.CSI_PARAM); + table.addMany(EXECUTABLES, STATE.CSI_IGNORE, ACTION.execute, STATE.CSI_IGNORE); + table.addMany(EXECUTABLES, STATE.CSI_INTERMEDIATE, ACTION.execute, STATE.CSI_INTERMEDIATE); + table.add(0x7f, STATE.CSI_INTERMEDIATE, ACTION.ignore, STATE.CSI_INTERMEDIATE); + table.addMany(EXECUTABLES, STATE.ESCAPE_INTERMEDIATE, ACTION.execute, STATE.ESCAPE_INTERMEDIATE); + table.add(0x7f, STATE.ESCAPE_INTERMEDIATE, ACTION.ignore, STATE.ESCAPE_INTERMEDIATE); // osc - t.add(0x5d, 1, 4, 8); - t.add_list(PRINTABLES, 8, 5, 8); - t.add(0x7f, 8, 5, 8); - t.add_list([0x9c, 0x1b, 0x18, 0x1a, 0x07], 8, 6, 0); - t.add_list(r(0x1c, 0x20), 8, 0, 8); + table.add(0x5d, STATE.ESCAPE, ACTION.osc_start, STATE.OSC_STRING); + table.addMany(PRINTABLES, STATE.OSC_STRING, ACTION.osc_put, STATE.OSC_STRING); + table.add(0x7f, STATE.OSC_STRING, ACTION.osc_put, STATE.OSC_STRING); + table.addMany([0x9c, 0x1b, 0x18, 0x1a, 0x07], STATE.OSC_STRING, ACTION.osc_end, STATE.GROUND); + table.addMany(r(0x1c, 0x20), STATE.OSC_STRING, ACTION.ignore, STATE.OSC_STRING); // sos/pm/apc does nothing - t.add_list([0x58, 0x5e, 0x5f], 1, 0, 7); - t.add_list(PRINTABLES, 7, null, 7); - t.add_list(EXECUTABLES, 7, null, 7); - t.add(0x9c, 7, 0, 0); + table.addMany([0x58, 0x5e, 0x5f], STATE.ESCAPE, ACTION.ignore, STATE.SOS_PM_APC_STRING); + table.addMany(PRINTABLES, STATE.SOS_PM_APC_STRING, ACTION.ignore, STATE.SOS_PM_APC_STRING); + table.addMany(EXECUTABLES, STATE.SOS_PM_APC_STRING, ACTION.ignore, STATE.SOS_PM_APC_STRING); + table.add(0x9c, STATE.SOS_PM_APC_STRING, ACTION.ignore, STATE.GROUND); // csi entries - t.add(0x5b, 1, 11, 3); - t.add_list(r(0x40, 0x7f), 3, 7, 0); - t.add_list(r(0x30, 0x3a), 3, 8, 4); - t.add(0x3b, 3, 8, 4); - t.add_list([0x3c, 0x3d, 0x3e, 0x3f], 3, 9, 4); - t.add_list(r(0x30, 0x3a), 4, 8, 4); - t.add(0x3b, 4, 8, 4); - t.add_list(r(0x40, 0x7f), 4, 7, 0); - t.add_list([0x3a, 0x3c, 0x3d, 0x3e, 0x3f], 4, 0, 6); - t.add_list(r(0x20, 0x40), 6, null, 6); - t.add(0x7f, 6, null, 6); - t.add_list(r(0x40, 0x7f), 6, 0, 0); - t.add(0x3a, 3, 0, 6); - t.add_list(r(0x20, 0x30), 3, 9, 5); - t.add_list(r(0x20, 0x30), 5, 9, 5); - t.add_list(r(0x30, 0x40), 5, 0, 6); - t.add_list(r(0x40, 0x7f), 5, 7, 0); - t.add_list(r(0x20, 0x30), 4, 9, 5); + table.add(0x5b, STATE.ESCAPE, ACTION.clear, STATE.CSI_ENTRY); + table.addMany(r(0x40, 0x7f), STATE.CSI_ENTRY, ACTION.csi_dispatch, STATE.GROUND); + table.addMany(r(0x30, 0x3a), STATE.CSI_ENTRY, ACTION.param, STATE.CSI_PARAM); + table.add(0x3b, STATE.CSI_ENTRY, ACTION.param, STATE.CSI_PARAM); + table.addMany([0x3c, 0x3d, 0x3e, 0x3f], STATE.CSI_ENTRY, ACTION.collect, STATE.CSI_PARAM); + table.addMany(r(0x30, 0x3a), STATE.CSI_PARAM, ACTION.param, STATE.CSI_PARAM); + table.add(0x3b, STATE.CSI_PARAM, ACTION.param, STATE.CSI_PARAM); + table.addMany(r(0x40, 0x7f), STATE.CSI_PARAM, ACTION.csi_dispatch, STATE.GROUND); + table.addMany([0x3a, 0x3c, 0x3d, 0x3e, 0x3f], STATE.CSI_PARAM, ACTION.ignore, STATE.CSI_IGNORE); + table.addMany(r(0x20, 0x40), STATE.CSI_IGNORE, null, STATE.CSI_IGNORE); + table.add(0x7f, STATE.CSI_IGNORE, null, STATE.CSI_IGNORE); + table.addMany(r(0x40, 0x7f), STATE.CSI_IGNORE, ACTION.ignore, STATE.GROUND); + table.add(0x3a, STATE.CSI_ENTRY, ACTION.ignore, STATE.CSI_IGNORE); + table.addMany(r(0x20, 0x30), STATE.CSI_ENTRY, ACTION.collect, STATE.CSI_INTERMEDIATE); + table.addMany(r(0x20, 0x30), STATE.CSI_INTERMEDIATE, ACTION.collect, STATE.CSI_INTERMEDIATE); + table.addMany(r(0x30, 0x40), STATE.CSI_INTERMEDIATE, ACTION.ignore, STATE.CSI_IGNORE); + table.addMany(r(0x40, 0x7f), STATE.CSI_INTERMEDIATE, ACTION.csi_dispatch, STATE.GROUND); + table.addMany(r(0x20, 0x30), STATE.CSI_PARAM, ACTION.collect, STATE.CSI_INTERMEDIATE); // esc_intermediate - t.add_list(r(0x20, 0x30), 1, 9, 2); - t.add_list(r(0x20, 0x30), 2, 9, 2); - t.add_list(r(0x30, 0x7f), 2, 10, 0); - t.add_list(r(0x30, 0x50), 1, 10, 0); - t.add_list([0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x59, 0x5a, 0x5c], 1, 10, 0); - t.add_list(r(0x60, 0x7f), 1, 10, 0); + table.addMany(r(0x20, 0x30), STATE.ESCAPE, ACTION.collect, STATE.ESCAPE_INTERMEDIATE); + table.addMany(r(0x20, 0x30), STATE.ESCAPE_INTERMEDIATE, ACTION.collect, STATE.ESCAPE_INTERMEDIATE); + table.addMany(r(0x30, 0x7f), STATE.ESCAPE_INTERMEDIATE, ACTION.esc_dispatch, STATE.GROUND); + table.addMany(r(0x30, 0x50), STATE.ESCAPE, ACTION.esc_dispatch, STATE.GROUND); + table.addMany(r(0x51, 0x58), STATE.ESCAPE, ACTION.esc_dispatch, STATE.GROUND); + table.addMany([0x59, 0x5a, 0x5c], STATE.ESCAPE, ACTION.esc_dispatch, STATE.GROUND); + table.addMany(r(0x60, 0x7f), STATE.ESCAPE, ACTION.esc_dispatch, STATE.GROUND); // dcs entry - t.add(0x50, 1, 11, 9); - t.add_list(EXECUTABLES, 9, null, 9); - t.add(0x7f, 9, null, 9); - t.add_list(r(0x1c, 0x20), 9, null, 9); - t.add_list(r(0x20, 0x30), 9, 9, 12); - t.add(0x3a, 9, 0, 11); - t.add_list(r(0x30, 0x3a), 9, 8, 10); - t.add(0x3b, 9, 8, 10); - t.add_list([0x3c, 0x3d, 0x3e, 0x3f], 9, 9, 10); - t.add_list(EXECUTABLES, 11, null, 11); - t.add_list(r(0x20, 0x80), 11, null, 11); - t.add_list(r(0x1c, 0x20), 11, null, 11); - t.add_list(EXECUTABLES, 10, null, 10); - t.add(0x7f, 10, null, 10); - t.add_list(r(0x1c, 0x20), 10, null, 10); - t.add_list(r(0x30, 0x3a), 10, 8, 10); - t.add(0x3b, 10, 8, 10); - t.add_list([0x3a, 0x3c, 0x3d, 0x3e, 0x3f], 10, 0, 11); - t.add_list(r(0x20, 0x30), 10, 9, 12); - t.add_list(EXECUTABLES, 12, null, 12); - t.add(0x7f, 12, null, 12); - t.add_list(r(0x1c, 0x20), 12, null, 12); - t.add_list(r(0x20, 0x30), 12, 9, 12); - t.add_list(r(0x30, 0x40), 12, 0, 11); - t.add_list(r(0x40, 0x7f), 12, 12, 13); - t.add_list(r(0x40, 0x7f), 10, 12, 13); - t.add_list(r(0x40, 0x7f), 9, 12, 13); - t.add_list(EXECUTABLES, 13, 13, 13); - t.add_list(PRINTABLES, 13, 13, 13); - t.add(0x7f, 13, null, 13); - t.add_list([0x1b, 0x9c], 13, 14, 0); + table.add(0x50, STATE.ESCAPE, ACTION.clear, STATE.DCS_ENTRY); + table.addMany(EXECUTABLES, STATE.DCS_ENTRY, ACTION.ignore, STATE.DCS_ENTRY); + table.add(0x7f, STATE.DCS_ENTRY, ACTION.ignore, STATE.DCS_ENTRY); + table.addMany(r(0x1c, 0x20), STATE.DCS_ENTRY, ACTION.ignore, STATE.DCS_ENTRY); + table.addMany(r(0x20, 0x30), STATE.DCS_ENTRY, ACTION.collect, STATE.DCS_INTERMEDIATE); + table.add(0x3a, STATE.DCS_ENTRY, ACTION.ignore, STATE.DCS_IGNORE); + table.addMany(r(0x30, 0x3a), STATE.DCS_ENTRY, ACTION.param, STATE.DCS_PARAM); + table.add(0x3b, STATE.DCS_ENTRY, ACTION.param, STATE.DCS_PARAM); + table.addMany([0x3c, 0x3d, 0x3e, 0x3f], STATE.DCS_ENTRY, ACTION.collect, STATE.DCS_PARAM); + table.addMany(EXECUTABLES, STATE.DCS_IGNORE, ACTION.ignore, STATE.DCS_IGNORE); + table.addMany(r(0x20, 0x80), STATE.DCS_IGNORE, ACTION.ignore, STATE.DCS_IGNORE); + table.addMany(r(0x1c, 0x20), STATE.DCS_IGNORE, ACTION.ignore, STATE.DCS_IGNORE); + table.addMany(EXECUTABLES, STATE.DCS_PARAM, ACTION.ignore, STATE.DCS_PARAM); + table.add(0x7f, STATE.DCS_PARAM, ACTION.ignore, STATE.DCS_PARAM); + table.addMany(r(0x1c, 0x20), STATE.DCS_PARAM, ACTION.ignore, STATE.DCS_PARAM); + table.addMany(r(0x30, 0x3a), STATE.DCS_PARAM, ACTION.param, STATE.DCS_PARAM); + table.add(0x3b, STATE.DCS_PARAM, ACTION.param, STATE.DCS_PARAM); + table.addMany([0x3a, 0x3c, 0x3d, 0x3e, 0x3f], STATE.DCS_PARAM, ACTION.ignore, STATE.DCS_IGNORE); + table.addMany(r(0x20, 0x30), STATE.DCS_PARAM, ACTION.collect, STATE.DCS_INTERMEDIATE); + table.addMany(EXECUTABLES, STATE.DCS_INTERMEDIATE, ACTION.ignore, STATE.DCS_INTERMEDIATE); + table.add(0x7f, STATE.DCS_INTERMEDIATE, ACTION.ignore, STATE.DCS_INTERMEDIATE); + table.addMany(r(0x1c, 0x20), STATE.DCS_INTERMEDIATE, ACTION.ignore, STATE.DCS_INTERMEDIATE); + table.addMany(r(0x20, 0x30), STATE.DCS_INTERMEDIATE, ACTION.collect, STATE.DCS_INTERMEDIATE); + table.addMany(r(0x30, 0x40), STATE.DCS_INTERMEDIATE, ACTION.ignore, STATE.DCS_IGNORE); + table.addMany(r(0x40, 0x7f), STATE.DCS_INTERMEDIATE, ACTION.dcs_hook, STATE.DCS_PASSTHROUGH); + table.addMany(r(0x40, 0x7f), STATE.DCS_PARAM, ACTION.dcs_hook, STATE.DCS_PASSTHROUGH); + table.addMany(r(0x40, 0x7f), STATE.DCS_ENTRY, ACTION.dcs_hook, STATE.DCS_PASSTHROUGH); + table.addMany(EXECUTABLES, STATE.DCS_PASSTHROUGH, ACTION.dcs_put, STATE.DCS_PASSTHROUGH); + table.addMany(PRINTABLES, STATE.DCS_PASSTHROUGH, ACTION.dcs_put, STATE.DCS_PASSTHROUGH); + table.add(0x7f, STATE.DCS_PASSTHROUGH, ACTION.ignore, STATE.DCS_PASSTHROUGH); + table.addMany([0x1b, 0x9c], STATE.DCS_PASSTHROUGH, ACTION.dcs_unhook, STATE.GROUND); - return t; + return table; })(); -export class AnsiParser { + +// default transition table points to global object +// Q: Copy table to allow custom sequences w'o changing global object? +export class EscapeSequenceParser { public initialState: number; public currentState: number; public transitions: TransitionTable; @@ -163,29 +227,34 @@ export class AnsiParser { public params: number[]; public collected: string; public term: any; - constructor(terminal: IParserTerminal) { - this.initialState = 0; - this.currentState = this.initialState | 0; - this.transitions = new TransitionTable(4095); - this.transitions.table.set(TRANSITION_TABLE.table); + constructor( + terminal?: IParserTerminal | any, + transitions: TransitionTable = VT500_TRANSITION_TABLE) + { + this.initialState = STATE.GROUND; + this.currentState = this.initialState; + this.transitions = transitions; this.osc = ''; this.params = [0]; this.collected = ''; this.term = terminal || {}; - let instructions = ['inst_p', 'inst_o', 'inst_x', 'inst_c', - 'inst_e', 'inst_H', 'inst_P', 'inst_U', 'inst_E']; + let instructions = [ + 'actionPrint', 'actionOSC', 'actionExecute', 'actionCSI', 'actionESC', + 'actionDCSHook', 'actionDCSPrint', 'actionDCSUnhook', 'actionError']; for (let i = 0; i < instructions.length; ++i) { if (!(instructions[i] in this.term)) { this.term[instructions[i]] = function(): void {}; } } } + reset(): void { this.currentState = this.initialState; this.osc = ''; this.params = [0]; this.collected = ''; } + parse(s: string): void { let code = 0; let transition = 0; @@ -204,79 +273,81 @@ export class AnsiParser { let l = s.length; for (let i = 0; i < l; ++i) { code = s.charCodeAt(i); + // shortcut for most chars (print action) - if (currentState === 0 && (code > 0x1f && code < 0x80)) { + if (currentState === STATE.GROUND && (code > 0x1f && code < 0x80)) { printed = (~printed) ? printed : i; continue; } - if (currentState === 4) { - if (code === 0x3b) { - params.push(0); - continue; - } - if (code > 0x2f && code < 0x39) { - params[params.length - 1] = params[params.length - 1] * 10 + code - 48; - continue; - } + + // shortcut for CSI params + if (currentState === STATE.CSI_PARAM && (code > 0x2f && code < 0x39)) { + params[params.length - 1] = params[params.length - 1] * 10 + code - 48; + continue; } - transition = ((code < 0xa0) ? (table[currentState << 8 | code]) : 16); + + // normal transition & action lookup + transition = (code < 0xa0) ? (table[currentState << 8 | code]) : DEFAULT_TRANSITION; switch (transition >> 4) { - case 2: // print + case ACTION.print: printed = (~printed) ? printed : i; break; - case 3: // execute - if (printed + 1) { - this.term.inst_p(s, printed, i); + case ACTION.execute: + if (~printed) { + this.term.actionPrint(s, printed, i); printed = -1; } - this.term.inst_x(String.fromCharCode(code)); + this.term.actionExecute(String.fromCharCode(code)); break; - case 0: // ignore - // handle leftover print and dcs chars - if (printed + 1) { - this.term.inst_p(s, printed, i); + case ACTION.ignore: + // handle leftover print or dcs chars + if (~printed) { + this.term.actionPrint(s, printed, i); printed = -1; - } else if (dcs + 1) { - this.term.inst_P(s.substring(dcs, i)); + } else if (~dcs) { + this.term.actionDCSPrint(s, dcs, i); dcs = -1; } break; - case 1: // error - // handle unicode chars in write buffers w'o state change + case ACTION.error: + // chars higher than 0x9f are handled by this action to + // keep the lookup table small if (code > 0x9f) { switch (currentState) { - case 0: // GROUND -> add char to print string + case STATE.GROUND: // add char to print string printed = (~printed) ? printed : i; break; - case 8: // OSC_STRING -> add char to osc string + case STATE.OSC_STRING: // add char to osc string osc += String.fromCharCode(code); - transition |= 8; + transition |= STATE.OSC_STRING; break; - case 6: // CSI_IGNORE -> ignore char - transition |= 6; + case STATE.CSI_IGNORE: // ignore char + transition |= STATE.CSI_IGNORE; break; - case 11: // DCS_IGNORE -> ignore char - transition |= 11; + case STATE.DCS_IGNORE: // ignore char + transition |= STATE.DCS_IGNORE; break; - case 13: // DCS_PASSTHROUGH -> add char to dcs - if (!(~dcs)) dcs = i | 0; - transition |= 13; + case STATE.DCS_PASSTHROUGH: // add char to dcs string + dcs = (~dcs) ? dcs : i; + transition |= STATE.DCS_PASSTHROUGH; break; - default: // real error + default: error = true; } - } else { // real error + } else { error = true; } + // if we end up here a real error happened + // FIXME: eval and inject return values if (error) { - if (this.term.inst_E( + if (this.term.actionError( { - pos: i, // position in parse string - character: String.fromCharCode(code), // wrong character - state: currentState, // in state - print: printed, // print buffer - dcs: dcs, // dcs buffer - osc: osc, // osc buffer + pos: i, // position in string + code: code, // actual character code + state: currentState, // current state + print: printed, // print buffer start index + dcs: dcs, // dcs buffer start index + osc: osc, // osc string buffer collect: collected, // collect buffer params: params // params buffer })) { @@ -285,22 +356,22 @@ export class AnsiParser { error = false; } break; - case 7: // csi_dispatch - this.term.inst_c(collected, params, String.fromCharCode(code)); + case ACTION.csi_dispatch: + this.term.actionCSI(collected, params, String.fromCharCode(code)); break; - case 8: // param + case ACTION.param: if (code === 0x3b) params.push(0); else params[params.length - 1] = params[params.length - 1] * 10 + code - 48; break; - case 9: // collect + case ACTION.collect: collected += String.fromCharCode(code); break; - case 10: // esc_dispatch - this.term.inst_e(collected, String.fromCharCode(code)); + case ACTION.esc_dispatch: + this.term.actionESC(collected, String.fromCharCode(code)); break; - case 11: // clear + case ACTION.clear: if (~printed) { - this.term.inst_p(s, printed, i); + this.term.actionPrint(s, printed, i); printed = -1; } osc = ''; @@ -308,34 +379,34 @@ export class AnsiParser { collected = ''; dcs = -1; break; - case 12: // dcs_hook - this.term.inst_H(collected, params, String.fromCharCode(code)); + case ACTION.dcs_hook: + this.term.actionDCSHook(collected, params, String.fromCharCode(code)); break; - case 13: // dcs_put - if (!(~dcs)) dcs = i; + case ACTION.dcs_put: + dcs = (~dcs) ? dcs : i; break; - case 14: // dcs_unhook - if (~dcs) this.term.inst_P(s.substring(dcs, i)); - this.term.inst_U(); - if (code === 0x1b) transition |= 1; + case ACTION.dcs_unhook: + if (~dcs) this.term.actionDCSPrint(s, dcs, i); + this.term.actionDCSUnhook(); + if (code === 0x1b) transition |= STATE.ESCAPE; osc = ''; params = [0]; collected = ''; dcs = -1; break; - case 4: // osc_start + case ACTION.osc_start: if (~printed) { - this.term.inst_p(s, printed, i); + this.term.actionPrint(s, printed, i); printed = -1; } osc = ''; break; - case 5: // osc_put + case ACTION.osc_put: osc += s.charAt(i); break; - case 6: // osc_end - if (osc && code !== 0x18 && code !== 0x1a) this.term.inst_o(osc); - if (code === 0x1b) transition |= 1; + case ACTION.osc_end: + if (osc && code !== 0x18 && code !== 0x1a) this.term.actionOSC(osc); + if (code === 0x1b) transition |= STATE.ESCAPE; osc = ''; params = [0]; collected = ''; @@ -346,10 +417,10 @@ export class AnsiParser { } // push leftover pushable buffers to terminal - if (!currentState && (printed + 1)) { - this.term.inst_p(s, printed, s.length); - } else if (currentState === 13 && (dcs + 1)) { - this.term.inst_P(s.substring(dcs)); + if (currentState === STATE.GROUND && ~printed) { + this.term.actionPrint(s, printed, s.length); + } else if (currentState === STATE.DCS_PASSTHROUGH && ~dcs) { + this.term.actionDCSPrint(s, dcs, s.length); } // save non pushable buffers @@ -363,31 +434,28 @@ export class AnsiParser { } - - - -import { IInputHandler, IInputHandlingTerminal } from './Types'; -import { CHARSETS, DEFAULT_CHARSET } from './Charsets'; -import { C0 } from './EscapeSequences'; - // glue code between AnsiParser and Terminal +// action methods are the places to call custom sequence handlers +// Q: Do we need custom handler support for all escape sequences types? +// Q: Merge class with InputHandler? export class ParserTerminal implements IParserTerminal { - private _parser: AnsiParser; + private _parser: EscapeSequenceParser; private _terminal: any; private _inputHandler: IInputHandler; - constructor(_terminal: any, _inputHandler: IInputHandler) { - this._parser = new AnsiParser(this); + constructor(_inputHandler: IInputHandler, _terminal: any) { + this._parser = new EscapeSequenceParser(this); this._terminal = _terminal; this._inputHandler = _inputHandler; } - write(data: string): void { + parse(data: string): void { const cursorStartX = this._terminal.buffer.x; const cursorStartY = this._terminal.buffer.y; if (this._terminal.debug) { this._terminal.log('data: ' + data); } + // apply leftover surrogate high from last write if (this._terminal.surrogate_high) { data = this._terminal.surrogate_high + data; @@ -401,8 +469,7 @@ export class ParserTerminal implements IParserTerminal { } } - inst_p(data: string, start: number, end: number): void { - // const l = data.length; + actionPrint(data: string, start: number, end: number): void { let ch; let code; let low; @@ -429,14 +496,19 @@ export class ParserTerminal implements IParserTerminal { } } - inst_o(data: string): void { - let params = data.split(';'); - switch (parseInt(params[0])) { + actionOSC(data: string): void { + let idx = data.indexOf(';'); + let identifier = parseInt(data.substring(0, idx)); + let content = data.substring(idx + 1); + + // TODO: call custom OSC handler here + + switch (identifier) { case 0: case 1: case 2: - if (params[1]) { - this._terminal.title = params[1]; + if (content) { + this._terminal.title = content; this._terminal.handleTitle(this._terminal.title); } break; @@ -487,11 +559,13 @@ export class ParserTerminal implements IParserTerminal { } } - inst_x(flag: string): void { + actionExecute(flag: string): void { + // Q: No XON/XOFF handling here - where is it done? + // Q: do we need the default fallback to addChar? switch (flag) { case C0.BEL: return this._inputHandler.bell(); - case C0.LF: return this._inputHandler.lineFeed(); - case C0.VT: return this._inputHandler.lineFeed(); + case C0.LF: + case C0.VT: case C0.FF: return this._inputHandler.lineFeed(); case C0.CR: return this._inputHandler.carriageReturn(); case C0.BS: return this._inputHandler.backspace(); @@ -504,7 +578,7 @@ export class ParserTerminal implements IParserTerminal { this._terminal.error('Unknown EXEC flag: %s.', flag); } - inst_c(collected: string, params: number[], flag: string): void { + actionCSI(collected: string, params: number[], flag: string): void { this._terminal.prefix = collected; switch (flag) { case '@': return this._inputHandler.insertChars(params); @@ -524,6 +598,7 @@ export class ParserTerminal implements IParserTerminal { case 'P': return this._inputHandler.deleteChars(params); case 'S': return this._inputHandler.scrollUp(params); case 'T': + // Q: Why this condition? if (params.length < 2 && !collected) { return this._inputHandler.scrollDown(params); } @@ -559,33 +634,26 @@ export class ParserTerminal implements IParserTerminal { this._terminal.error('Unknown CSI code: %s %s %s.', collected, params, flag); } - inst_e(collected: string, flag: string): void { - let cs; - + actionESC(collected: string, flag: string): void { switch (collected) { case '': switch (flag) { // case '6': // Back Index (DECBI), VT420 and up - not supported case '7': // Save Cursor (DECSC) - this._inputHandler.saveCursor(); - return; + return this._inputHandler.saveCursor(); case '8': // Restore Cursor (DECRC) - this._inputHandler.restoreCursor(); - return; + return this._inputHandler.restoreCursor(); // case '9': // Forward Index (DECFI), VT420 and up - not supported case 'D': // Index (IND is 0x84) - this._terminal.index(); - return; + return this._terminal.index(); case 'E': // Next Line (NEL is 0x85) this._terminal.buffer.x = 0; this._terminal.index(); return; case 'H': // ESC H Tab Set (HTS is 0x88) - (this._terminal).tabSet(); - return; + return (this._terminal).tabSet(); case 'M': // Reverse Index (RI is 0x8d) - this._terminal.reverseIndex(); - return; + return this._terminal.reverseIndex(); case 'N': // Single Shift Select of G2 Character Set ( SS2 is 0x8e) - Is this supported? case 'O': // Single Shift Select of G3 Character Set ( SS3 is 0x8f) return; @@ -620,20 +688,15 @@ export class ParserTerminal implements IParserTerminal { // case 'l': // Memory Lock (per HP terminals). Locks memory above the cursor. // case 'm': // Memory Unlock (per HP terminals). case 'n': // Invoke the G2 Character Set as GL (LS2). - this._terminal.setgLevel(2); - return; + return this._terminal.setgLevel(2); case 'o': // Invoke the G3 Character Set as GL (LS3). - this._terminal.setgLevel(3); - return; + return this._terminal.setgLevel(3); case '|': // Invoke the G3 Character Set as GR (LS3R). - this._terminal.setgLevel(3); - return; + return this._terminal.setgLevel(3); case '}': // Invoke the G2 Character Set as GR (LS2R). - this._terminal.setgLevel(2); - return; + return this._terminal.setgLevel(2); case '~': // Invoke the G1 Character Set as GR (LS1R). - this._terminal.setgLevel(1); - return; + return this._terminal.setgLevel(1); } // case ' ': // switch (flag) { @@ -664,55 +727,59 @@ export class ParserTerminal implements IParserTerminal { // load character sets case '(': // G0 (VT100) - cs = CHARSETS[flag]; - if (!cs) cs = DEFAULT_CHARSET; - this._terminal.setgCharset(0, cs); - return; + return this._terminal.setgCharset(0, CHARSETS[flag] || DEFAULT_CHARSET); case ')': // G1 (VT100) - cs = CHARSETS[flag]; - if (!cs) cs = DEFAULT_CHARSET; - this._terminal.setgCharset(1, cs); - return; + return this._terminal.setgCharset(1, CHARSETS[flag] || DEFAULT_CHARSET); case '*': // G2 (VT220) - cs = CHARSETS[flag]; - if (!cs) cs = DEFAULT_CHARSET; - this._terminal.setgCharset(2, cs); - return; + return this._terminal.setgCharset(2, CHARSETS[flag] || DEFAULT_CHARSET); case '+': // G3 (VT220) - cs = CHARSETS[flag]; - if (!cs) cs = DEFAULT_CHARSET; - this._terminal.setgCharset(3, cs); - return; + return this._terminal.setgCharset(3, CHARSETS[flag] || DEFAULT_CHARSET); case '-': // G1 (VT300) - cs = CHARSETS[flag]; - if (!cs) cs = DEFAULT_CHARSET; - this._terminal.setgCharset(1, cs); - return; + return this._terminal.setgCharset(1, CHARSETS[flag] || DEFAULT_CHARSET); case '.': // G2 (VT300) - if (!cs) cs = DEFAULT_CHARSET; - this._terminal.setgCharset(2, cs); - return; + return this._terminal.setgCharset(2, CHARSETS[flag] || DEFAULT_CHARSET); case '/': // G3 (VT300) - // not supported - how to deal with this? (original code is not reachable) + // not supported - how to deal with this? (Q: original code is not reachable?) return; default: this._terminal.error('Unknown ESC control: %s %s.', collected, flag); } } - inst_H(collected: string, params: number[], flag: string): void { + actionDCSHook(collected: string, params: number[], flag: string): void { + // TODO + custom hook + } + + actionDCSPrint(data: string): void { + // TODO + custom hook + } + + actionDCSUnhook(): void { + // TODO + custom hook + } + + actionError(): void { // TODO } - inst_P(dcs: string): void { + // custom handler interface + // Q: explicit like below or with an event like interface? + // tricky part: DCS handler need to be stateful over several + // actionDCSPrint invocations - own base interface/abstract class type? + + registerOSCHandler(): void { // TODO } - inst_U(): void { + unregisterOSCHandler(): void { // TODO } - inst_E(): void { + registerDCSHandler(): void { + // TODO + } + + unregisterDCSHandler(): void { // TODO } } diff --git a/src/Terminal.ts b/src/Terminal.ts index 44c90643..54548f9e 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -334,7 +334,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this._inputHandler = new InputHandler(this); // this._parser = new Parser(this._inputHandler, this); - this._newParser = new ParserTerminal(this, this._inputHandler); + this._newParser = new ParserTerminal(this._inputHandler, this); // Reuse renderer if the Terminal is being recreated via a reset call. this.renderer = this.renderer || null; this.selectionManager = this.selectionManager || null; @@ -1320,7 +1320,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // just sets the state back based on the correct return statement. // const state = this._parser.parse(data); - this._newParser.write(data); + this._newParser.parse(data); // this._parser.setState(state); this.updateRange(this.buffer.y);