From f8a4ef030a152f8d8b654ca183ca41554a378348 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Sun, 15 Sep 2024 02:54:33 -0700 Subject: [PATCH] Add keyword arguments for Liquid shortcodes This is an adaptation of #1733 to use the built-in Liquid argument parser that was added in #2679. Co-Authored-By: Dylan Awalt-Conley --- src/Engines/Liquid.js | 71 +++++++++++++++------------ test/TemplateRenderLiquidTest.js | 82 ++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 30 deletions(-) diff --git a/src/Engines/Liquid.js b/src/Engines/Liquid.js index 4e94226e7..f9091e7e3 100644 --- a/src/Engines/Liquid.js +++ b/src/Engines/Liquid.js @@ -172,20 +172,57 @@ 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. + return value?.kind === 64 && value.value === undefined ? value.name : value; + } + + let value = readValue(); while (value) { parsedArgs.push(value); tokenizer.skipBlank(); if (tokenizer.peek() === ",") { tokenizer.advance(); } - value = tokenizer.readValue(); + value = readValue(); } tokenizer.end(); return parsedArgs; } + static *evalArguments(tag, ctx) { + let argArray = []; + let namedArgs = {}; + if (tag.legacyArgs) { + let rawArgs = Liquid.parseArguments(_t.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 +237,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* Liquid.evalArguments(this, ctx); let ret = yield shortcodeFn.call(Liquid.normalizeScope(ctx), ...argArray); return ret; }, @@ -249,19 +272,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* Liquid.evalArguments(this, ctx); const html = yield liquidEngine.renderer.renderTemplates(this.templates, ctx); diff --git a/test/TemplateRenderLiquidTest.js b/test/TemplateRenderLiquidTest.js index e9a43908b..a723d9dbb 100644 --- a/test/TemplateRenderLiquidTest.js +++ b/test/TemplateRenderLiquidTest.js @@ -658,6 +658,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/"); + 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/"); + 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 +705,56 @@ test("Liquid Shortcode Multiple Args", async (t) => { ); }); +test("Liquid Shortcode Keyword Arg", async (t) => { + let tr = await getNewTemplateRender("liquid", "./test/stubs/"); + 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/"); + 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/"); + 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");