diff --git a/src/Engines/Liquid.js b/src/Engines/Liquid.js index 4e94226e7..4a8ad184d 100644 --- a/src/Engines/Liquid.js +++ b/src/Engines/Liquid.js @@ -172,20 +172,59 @@ class Liquid extends TemplateEngine { let tokenizer = new Tokenizer(args); let parsedArgs = []; - let value = tokenizer.readValue(); + function readValue() { + let value = tokenizer.readHash() ?? tokenizer.readValue(); + // readHash() treats unmarked identifiers as hash keys with undefined + // values, but we want to parse them as positional arguments instead. + if (value?.kind !== TokenKind.Hash) return value; + let hash = /** @type {import("liquidjs/dist/tokens/hash-token.d.ts").HashToken} */ (value); + return hash.value === undefined ? hash.name : hash; + } + + let value = readValue(); while (value) { parsedArgs.push(value); tokenizer.skipBlank(); if (tokenizer.peek() === ",") { tokenizer.advance(); } - value = tokenizer.readValue(); + value = readValue(); } tokenizer.end(); return parsedArgs; } + *evalArguments(liquidEngine, tag, ctx) { + let argArray = []; + let namedArgs = {}; + if (tag.legacyArgs) { + let rawArgs = Liquid.parseArguments(this.argLexer, tag.legacyArgs); + for (let arg of rawArgs) { + let b = yield liquidEngine.evalValue(arg, ctx); + argArray.push(b); + } + } else if (tag.orderedArgs) { + for (let arg of tag.orderedArgs) { + if (arg.kind == 64) { + if (arg.value === undefined) { + namedArgs[arg.name.content] = true; + } else { + namedArgs[arg.name.content] = yield evalToken(arg.value, ctx); + } + } else { + let b = yield evalToken(arg, ctx); + argArray.push(b); + } + } + } + + if (Object.keys(namedArgs).length > 0) { + argArray.push(namedArgs); + } + return argArray; + } + addShortcode(shortcodeName, shortcodeFn) { let _t = this; this.addTag(shortcodeName, function (liquidEngine) { @@ -200,21 +239,7 @@ class Liquid extends TemplateEngine { } }, render: function* (ctx) { - let argArray = []; - - if (this.legacyArgs) { - let rawArgs = Liquid.parseArguments(_t.argLexer, this.legacyArgs); - for (let arg of rawArgs) { - let b = yield liquidEngine.evalValue(arg, ctx); - argArray.push(b); - } - } else if (this.orderedArgs) { - for (let arg of this.orderedArgs) { - let b = yield evalToken(arg, ctx); - argArray.push(b); - } - } - + let argArray = yield* _t.evalArguments(liquidEngine, this, ctx); let ret = yield shortcodeFn.call(Liquid.normalizeScope(ctx), ...argArray); return ret; }, @@ -249,19 +274,7 @@ class Liquid extends TemplateEngine { stream.start(); }, render: function* (ctx /*, emitter*/) { - let argArray = []; - if (this.legacyArgs) { - let rawArgs = Liquid.parseArguments(_t.argLexer, this.legacyArgs); - for (let arg of rawArgs) { - let b = yield liquidEngine.evalValue(arg, ctx); - argArray.push(b); - } - } else if (this.orderedArgs) { - for (let arg of this.orderedArgs) { - let b = yield evalToken(arg, ctx); - argArray.push(b); - } - } + let argArray = yield* _t.evalArguments(liquidEngine, this, ctx); const html = yield liquidEngine.renderer.renderTemplates(this.templates, ctx); diff --git a/test/TemplateRenderLiquidTest.js b/test/TemplateRenderLiquidTest.js index e9a43908b..06d6de487 100644 --- a/test/TemplateRenderLiquidTest.js +++ b/test/TemplateRenderLiquidTest.js @@ -7,12 +7,14 @@ import EleventyExtensionMap from "../src/EleventyExtensionMap.js"; import { getTemplateConfigInstance } from "./_testHelpers.js"; -async function getNewTemplateRender(name, inputDir, userConfig = {}) { +async function getNewTemplateRender(name, inputDir, configure = null) { let eleventyConfig = await getTemplateConfigInstance({ dir: { input: inputDir } - }, null, userConfig); + }, null, {}); + + if (configure) await configure(eleventyConfig); let tr = new TemplateRender(name, eleventyConfig); tr.extensionMap = new EleventyExtensionMap(eleventyConfig); @@ -21,6 +23,16 @@ async function getNewTemplateRender(name, inputDir, userConfig = {}) { return tr; } +function noDynamicPartials(eleventyConfig) { + eleventyConfig.setLiquidOptions({ + dynamicPartials: false, + }); +} + +function builtinParameterParsing(eleventyConfig) { + eleventyConfig.setLiquidParameterParsing("builtin"); +} + async function getPromise(resolveTo) { return new Promise(function (resolve) { setTimeout(function () { @@ -72,11 +84,7 @@ test("Liquid Render Include", async (t) => { let tr1 = await getNewTemplateRender("liquid", "./test/stubs/"); t.is(tr1.getEngineName(), "liquid"); - let tr2 = await getNewTemplateRender("liquid", "./test/stubs/", { - liquidOptions: { - dynamicPartials: false, - }, - }); + let tr2 = await getNewTemplateRender("liquid", "./test/stubs/", noDynamicPartials); let fn = await tr2.getCompiledTemplate("

{% include included %}

"); t.is(await fn(), "

This is an include.

"); @@ -86,11 +94,7 @@ test("Liquid Render Relative Include (dynamicPartials off)", async (t) => { let tr1 = await getNewTemplateRender("liquid", "./test/stubs/"); t.is(tr1.getEngineName(), "liquid"); - let tr2 = await getNewTemplateRender("liquid", "./test/stubs/", { - liquidOptions: { - dynamicPartials: false, - }, - }); + let tr2 = await getNewTemplateRender("liquid", "./test/stubs/", noDynamicPartials); // Important note: when inputPath is set to `liquid`, this *only* uses _includes relative paths in Liquid->compile let fn = await tr2.getCompiledTemplate("

{% include ./included %}

"); @@ -112,11 +116,7 @@ test("Liquid Render Relative (current dir) Include", async (t) => { let tr = await getNewTemplateRender( "./test/stubs/relative-liquid/does_not_exist_and_thats_ok.liquid", "./test/stubs/", - { - liquidOptions: { - dynamicPartials: false, - }, - } + noDynamicPartials, ); let fn = await tr.getCompiledTemplate("

{% include ./dir/included %}

"); @@ -127,11 +127,7 @@ test("Liquid Render Relative (parent dir) Include", async (t) => { let tr = await getNewTemplateRender( "./test/stubs/relative-liquid/dir/does_not_exist_and_thats_ok.liquid", "./test/stubs/", - { - liquidOptions: { - dynamicPartials: false, - }, - } + noDynamicPartials, ); let fn = await tr.getCompiledTemplate("

{% include ../dir/included %}

"); @@ -141,8 +137,7 @@ test("Liquid Render Relative (parent dir) Include", async (t) => { test("Liquid Render Relative (relative include should ignore _includes dir) Include", async (t) => { let tr = await getNewTemplateRender( "./test/stubs/does_not_exist_and_thats_ok.liquid", - "./test/stubs/", - {} + "./test/stubs/" ); let fn = await tr.getCompiledTemplate(`

{% include './included' %}

`); @@ -153,11 +148,7 @@ test("Liquid Render Include with Liquid Suffix", async (t) => { let tr1 = await getNewTemplateRender("liquid", "./test/stubs/"); t.is(tr1.getEngineName(), "liquid"); - let tr2 = await getNewTemplateRender("liquid", "./test/stubs/", { - liquidOptions: { - dynamicPartials: false, - }, - }); + let tr2 = await getNewTemplateRender("liquid", "./test/stubs/", noDynamicPartials); let fn = await tr2.getCompiledTemplate("

{% include included.liquid %}

"); t.is(await fn(), "

This is an include.

"); @@ -167,11 +158,7 @@ test("Liquid Render Include with HTML Suffix", async (t) => { let tr1 = await getNewTemplateRender("liquid", "./test/stubs/"); t.is(tr1.getEngineName(), "liquid"); - let tr2 = await getNewTemplateRender("liquid", "./test/stubs/", { - liquidOptions: { - dynamicPartials: false, - }, - }); + let tr2 = await getNewTemplateRender("liquid", "./test/stubs/", noDynamicPartials); let fn = await tr2.getCompiledTemplate("

{% include included.html %}

"); t.is(await fn(), "

This is an include.

"); @@ -181,11 +168,7 @@ test("Liquid Render Include with HTML Suffix and Data Pass in", async (t) => { let tr1 = await getNewTemplateRender("liquid", "./test/stubs/"); t.is(tr1.getEngineName(), "liquid"); - let tr2 = await getNewTemplateRender("liquid", "./test/stubs/", { - liquidOptions: { - dynamicPartials: false, - }, - }); + let tr2 = await getNewTemplateRender("liquid", "./test/stubs/", noDynamicPartials); let fn = await tr2.getCompiledTemplate("{% include included-data.html, myVariable: 'myValue' %}"); t.is((await fn()).trim(), "This is an include. myValue"); @@ -215,11 +198,7 @@ test("Liquid Async Filter", async (t) => { }); test("Issue 3206: Strict variables and custom filters in includes", async (t) => { - let tr = await getNewTemplateRender("liquid", "test/stubs", { - liquidOptions: { - strictVariables: true - } - }); + let tr = await getNewTemplateRender("liquid", "test/stubs", noDynamicPartials); tr.engine.addFilter("makeItFoo", function () { return "foo"; }); @@ -465,33 +444,21 @@ test("Liquid Async Paired Shortcode", async (t) => { }); test("Liquid Render Include Subfolder", async (t) => { - let tr = await getNewTemplateRender("liquid", "./test/stubs/", { - liquidOptions: { - dynamicPartials: false, - }, - }); + let tr = await getNewTemplateRender("liquid", "./test/stubs/", noDynamicPartials); let fn = await tr.getCompiledTemplate(`

{% include subfolder/included.liquid %}

`); t.is(await fn(), "

This is an include.

"); }); test("Liquid Render Include Subfolder HTML", async (t) => { - let tr = await getNewTemplateRender("liquid", "./test/stubs/", { - liquidOptions: { - dynamicPartials: false, - }, - }); + let tr = await getNewTemplateRender("liquid", "./test/stubs/", noDynamicPartials); let fn = await tr.getCompiledTemplate(`

{% include subfolder/included.html %}

`); t.is(await fn(), "

This is an include.

"); }); test("Liquid Render Include Subfolder No file extension", async (t) => { - let tr = await getNewTemplateRender("liquid", "./test/stubs/", { - liquidOptions: { - dynamicPartials: false, - }, - }); + let tr = await getNewTemplateRender("liquid", "./test/stubs/", noDynamicPartials); let fn = await tr.getCompiledTemplate(`

{% include subfolder/included %}

`); t.is(await fn(), "

This is an include.

"); @@ -537,11 +504,7 @@ test("Liquid Render Include Subfolder Double quotes No file extension", async (t /* End tests related to dynamicPartials */ test("Liquid Options Overrides", async (t) => { - let tr = await getNewTemplateRender("liquid", "./test/stubs/", { - liquidOptions: { - dynamicPartials: false, - }, - }); + let tr = await getNewTemplateRender("liquid", "./test/stubs/", noDynamicPartials); let options = tr.engine.getLiquidOptions(); t.is(options.dynamicPartials, false); @@ -658,6 +621,38 @@ test("Liquid Nested Paired Shortcode", async (t) => { ); }); +test("Liquid Paired Kwargs Shortcode with Tag Inside", async (t) => { + let tr = await getNewTemplateRender("liquid", "./test/stubs/", builtinParameterParsing); + tr.engine.addPairedShortcode("postfixWithZach", function (content, kwargs) { + var { str } = kwargs ?? {}; + return str + content + "Zach"; + }); + + t.is( + await tr._testRender( + "{% postfixWithZach str: name %}Content{% if tester %}If{% endif %}{% endpostfixWithZach %}", + { name: "test", tester: true } + ), + "testContentIfZach" + ); +}); + +test("Liquid Nested Paired Kwargs Shortcode", async (t) => { + let tr = await getNewTemplateRender("liquid", "./test/stubs/", builtinParameterParsing); + tr.engine.addPairedShortcode("postfixWithZach", function (content, kwargs) { + var { str } = kwargs ?? {}; + return str + content + "Zach"; + }); + + t.is( + await tr._testRender( + "{% postfixWithZach str: name %}Content{% postfixWithZach str: name2 %}Content{% endpostfixWithZach %}{% endpostfixWithZach %}", + { name: "test", name2: "test2" } + ), + "testContenttest2ContentZachZach" + ); +}); + test("Liquid Shortcode Multiple Args", async (t) => { let tr = await getNewTemplateRender("liquid", "./test/stubs/"); tr.engine.addShortcode("postfixWithZach", function (str, str2) { @@ -673,6 +668,56 @@ test("Liquid Shortcode Multiple Args", async (t) => { ); }); +test("Liquid Shortcode Keyword Arg", async (t) => { + let tr = await getNewTemplateRender("liquid", "./test/stubs/", builtinParameterParsing); + tr.engine.addShortcode("postfixWithZach", function (str, kwargs) { + let { append } = kwargs ?? {}; + return str + "Zach" + append; + }); + + t.is( + await tr._testRender("{% postfixWithZach name append: other %}", { + name: "test", + other: "howdy", + }), + "testZachhowdy" + ); +}); + +test("Liquid Shortcode Multiple Keyword Args", async (t) => { + let tr = await getNewTemplateRender("liquid", "./test/stubs/", builtinParameterParsing); + tr.engine.addShortcode("postfixWithZach", function (str, kwargs) { + let { prepend, append } = kwargs ?? {}; + return prepend + str + "Zach" + append; + }); + + t.is( + await tr._testRender( + "{% postfixWithZach name prepend: 'string' append: other %}", + { + name: "test", + other: "howdy", + } + ), + "stringtestZachhowdy" + ); +}); + +test("Liquid Shortcode Only Keyword Args", async (t) => { + let tr = await getNewTemplateRender("liquid", "./test/stubs/", builtinParameterParsing); + tr.engine.addShortcode("postfixWithZach", function (kwargs) { + let { prepend, append } = kwargs ?? {}; + return prepend + "Zach" + append; + }); + + t.is( + await tr._testRender("{% postfixWithZach prepend: 'string' append: name %}", { + name: "test", + }), + "stringZachtest" + ); +}); + test("Liquid Include Scope Leak", async (t) => { let tr1 = await getNewTemplateRender("liquid", "./test/stubs/"); t.is(tr1.getEngineName(), "liquid");