Skip to content

Commit 29b4733

Browse files
committed
--wip-- [skip ci]
1 parent dbc8257 commit 29b4733

File tree

4 files changed

+643
-0
lines changed

4 files changed

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

0 commit comments

Comments
 (0)