diff --git a/src/compile-string.ts b/src/compile-string.ts index 218118e..e26249e 100644 --- a/src/compile-string.ts +++ b/src/compile-string.ts @@ -23,7 +23,7 @@ export function compileToString( let include = (__eta_t, __eta_d) => this.render(__eta_t, {...${config.varName}, ...(__eta_d ?? {})}, options); let includeAsync = (__eta_t, __eta_d) => this.renderAsync(__eta_t, {...${config.varName}, ...(__eta_d ?? {})}, options); -let __eta = {res: "", e: this.config.escapeFunction, f: this.config.filterFunction${ +let __eta = {res: "", e: this.config.escapeFunction, f: this.config.filterFunction, blocks: {}${ config.debug ? ', line: 1, templateStr: "' + str.replace(/\\|"/g, "\\$&").replace(/\r\n|\n|\r/g, "\\n") + @@ -41,6 +41,8 @@ function layout(path, data) { function ${config.outputFunctionName}(s){__eta.res+=s;} function capture(fn){const s=__eta.res;__eta.res='';try{fn();return __eta.res}finally{__eta.res=s;}} async function captureAsync(fn){const s=__eta.res;__eta.res='';try{await fn();return __eta.res}finally{__eta.res=s;}} +function block(name,fn){if(__eta.layout){if(fn){__eta.blocks[name]=capture(fn);}return '';}const b=${config.varName}.__blocks||{};if(name in b){return b[name];}return fn?capture(fn):'';} +async function blockAsync(name,fn){if(__eta.layout){if(fn){__eta.blocks[name]=await captureAsync(fn);}return '';}const b=${config.varName}.__blocks||{};if(name in b){return b[name];}return fn?await captureAsync(fn):'';} ${compileBody.call(this, buffer)} if (__eta.layout) { @@ -48,7 +50,7 @@ if (__eta.layout) { isAsync ? "await includeAsync" : "include" } (__eta.layout, {...${ config.varName - }, body: __eta.res, ...__eta.layoutData}); + }, body: __eta.res, ...__eta.layoutData, __blocks: __eta.blocks}); } ${config.useWith ? "}" : ""}${ config.debug diff --git a/test/compile-string.spec.ts b/test/compile-string.spec.ts index 8c0d6a7..0e303b9 100644 --- a/test/compile-string.spec.ts +++ b/test/compile-string.spec.ts @@ -17,7 +17,7 @@ describe("Compile to String test", () => { let include = (__eta_t, __eta_d) => this.render(__eta_t, {...it, ...(__eta_d ?? {})}, options); let includeAsync = (__eta_t, __eta_d) => this.renderAsync(__eta_t, {...it, ...(__eta_d ?? {})}, options); -let __eta = {res: "", e: this.config.escapeFunction, f: this.config.filterFunction}; +let __eta = {res: "", e: this.config.escapeFunction, f: this.config.filterFunction, blocks: {}}; function layout(path, data) { __eta.layout = path; @@ -27,12 +27,14 @@ function layout(path, data) { function output(s){__eta.res+=s;} function capture(fn){const s=__eta.res;__eta.res='';try{fn();return __eta.res}finally{__eta.res=s;}} async function captureAsync(fn){const s=__eta.res;__eta.res='';try{await fn();return __eta.res}finally{__eta.res=s;}} +function block(name,fn){if(__eta.layout){if(fn){__eta.blocks[name]=capture(fn);}return '';}const b=it.__blocks||{};if(name in b){return b[name];}return fn?capture(fn):'';} +async function blockAsync(name,fn){if(__eta.layout){if(fn){__eta.blocks[name]=await captureAsync(fn);}return '';}const b=it.__blocks||{};if(name in b){return b[name];}return fn?await captureAsync(fn):'';} __eta.res+='hi '; __eta.res+=__eta.e(it.name); if (__eta.layout) { - __eta.res = include (__eta.layout, {...it, body: __eta.res, ...__eta.layoutData}); + __eta.res = include (__eta.layout, {...it, body: __eta.res, ...__eta.layoutData, __blocks: __eta.blocks}); } return __eta.res; @@ -45,7 +47,7 @@ return __eta.res; let include = (__eta_t, __eta_d) => this.render(__eta_t, {...it, ...(__eta_d ?? {})}, options); let includeAsync = (__eta_t, __eta_d) => this.renderAsync(__eta_t, {...it, ...(__eta_d ?? {})}, options); -let __eta = {res: "", e: this.config.escapeFunction, f: this.config.filterFunction}; +let __eta = {res: "", e: this.config.escapeFunction, f: this.config.filterFunction, blocks: {}}; function layout(path, data) { __eta.layout = path; @@ -55,12 +57,14 @@ function layout(path, data) { function output(s){__eta.res+=s;} function capture(fn){const s=__eta.res;__eta.res='';try{fn();return __eta.res}finally{__eta.res=s;}} async function captureAsync(fn){const s=__eta.res;__eta.res='';try{await fn();return __eta.res}finally{__eta.res=s;}} +function block(name,fn){if(__eta.layout){if(fn){__eta.blocks[name]=capture(fn);}return '';}const b=it.__blocks||{};if(name in b){return b[name];}return fn?capture(fn):'';} +async function blockAsync(name,fn){if(__eta.layout){if(fn){__eta.blocks[name]=await captureAsync(fn);}return '';}const b=it.__blocks||{};if(name in b){return b[name];}return fn?await captureAsync(fn):'';} __eta.res+='hi '; __eta.res+=it.name; if (__eta.layout) { - __eta.res = include (__eta.layout, {...it, body: __eta.res, ...__eta.layoutData}); + __eta.res = include (__eta.layout, {...it, body: __eta.res, ...__eta.layoutData, __blocks: __eta.blocks}); } return __eta.res; @@ -75,7 +79,7 @@ return __eta.res; let include = (__eta_t, __eta_d) => this.render(__eta_t, {...it, ...(__eta_d ?? {})}, options); let includeAsync = (__eta_t, __eta_d) => this.renderAsync(__eta_t, {...it, ...(__eta_d ?? {})}, options); -let __eta = {res: "", e: this.config.escapeFunction, f: this.config.filterFunction}; +let __eta = {res: "", e: this.config.escapeFunction, f: this.config.filterFunction, blocks: {}}; function layout(path, data) { __eta.layout = path; @@ -85,13 +89,15 @@ function layout(path, data) { function output(s){__eta.res+=s;} function capture(fn){const s=__eta.res;__eta.res='';try{fn();return __eta.res}finally{__eta.res=s;}} async function captureAsync(fn){const s=__eta.res;__eta.res='';try{await fn();return __eta.res}finally{__eta.res=s;}} +function block(name,fn){if(__eta.layout){if(fn){__eta.blocks[name]=capture(fn);}return '';}const b=it.__blocks||{};if(name in b){return b[name];}return fn?capture(fn):'';} +async function blockAsync(name,fn){if(__eta.layout){if(fn){__eta.blocks[name]=await captureAsync(fn);}return '';}const b=it.__blocks||{};if(name in b){return b[name];}return fn?await captureAsync(fn):'';} __eta.res+='hi'; __eta.res+=__eta.e(it.firstname); __eta.res+=__eta.e(it.lastname); if (__eta.layout) { - __eta.res = include (__eta.layout, {...it, body: __eta.res, ...__eta.layoutData}); + __eta.res = include (__eta.layout, {...it, body: __eta.res, ...__eta.layoutData, __blocks: __eta.blocks}); } return __eta.res; @@ -104,7 +110,7 @@ return __eta.res; let include = (__eta_t, __eta_d) => this.render(__eta_t, {...it, ...(__eta_d ?? {})}, options); let includeAsync = (__eta_t, __eta_d) => this.renderAsync(__eta_t, {...it, ...(__eta_d ?? {})}, options); -let __eta = {res: "", e: this.config.escapeFunction, f: this.config.filterFunction}; +let __eta = {res: "", e: this.config.escapeFunction, f: this.config.filterFunction, blocks: {}}; function layout(path, data) { __eta.layout = path; @@ -114,6 +120,8 @@ function layout(path, data) { function output(s){__eta.res+=s;} function capture(fn){const s=__eta.res;__eta.res='';try{fn();return __eta.res}finally{__eta.res=s;}} async function captureAsync(fn){const s=__eta.res;__eta.res='';try{await fn();return __eta.res}finally{__eta.res=s;}} +function block(name,fn){if(__eta.layout){if(fn){__eta.blocks[name]=capture(fn);}return '';}const b=it.__blocks||{};if(name in b){return b[name];}return fn?capture(fn):'';} +async function blockAsync(name,fn){if(__eta.layout){if(fn){__eta.blocks[name]=await captureAsync(fn);}return '';}const b=it.__blocks||{};if(name in b){return b[name];}return fn?await captureAsync(fn):'';} __eta.res+='Hi\\n'; console.log("Hope you like Eta!") @@ -140,7 +148,7 @@ __eta.res+='\\nThis is a partial: '; __eta.res+=include("mypartial"); if (__eta.layout) { - __eta.res = include (__eta.layout, {...it, body: __eta.res, ...__eta.layoutData}); + __eta.res = include (__eta.layout, {...it, body: __eta.res, ...__eta.layoutData, __blocks: __eta.blocks}); } return __eta.res; diff --git a/test/render.spec.ts b/test/render.spec.ts index ac47d90..b043ec1 100644 --- a/test/render.spec.ts +++ b/test/render.spec.ts @@ -364,15 +364,106 @@ describe("forEach loops in included templates", () => { }); }); -describe("capture", () => { +describe("block", () => { const eta = new Eta(); - it("captures inline HTML and passes it to an included template", () => { + it("child overrides layout default", () => { + eta.loadTemplate( + "@block-layout", + `
<%~ block('styles', () => { %><% }) %><%~ it.body %>`, + ); + + const res = eta.renderString( + `<% layout("@block-layout") %>Body
<% block('styles', () => { %><% }) %>`, + {}, + ); + + expect(res).toEqual('Body
'); + }); + + it("block with default content when no override", () => { + eta.loadTemplate( + "@block-default", + `<%~ block('styles', () => { %><% }) %><%~ it.body %>`, + ); + + const res = eta.renderString( + `<% layout("@block-default") %>Body
`, + {}, + ); + + expect(res).toEqual('Body
'); + }); + + it("empty block with no default and no override", () => { + eta.loadTemplate("@block-empty", `[<%~ block('sidebar') %>]<%~ it.body %>`); + + const res = eta.renderString(`<% layout("@block-empty") %>main`, {}); + + expect(res).toEqual("[]main"); + }); + + it("multiple blocks in one layout", () => { + eta.loadTemplate( + "@block-multi", + `<%~ block('styles') %><%~ it.body %>`, + ); + + const res = eta.renderString( + `<% layout("@block-multi") %>body<% block('styles', () => { %>S<% }) %><% block('scripts', () => { %>J<% }) %>`, + {}, + ); + + expect(res).toEqual("Sbody"); + }); + + it("block accessing outer scope data", () => { + eta.loadTemplate("@block-data", `<%~ block('title') %>|<%~ it.body %>`); + + const res = eta.renderString( + `<% layout("@block-data") %>body<% block('title', () => { %><%= it.name %><% }) %>`, + { name: "Ada" }, + ); + + expect(res).toEqual("Ada|body"); + }); + + it("block doesn't leak into it.body", () => { + eta.loadTemplate( + "@block-noleak", + `[<%~ block('extra') %>][<%~ it.body %>]`, + ); + + const res = eta.renderString( + `<% layout("@block-noleak") %>body<% block('extra', () => { %>X<% }) %>`, + {}, + ); + + expect(res).toEqual("[X][body]"); + }); + + it("blockAsync with async content", async () => { eta.loadTemplate( - "@wrapper", - `async content
<% })}) %>`, @@ -463,10 +551,7 @@ describe("capture", () => { }); it("captureAsync works with async content", async () => { - eta.loadTemplate( - "@async-slot", - `<%= await it.getData() %>
<% })}) %>`,