Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add keyword arguments for Liquid shortcodes #3444

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 43 additions & 30 deletions src/Engines/Liquid.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
nex3 marked this conversation as resolved.
Show resolved Hide resolved
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) {
Expand All @@ -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;
},
Expand Down Expand Up @@ -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);

Expand Down
173 changes: 109 additions & 64 deletions test/TemplateRenderLiquidTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 () {
Expand Down Expand Up @@ -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("<p>{% include included %}</p>");
t.is(await fn(), "<p>This is an include.</p>");
Expand All @@ -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("<p>{% include ./included %}</p>");
Expand All @@ -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("<p>{% include ./dir/included %}</p>");
Expand All @@ -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("<p>{% include ../dir/included %}</p>");
Expand All @@ -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(`<p>{% include './included' %}</p>`);
Expand All @@ -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("<p>{% include included.liquid %}</p>");
t.is(await fn(), "<p>This is an include.</p>");
Expand All @@ -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("<p>{% include included.html %}</p>");
t.is(await fn(), "<p>This is an include.</p>");
Expand All @@ -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");
Expand Down Expand Up @@ -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";
});
Expand Down Expand Up @@ -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(`<p>{% include subfolder/included.liquid %}</p>`);
t.is(await fn(), "<p>This is an include.</p>");
});

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(`<p>{% include subfolder/included.html %}</p>`);
t.is(await fn(), "<p>This is an include.</p>");
});

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(`<p>{% include subfolder/included %}</p>`);
t.is(await fn(), "<p>This is an include.</p>");
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand All @@ -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");
Expand Down
Loading