Skip to content

Commit

Permalink
--wip-- [skip ci]
Browse files Browse the repository at this point in the history
  • Loading branch information
gund committed Sep 25, 2022
1 parent dbc8257 commit 9db03f5
Show file tree
Hide file tree
Showing 5 changed files with 660 additions and 1 deletion.
7 changes: 6 additions & 1 deletion angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@
},
"test": {
"builder": "@angular-builders/jest:run",
"options": {}
"options": {},
"configurations": {
"watch": {
"watch": true
}
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
Expand Down
136 changes: 136 additions & 0 deletions projects/ng-dynamic-component/src/lib/template/parser.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { TemplateParser } from './parser';
import { TemplateTokeniser } from './tokeniser';

describe('TemplateParser', () => {
it('should parse IO object from tokens and component', async () => {
const component = { prop: 'val', handler: jest.fn() };
const tokeniser = new TemplateTokeniser();
const parser = new TemplateParser(tokeniser, component);

const io = parser.getIo();

tokeniser.feed('[input]=prop (output)=handler($event)');

await expect(io).resolves.toMatchObject({
'[input]': 'val',
'(output)': {
handler: expect.any(Function),
args: ['$event'],
},
});

((await io)['(output)'] as any).handler('mock-event');

expect(component.handler).toHaveBeenCalledWith('mock-event');
});

describe('inputs', () => {
it('should parse plain input', async () => {
const component = { prop: 'val' };
const tokeniser = new TemplateTokeniser();
const parser = new TemplateParser(tokeniser, component);

const io = parser.getIo();

tokeniser.feed('input=prop ');

await expect(io).resolves.toMatchObject({
input: 'val',
});
});

it('should parse prop input', async () => {
const component = { prop: 'val' };
const tokeniser = new TemplateTokeniser();
const parser = new TemplateParser(tokeniser, component);

const io = parser.getIo();

tokeniser.feed('[input]=prop ');

await expect(io).resolves.toMatchObject({
'[input]': 'val',
});
});

it('should NOT parse input with quotes', async () => {
const component = { '"prop"': 'val' };
const tokeniser = new TemplateTokeniser();
const parser = new TemplateParser(tokeniser, component);

const io = parser.getIo();

tokeniser.feed('[input]="prop" ');

await expect(io).resolves.toMatchObject({
'[input]': 'val',
});
});
});

describe('outputs', () => {
it('should parse output without args', async () => {
const component = { handler: jest.fn() };
const tokeniser = new TemplateTokeniser();
const parser = new TemplateParser(tokeniser, component);

const io = parser.getIo();

tokeniser.feed('(output)=handler()');

await expect(io).resolves.toMatchObject({
'(output)': {
handler: expect.any(Function),
args: [],
},
});

((await io)['(output)'] as any).handler();

expect(component.handler).toHaveBeenCalledWith();
});

it('should parse output with one arg', async () => {
const component = { handler: jest.fn() };
const tokeniser = new TemplateTokeniser();
const parser = new TemplateParser(tokeniser, component);

const io = parser.getIo();

tokeniser.feed('(output)=handler($event)');

await expect(io).resolves.toMatchObject({
'(output)': {
handler: expect.any(Function),
args: ['$event'],
},
});

((await io)['(output)'] as any).handler('mock-event');

expect(component.handler).toHaveBeenCalledWith('mock-event');
});

// TODO: Implement multiple args parsing
fit('should parse output with multiple args', async () => {
const component = { handler: jest.fn() };
const tokeniser = new TemplateTokeniser();
const parser = new TemplateParser(tokeniser, component);

const io = parser.getIo();

tokeniser.feed('(output)=handler($event, prop)');

await expect(io).resolves.toMatchObject({
'(output)': {
handler: expect.any(Function),
args: ['$event', 'prop'],
},
});

((await io)['(output)'] as any).handler('mock-event', 'val');

expect(component.handler).toHaveBeenCalledWith('mock-event', 'val');
});
});
});
168 changes: 168 additions & 0 deletions projects/ng-dynamic-component/src/lib/template/parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { OutputWithArgs } from '../io';
import {
TemplateToken,
TemplateTokenAssignment,
TemplateTokenComma,
TemplateTokenInputPropBindingClose,
TemplateTokenInputPropBindingOpen,
TemplateTokeniser,
TemplateTokenOutputBindingClose,
TemplateTokenOutputBindingOpen,
TemplateTokenString,
TemplateTokenMap,
} from './tokeniser';

enum TemplateParserState {
Idle,
InInput,
InOutput,
InValue,
InArgs,
}

export class TemplateParser {
constructor(
protected tokeniser: TemplateTokeniser,
protected component: Record<string, unknown>,
protected tokenMap = TemplateTokenMap,
) {}

async getIo() {
const io: Record<string, unknown> = {};

let state = TemplateParserState.Idle;
let lastState = TemplateParserState.Idle;
let ioBinding = '';

for await (const token of this.tokeniser) {
if (token instanceof TemplateTokenInputPropBindingOpen) {
if (state !== TemplateParserState.Idle) {
throw new TemplateParserError('Unexpected input binding', token);
}

state = TemplateParserState.InInput;
ioBinding += this.tokenMap.InputPropBindingOpen;
continue;
} else if (token instanceof TemplateTokenInputPropBindingClose) {
if (state !== TemplateParserState.InInput) {
throw new TemplateParserError(
'Unexpected input binding closing',
token,
);
}

ioBinding += this.tokenMap.InputPropBindingClose;
io[ioBinding] = undefined;
continue;
} else if (token instanceof TemplateTokenOutputBindingOpen) {
if (
state !== TemplateParserState.Idle &&
state !== TemplateParserState.InOutput
) {
throw new TemplateParserError('Unexpected output binding', token);
}

if (state === TemplateParserState.InOutput) {
state = TemplateParserState.InArgs;
} else {
state = TemplateParserState.InOutput;
ioBinding += this.tokenMap.OutputBindingOpen;
}

continue;
} else if (token instanceof TemplateTokenOutputBindingClose) {
if (
state !== TemplateParserState.InOutput &&
state !== TemplateParserState.InArgs
) {
throw new TemplateParserError(
'Unexpected output binding closing',
token,
);
}

if (state === TemplateParserState.InArgs) {
state = TemplateParserState.Idle;
ioBinding = '';
} else {
ioBinding += this.tokenMap.OutputBindingClose;
io[ioBinding] = undefined;
}

continue;
} else if (token instanceof TemplateTokenAssignment) {
if (
state !== TemplateParserState.InInput &&
(state as any) !== TemplateParserState.InOutput
) {
throw new TemplateParserError('Unexpected assignment', token);
}

lastState = state;
state = TemplateParserState.InValue;
continue;
} else if (token instanceof TemplateTokenString) {
if (
state === TemplateParserState.InInput ||
state === TemplateParserState.InOutput
) {
ioBinding += token.string;
continue;
} else if (state === TemplateParserState.InValue) {
if (lastState === TemplateParserState.InInput) {
delete io[ioBinding];
Object.defineProperty(io, ioBinding, {
enumerable: true,
configurable: true,
get: () => this.component[token.string],
});
state = lastState = TemplateParserState.Idle;
ioBinding = '';
continue;
} else if (lastState === TemplateParserState.InOutput) {
io[ioBinding] = {
handler: this.component[token.string] as any,
args: [],
} as OutputWithArgs;
// state = TemplateParserState.InOutput;
// lastState = TemplateParserState.Idle;
continue;
}

throw new TemplateParserError('Unexpected identifier', token);
} else if (state === TemplateParserState.InArgs) {
(io[ioBinding] as OutputWithArgs).args!.push(token.string);
continue;
} else if (state === TemplateParserState.Idle) {
state = TemplateParserState.InInput;
ioBinding = token.string;
io[ioBinding] = undefined;
continue;
}

throw new TemplateParserError('Unexpected identifier', token);
} else if (token instanceof TemplateTokenComma) {
if (state !== TemplateParserState.InArgs) {
throw new TemplateParserError('Unexpected comma', token);
}
continue;
}

throw new TemplateParserError('Unexpected token', token);
}

return io;
}
}

export class TemplateParserError extends Error {
constructor(reason: string, token: TemplateToken) {
super(
`${reason} at ${token.constructor.name}:${JSON.stringify(
token,
null,
2,
)}`,
);
}
}
Loading

0 comments on commit 9db03f5

Please sign in to comment.