Skip to content

Commit 63ddc69

Browse files
committed
--wip-- [skip ci]
1 parent f2f4959 commit 63ddc69

File tree

4 files changed

+662
-0
lines changed

4 files changed

+662
-0
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { TemplateParser } from './parser';
2+
import { TemplateTokeniser } from './tokeniser';
3+
4+
describe('TemplateParser', () => {
5+
it('should parse IO object from tokens and component', async () => {
6+
const component = { prop: 'val', handler: jest.fn() };
7+
const tokeniser = new TemplateTokeniser();
8+
const parser = new TemplateParser(tokeniser, component);
9+
10+
const io = parser.getIo();
11+
12+
tokeniser.feed('[input]=prop (output)=handler($event)');
13+
14+
await expect(io).resolves.toMatchObject({
15+
'[input]': 'val',
16+
'(output)': {
17+
handler: expect.any(Function),
18+
args: ['$event'],
19+
},
20+
});
21+
22+
((await io)['(output)'] as any).handler('mock-event');
23+
24+
expect(component.handler).toHaveBeenCalledWith('mock-event');
25+
});
26+
27+
describe('inputs', () => {
28+
it('should parse plain input', async () => {
29+
const component = { prop: 'val' };
30+
const tokeniser = new TemplateTokeniser();
31+
const parser = new TemplateParser(tokeniser, component);
32+
33+
const io = parser.getIo();
34+
35+
tokeniser.feed('input=prop ');
36+
37+
await expect(io).resolves.toMatchObject({
38+
input: 'val',
39+
});
40+
});
41+
42+
it('should parse prop input', async () => {
43+
const component = { prop: 'val' };
44+
const tokeniser = new TemplateTokeniser();
45+
const parser = new TemplateParser(tokeniser, component);
46+
47+
const io = parser.getIo();
48+
49+
tokeniser.feed('[input]=prop ');
50+
51+
await expect(io).resolves.toMatchObject({
52+
'[input]': 'val',
53+
});
54+
});
55+
56+
it('should NOT parse input with quotes', async () => {
57+
const component = { '"prop"': 'val' };
58+
const tokeniser = new TemplateTokeniser();
59+
const parser = new TemplateParser(tokeniser, component);
60+
61+
const io = parser.getIo();
62+
63+
tokeniser.feed('[input]="prop" ');
64+
65+
await expect(io).resolves.toMatchObject({
66+
'[input]': 'val',
67+
});
68+
});
69+
});
70+
71+
describe('outputs', () => {
72+
it('should parse output without args', async () => {
73+
const component = { handler: jest.fn() };
74+
const tokeniser = new TemplateTokeniser();
75+
const parser = new TemplateParser(tokeniser, component);
76+
77+
const io = parser.getIo();
78+
79+
tokeniser.feed('(output)=handler()');
80+
81+
await expect(io).resolves.toMatchObject({
82+
'(output)': {
83+
handler: expect.any(Function),
84+
args: [],
85+
},
86+
});
87+
88+
((await io)['(output)'] as any).handler();
89+
90+
expect(component.handler).toHaveBeenCalledWith();
91+
});
92+
93+
it('should parse output with one arg', async () => {
94+
const component = { handler: jest.fn() };
95+
const tokeniser = new TemplateTokeniser();
96+
const parser = new TemplateParser(tokeniser, component);
97+
98+
const io = parser.getIo();
99+
100+
tokeniser.feed('(output)=handler($event)');
101+
102+
await expect(io).resolves.toMatchObject({
103+
'(output)': {
104+
handler: expect.any(Function),
105+
args: ['$event'],
106+
},
107+
});
108+
109+
((await io)['(output)'] as any).handler('mock-event');
110+
111+
expect(component.handler).toHaveBeenCalledWith('mock-event');
112+
});
113+
114+
it('should parse output with multiple args', async () => {
115+
const component = { handler: jest.fn() };
116+
const tokeniser = new TemplateTokeniser();
117+
const parser = new TemplateParser(tokeniser, component);
118+
119+
const io = parser.getIo();
120+
121+
tokeniser.feed('(output)=handler($event, prop)');
122+
123+
await expect(io).resolves.toMatchObject({
124+
'(output)': {
125+
handler: expect.any(Function),
126+
args: ['$event', 'prop'],
127+
},
128+
});
129+
130+
((await io)['(output)'] as any).handler('mock-event', 'val');
131+
132+
expect(component.handler).toHaveBeenCalledWith('mock-event', 'val');
133+
});
134+
});
135+
});
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { OutputWithArgs } from '../io';
2+
import {
3+
TemplateToken,
4+
TemplateTokenAssignment,
5+
TemplateTokenComma,
6+
TemplateTokenInputPropBindingClose,
7+
TemplateTokenInputPropBindingOpen,
8+
TemplateTokeniser,
9+
TemplateTokenOutputBindingClose,
10+
TemplateTokenOutputBindingOpen,
11+
TemplateTokenString,
12+
TemplateTokenMap,
13+
} from './tokeniser';
14+
15+
enum TemplateParserState {
16+
Idle,
17+
InInput,
18+
InOutput,
19+
InValue,
20+
InArgs,
21+
}
22+
23+
export class TemplateParser {
24+
constructor(
25+
protected tokeniser: TemplateTokeniser,
26+
protected component: Record<string, unknown>,
27+
protected tokenMap = TemplateTokenMap,
28+
) {}
29+
30+
async getIo() {
31+
const io: Record<string, unknown> = {};
32+
33+
let state = TemplateParserState.Idle;
34+
let lastState = TemplateParserState.Idle;
35+
let ioBinding = '';
36+
37+
for await (const token of this.tokeniser) {
38+
if (token instanceof TemplateTokenInputPropBindingOpen) {
39+
if (state !== TemplateParserState.Idle) {
40+
throw new TemplateParserError('Unexpected input binding', token);
41+
}
42+
43+
state = TemplateParserState.InInput;
44+
ioBinding += this.tokenMap.InputPropBindingOpen;
45+
continue;
46+
} else if (token instanceof TemplateTokenInputPropBindingClose) {
47+
if (state !== TemplateParserState.InInput) {
48+
throw new TemplateParserError(
49+
'Unexpected input binding closing',
50+
token,
51+
);
52+
}
53+
54+
ioBinding += this.tokenMap.InputPropBindingClose;
55+
io[ioBinding] = undefined;
56+
continue;
57+
} else if (token instanceof TemplateTokenOutputBindingOpen) {
58+
if (
59+
state !== TemplateParserState.Idle &&
60+
state !== TemplateParserState.InOutput
61+
) {
62+
throw new TemplateParserError('Unexpected output binding', token);
63+
}
64+
65+
if (state === TemplateParserState.InOutput) {
66+
state = TemplateParserState.InArgs;
67+
} else {
68+
state = TemplateParserState.InOutput;
69+
ioBinding += this.tokenMap.OutputBindingOpen;
70+
}
71+
72+
continue;
73+
} else if (token instanceof TemplateTokenOutputBindingClose) {
74+
if (
75+
state !== TemplateParserState.InOutput &&
76+
state !== TemplateParserState.InArgs
77+
) {
78+
throw new TemplateParserError(
79+
'Unexpected output binding closing',
80+
token,
81+
);
82+
}
83+
84+
if (state === TemplateParserState.InArgs) {
85+
state = TemplateParserState.Idle;
86+
ioBinding = '';
87+
} else {
88+
ioBinding += this.tokenMap.OutputBindingClose;
89+
io[ioBinding] = undefined;
90+
}
91+
92+
continue;
93+
} else if (token instanceof TemplateTokenAssignment) {
94+
if (
95+
state !== TemplateParserState.InInput &&
96+
(state as any) !== TemplateParserState.InOutput
97+
) {
98+
throw new TemplateParserError('Unexpected assignment', token);
99+
}
100+
101+
lastState = state;
102+
state = TemplateParserState.InValue;
103+
continue;
104+
} else if (token instanceof TemplateTokenString) {
105+
if (
106+
state === TemplateParserState.InInput ||
107+
state === TemplateParserState.InOutput
108+
) {
109+
ioBinding += token.string;
110+
continue;
111+
} else if (state === TemplateParserState.InValue) {
112+
if (lastState === TemplateParserState.InInput) {
113+
delete io[ioBinding];
114+
Object.defineProperty(io, ioBinding, {
115+
enumerable: true,
116+
configurable: true,
117+
get: () => this.component[token.string],
118+
});
119+
state = lastState = TemplateParserState.Idle;
120+
ioBinding = '';
121+
continue;
122+
} else if (lastState === TemplateParserState.InOutput) {
123+
const handler = () => this.component[token.string] as any;
124+
io[ioBinding] = {
125+
get handler() {
126+
return handler();
127+
},
128+
args: [],
129+
} as OutputWithArgs;
130+
state = TemplateParserState.InOutput;
131+
lastState = TemplateParserState.Idle;
132+
continue;
133+
}
134+
135+
throw new TemplateParserError('Unexpected identifier', token);
136+
} else if (state === TemplateParserState.InArgs) {
137+
(io[ioBinding] as OutputWithArgs).args!.push(token.string);
138+
continue;
139+
} else if (state === TemplateParserState.Idle) {
140+
state = TemplateParserState.InInput;
141+
ioBinding = token.string;
142+
io[ioBinding] = undefined;
143+
continue;
144+
}
145+
146+
throw new TemplateParserError('Unexpected identifier', token);
147+
} else if (token instanceof TemplateTokenComma) {
148+
if (state !== TemplateParserState.InArgs) {
149+
throw new TemplateParserError('Unexpected comma', token);
150+
}
151+
continue;
152+
}
153+
154+
throw new TemplateParserError('Unexpected token', token);
155+
}
156+
157+
return io;
158+
}
159+
}
160+
161+
export class TemplateParserError extends Error {
162+
constructor(reason: string, token: TemplateToken) {
163+
super(
164+
`${reason} ${token.constructor.name}` +
165+
` at (${token.start}:${token.end})` +
166+
`\n${JSON.stringify(token, null, 2)}`,
167+
);
168+
}
169+
}

0 commit comments

Comments
 (0)