-
-
Notifications
You must be signed in to change notification settings - Fork 63
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
660 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
136 changes: 136 additions & 0 deletions
136
projects/ng-dynamic-component/src/lib/template/parser.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
168
projects/ng-dynamic-component/src/lib/template/parser.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
)}`, | ||
); | ||
} | ||
} |
Oops, something went wrong.