From 46763dbae762bf5d14d2e4d3aebf335d6a672ace Mon Sep 17 00:00:00 2001 From: Harminder virk Date: Thu, 26 Mar 2020 21:06:37 +0530 Subject: [PATCH] refactor: finalize compiler API --- src/Compiler/index.ts | 102 ++++++------------ test/compiler.spec.ts | 236 ++++++++++++++++++------------------------ 2 files changed, 130 insertions(+), 208 deletions(-) diff --git a/src/Compiler/index.ts b/src/Compiler/index.ts index c7311d5..d187e31 100644 --- a/src/Compiler/index.ts +++ b/src/Compiler/index.ts @@ -15,12 +15,8 @@ import { CacheManager } from '../CacheManager' import { LoaderContract, TagsContract, LoaderTemplate, CompilerContract } from '../Contracts' /** - * Compiler compiles the template to a function, which can be invoked at a later - * stage using the [[Context]]. [edge-parser](https://npm.im/edge-parser) is - * used under the hood to parse the templates. - * - * Also, the `layouts` are handled natively by the compiler. Before starting - * the parsing process, it will recursively merge the layout sections. + * Compiler is to used to compile templates using the `edge-parser`. Along with that + * it natively merges the contents of a layout with a parent template. */ export class Compiler implements CompilerContract { public cacheManager = new CacheManager(this.cache) @@ -49,13 +45,14 @@ export class Compiler implements CompilerContract { extended .forEach((node) => { /** - * Ignore new lines, layout tag and empty raw nodes inside the parent + * Ignore new lines, comments, layout tag and empty raw nodes inside the parent * template */ if ( - lexerUtils.isTag(node, 'layout') || - node.type === 'newline' || - (node.type === 'raw' && !node.value.trim()) + lexerUtils.isTag(node, 'layout') + || node.type === 'newline' + || (node.type === 'raw' && !node.value.trim()) + || node.type === 'comment' ) { return } @@ -124,8 +121,8 @@ export class Compiler implements CompilerContract { * are checked for layouts and if layouts are used, their sections will be * merged together. */ - private templateContentToTokens (content: string, parser: Parser): Token[] { - let templateTokens = parser.tokenize(content) + private templateContentToTokens (content: string, parser: Parser, absPath: string): Token[] { + let templateTokens = parser.tokenize(content, absPath) const firstToken = templateTokens[0] /** @@ -134,50 +131,36 @@ export class Compiler implements CompilerContract { */ if (lexerUtils.isTag(firstToken, 'layout')) { const layoutName = firstToken.properties.jsArg.replace(/'|"/g, '') - templateTokens = this.mergeSections(this.tokenize(layoutName), templateTokens) + templateTokens = this.mergeSections(this.tokenize(layoutName, parser), templateTokens) } return templateTokens } /** - * Converts the template content to an [array of lexer tokens]. The method is + * Converts the template content to an array of lexer tokens. The method is * same as the `parser.tokenize`, but it also handles layouts natively. * * ``` * compiler.tokenize('') * ``` */ - public tokenize (templatePath: string): Token[] { + public tokenize (templatePath: string, parser?: Parser): Token[] { const absPath = this.loader.makePath(templatePath) - const { template } = this.loader.resolve(absPath, false) - - const parser = new Parser(this.tags, { filename: absPath }) - return this.templateContentToTokens(template, parser) + const { template } = this.loader.resolve(absPath) + return this.templateContentToTokens(template, parser || new Parser(this.tags), absPath) } /** - * Compiles the template contents to a function string, which can be invoked - * later. - * - * When `inline` is set to true, the compiled output **will not have it's own scope** and - * neither an attempt to load the presenter is made. The `inline` is mainly used for partials. + * Compiles the template contents to string. The output is same as the `edge-parser`, + * it's just that the compiler uses the loader to load the templates and also + * handles layouts. * * ```js - * compiler.compile('welcome', false) - * // output - * - * { - * template: `function (template, ctx) { - * let out = '' - * out += '' - * return out - * })(template, ctx)`, - * Presenter: class Presenter | undefined - * } + * compiler.compile('welcome') * ``` */ - public compile (templatePath: string, inline: boolean): LoaderTemplate { + public compile (templatePath: string, localVariables?: string[]): LoaderTemplate { const absPath = this.loader.makePath(templatePath) /** @@ -189,46 +172,23 @@ export class Compiler implements CompilerContract { return cachedResponse } - /** - * Do not return presenter in inline mode - */ - const loadPresenter = !inline - - /** - * Inline templates are not wrapped inside a function - * call. They share the parent template scope - */ - const wrapAsFunction = !inline + const parser = new Parser(this.tags) + const buffer = new EdgeBuffer(absPath) /** - * Get a new instance of the parser. + * Define local variables on the parser. This is helpful when trying to compile + * a partail and we want to share the local state of the parent template + * with it */ - const parser = new Parser(this.tags, { filename: absPath }) - - /** - * Resolve the template and Presenter using the given loader. We always - * load the presenter but don't return it when `loadPresenter = false`. - */ - const { template, Presenter } = this.loader.resolve(absPath, true) - - /** - * Convert template to AST. The AST will have the layout actions merged (if layout) - * is used. - */ - const templateTokens = this.templateContentToTokens(template, parser) + if (localVariables) { + localVariables.forEach((localVariable) => parser.stack.defineVariable(localVariable)) + } - /** - * Finally process the ast - */ - const buffer = new EdgeBuffer(absPath, wrapAsFunction) + const templateTokens = this.tokenize(absPath, parser) templateTokens.forEach((token) => parser.processToken(token, buffer)) + const template = buffer.flush() - const payload = { - template: buffer.flush(), - Presenter, - } - - this.cacheManager.set(absPath, payload) - return loadPresenter ? payload : { template: payload.template } + this.cacheManager.set(absPath, { template }) + return { template } } } diff --git a/test/compiler.spec.ts b/test/compiler.spec.ts index a0afebe..eb74f39 100644 --- a/test/compiler.spec.ts +++ b/test/compiler.spec.ts @@ -15,13 +15,12 @@ import { TagTypes, MustacheTypes } from 'edge-lexer' import { Loader } from '../src/Loader' import { Context } from '../src/Context' +import { setTag } from '../src/Tags/Set' import { Compiler } from '../src/Compiler' import { layoutTag } from '../src/Tags/Layout' import { sectionTag } from '../src/Tags/Section' -import { setTag } from '../src/Tags/Set' -import './assert-extend' -const tags = {} +import './assert-extend' const fs = new Filesystem(join(__dirname, 'views')) @@ -36,21 +35,19 @@ test.group('Compiler | Cache', (group) => { const loader = new Loader() loader.mount('default', fs.basePath) - const compiler = new Compiler(loader, tags) - const { template } = compiler.compile('foo', false) - - assert.stringEqual(template, dedent`return (function (template, ctx) { - let out = ""; - ctx.$lineNumber = 1; - ctx.$filename = "${join(fs.basePath, 'foo.edge')}"; - try { - out += "Hello "; - out += \`\${ctx.escape(ctx.resolve('username'))}\`; - } catch (error) { - ctx.reThrow(error); - } - return out; - })(template, ctx)`) + const compiler = new Compiler(loader, {}) + const { template } = compiler.compile('foo') + + assert.stringEqual(template, dedent`let out = ""; + let $lineNumber = 1; + let $filename = "${join(fs.basePath, 'foo.edge')}"; + try { + out += "Hello "; + out += \`\${ctx.escape(state.username)}\`; + } catch (error) { + ctx.reThrow(error, $filename, $lineNumber); + } + return out;`) }) test('save template to cache when caching is turned on', async (assert) => { @@ -59,25 +56,10 @@ test.group('Compiler | Cache', (group) => { const loader = new Loader() loader.mount('default', fs.basePath) - const compiler = new Compiler(loader, tags, true) - assert.equal( - compiler.compile('foo', false), - compiler.cacheManager.get(join(fs.basePath, 'foo.edge')), - ) - }) - - test('save template and presenter both to the cache when caching is turned on', async (assert) => { - await fs.add('foo.edge', 'Hello {{ username }}') - await fs.add('foo.presenter.js', 'module.exports = class Foo {}') - - const loader = new Loader() - loader.mount('default', fs.basePath) - - const compiler = new Compiler(loader, tags, true) - compiler.compile('foo', false) + const compiler = new Compiler(loader, {}, true) assert.equal( - compiler.cacheManager.get(join(fs.basePath, 'foo.edge'))!.Presenter!['name'], - 'Foo', + compiler.compile('foo').template, + compiler.cacheManager.get(join(fs.basePath, 'foo.edge'))!.template, ) }) @@ -87,22 +69,10 @@ test.group('Compiler | Cache', (group) => { const loader = new Loader() loader.mount('default', fs.basePath) - const compiler = new Compiler(loader, tags, false) - compiler.compile('foo', false) + const compiler = new Compiler(loader, {}, false) + compiler.compile('foo') assert.isUndefined(compiler.cacheManager.get(join(fs.basePath, 'foo.edge'))) }) - - test('do not load presenter for inline templates', async (assert) => { - await fs.add('foo.edge', 'Hello {{ username }}') - await fs.add('foo.presenter.js', '') - - const loader = new Loader() - loader.mount('default', fs.basePath) - - const compiler = new Compiler(loader, tags) - assert.isUndefined(compiler.compile('foo', true).Presenter) - assert.isDefined(compiler.compile('foo', false).Presenter) - }) }) test.group('Compiler | Tokenize', (group) => { @@ -112,15 +82,15 @@ test.group('Compiler | Tokenize', (group) => { test('during tokenize, merge @section tags of a given layout', async (assert) => { await fs.add('master.edge', dedent` - Master page - @!section('content') + Master page + @!section('content') `) await fs.add('index.edge', dedent` - @layout('master') - @section('content') - Hello world - @endsection + @layout('master') + @section('content') + Hello world + @endsection `) const loader = new Loader() @@ -130,7 +100,6 @@ test.group('Compiler | Tokenize', (group) => { section: sectionTag, layout: layoutTag, }) - assert.deepEqual(compiler.tokenize('index.edge'), [ { type: 'raw' as const, @@ -165,16 +134,16 @@ test.group('Compiler | Tokenize', (group) => { test('during tokenize, merge @set tags of a given layout', async (assert) => { await fs.add('master.edge', dedent` - Master page - @!section('content') + Master page + @!section('content') `) await fs.add('index.edge', dedent` - @layout('master') - @set('username', 'virk') - @section('content') - Hello world - @endsection + @layout('master') + @set('username', 'virk') + @section('content') + Hello world + @endsection `) const loader = new Loader() @@ -232,13 +201,13 @@ test.group('Compiler | Tokenize', (group) => { assert.plan(4) await fs.add('master.edge', dedent` - Master page - @!section('content') + Master page + @!section('content') `) await fs.add('index.edge', dedent` - @layout('master') - Hello world + @layout('master') + Hello world `) const loader = new Loader() @@ -264,24 +233,24 @@ test.group('Compiler | Tokenize', (group) => { test('during tokenize, merge @section tags of a nested layouts', async (assert) => { await fs.add('super-master.edge', dedent` - Master page - @!section('header') - @!section('content') + Master page + @!section('header') + @!section('content') `) await fs.add('master.edge', dedent` - @layout('super-master') - @section('header') - This is header - @endsection - @!section('content') + @layout('super-master') + @section('header') + This is header + @endsection + @!section('content') `) await fs.add('index.edge', dedent` - @layout('master') - @section('content') - This is content - @endsection + @layout('master') + @section('content') + This is content + @endsection `) const loader = new Loader() @@ -343,15 +312,15 @@ test.group('Compiler | Tokenize', (group) => { test('layout tokens must point to its own filename', async (assert) => { await fs.add('master.edge', dedent` - {{ username }} - @!section('content') + {{ username }} + @!section('content') `) await fs.add('index.edge', dedent` - @layout('master') - @section('content') - Hello world - @endsection + @layout('master') + @section('content') + Hello world + @endsection `) const loader = new Loader() @@ -405,15 +374,15 @@ test.group('Compiler | Compile', (group) => { test('compile template with layouts', async (assert) => { await fs.add('master.edge', dedent` - {{ username }} - @!section('content') + {{ username }} + @!section('content') `) await fs.add('index.edge', dedent` - @layout('master') - @section('content') - {{ content }} - @endsection + @layout('master') + @section('content') + {{ content }} + @endsection `) const loader = new Loader() @@ -424,24 +393,21 @@ test.group('Compiler | Compile', (group) => { layout: layoutTag, }) - assert.stringEqual(compiler.compile('index.edge', false).template, dedent` - return (function (template, ctx) { - let out = ""; - ctx.$lineNumber = 1; - ctx.$filename = "${join(fs.basePath, 'index.edge')}"; - try { - ctx.$filename = "${join(fs.basePath, 'master.edge')}"; - out += \`\${ctx.escape(ctx.resolve('username'))}\`; - out += "\\n"; - out += " "; - ctx.$filename = "${join(fs.basePath, 'index.edge')}"; - ctx.$lineNumber = 3; - out += \`\${ctx.escape(ctx.resolve('content'))}\`; - } catch (error) { - ctx.reThrow(error); - } - return out; - })(template, ctx) + assert.stringEqual(compiler.compile('index.edge').template, dedent`let out = ""; + let $lineNumber = 1; + let $filename = "${join(fs.basePath, 'index.edge')}"; + try { + $filename = "${join(fs.basePath, 'master.edge')}"; + out += \`\${ctx.escape(state.username)}\`; + out += "\\n"; + out += " "; + $filename = "${join(fs.basePath, 'index.edge')}"; + $lineNumber = 3; + out += \`\${ctx.escape(state.content)}\`; + } catch (error) { + ctx.reThrow(error, $filename, $lineNumber); + } + return out; `) }) @@ -449,15 +415,15 @@ test.group('Compiler | Compile', (group) => { assert.plan(3) await fs.add('master.edge', dedent` - {{ user name }} - @!section('content') + {{ user name }} + @!section('content') `) await fs.add('index.edge', dedent` - @layout('master') - @section('content') - {{ content }} - @endsection + @layout('master') + @section('content') + {{ content }} + @endsection `) const loader = new Loader() @@ -469,7 +435,7 @@ test.group('Compiler | Compile', (group) => { }) try { - compiler.compile('index.edge', false) + compiler.compile('index.edge') } catch (error) { assert.equal(error.filename, join(fs.basePath, 'master.edge')) assert.equal(error.line, 1) @@ -481,15 +447,15 @@ test.group('Compiler | Compile', (group) => { assert.plan(3) await fs.add('master.edge', dedent` - {{ username }} - @!section('content') + {{ username }} + @!section('content') `) await fs.add('index.edge', dedent` - @layout('master') - @section('content') - {{ con tent }} - @endsection + @layout('master') + @section('content') + {{ con tent }} + @endsection `) const loader = new Loader() @@ -501,7 +467,7 @@ test.group('Compiler | Compile', (group) => { }) try { - compiler.compile('index.edge', false) + compiler.compile('index.edge') } catch (error) { assert.equal(error.filename, join(fs.basePath, 'index.edge')) assert.equal(error.line, 3) @@ -514,14 +480,14 @@ test.group('Compiler | Compile', (group) => { await fs.add('master.edge', dedent` {{ getUserName() }} - @!section('content') + @!section('content') `) await fs.add('index.edge', dedent` - @layout('master') - @section('content') - {{ content }} - @endsection + @layout('master') + @section('content') + {{ content }} + @endsection `) const loader = new Loader() @@ -533,12 +499,9 @@ test.group('Compiler | Compile', (group) => { }) try { - new Function('template', 'ctx', compiler.compile('index.edge', false).template)( - {}, - new Context({ state: {}, sharedState: {} }), - ) + new Function('state', 'ctx', compiler.compile('index.edge').template)({}, new Context()) } catch (error) { - assert.equal(error.message, 'ctx.resolve(...) is not a function') + assert.equal(error.message, 'getUserName is not a function') assert.equal(error.filename, join(fs.basePath, 'master.edge')) assert.equal(error.line, 1) assert.equal(error.col, 0) @@ -569,10 +532,9 @@ test.group('Compiler | Compile', (group) => { }) try { - const fn = new Function('template', 'ctx', compiler.compile('index.edge', false).template) - fn({}, new Context({ state: {}, sharedState: {} })) + new Function('state', 'ctx', compiler.compile('index.edge').template)({}, new Context()) } catch (error) { - assert.equal(error.message, 'ctx.resolve(...) is not a function') + assert.equal(error.message, 'getContent is not a function') assert.equal(error.filename, join(fs.basePath, 'index.edge')) assert.equal(error.line, 3) assert.equal(error.col, 0)