diff --git a/src/Engines/Liquid.js b/src/Engines/Liquid.js index 3610baf0a..5e0befb84 100644 --- a/src/Engines/Liquid.js +++ b/src/Engines/Liquid.js @@ -1,5 +1,5 @@ import moo from "moo"; -import liquidLib from "liquidjs"; +import liquidLib, { Tokenizer, evalToken } from "liquidjs"; import { TemplatePath } from "@11ty/eleventy-utils"; // import debugUtil from "debug"; @@ -170,15 +170,31 @@ class Liquid extends TemplateEngine { return { parse(tagToken) { this.name = tagToken.name; - this.args = tagToken.args; + if (_t.config.liquidParameterParsing === "builtin") { + let tokenizer = new Tokenizer(tagToken.args); + this.orderedArgs = []; + while (!tokenizer.end()) { + this.orderedArgs.push(tokenizer.readValue()); + } + // note that Liquid does have a Hash class for name-based argument parsing but offers no easy to support both modes in one class + } else { + this.legacyArgs = tagToken.args; + } }, render: function* (ctx) { - let rawArgs = Liquid.parseArguments(_t.argLexer, this.args); let argArray = []; - let contextScope = ctx.getAll(); - for (let arg of rawArgs) { - let b = yield liquidEngine.evalValue(arg, contextScope); - argArray.push(b); + + 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 ret = yield shortcodeFn.call(Liquid.normalizeScope(ctx), ...argArray); @@ -194,7 +210,18 @@ class Liquid extends TemplateEngine { return { parse(tagToken, remainTokens) { this.name = tagToken.name; - this.args = tagToken.args; + + if (_t.config.liquidParameterParsing === "builtin") { + let tokenizer = new Tokenizer(tagToken.args); + this.orderedArgs = []; + while (!tokenizer.end()) { + this.orderedArgs.push(tokenizer.readValue()); + } + // note that Liquid does have a Hash class for name-based argument parsing but offers no easy to support both modes in one class + } else { + this.legacyArgs = tagToken.args; + } + this.templates = []; var stream = liquidEngine.parser @@ -208,18 +235,24 @@ class Liquid extends TemplateEngine { stream.start(); }, render: function* (ctx /*, emitter*/) { - let rawArgs = Liquid.parseArguments(_t.argLexer, this.args); let argArray = []; - let contextScope = ctx.getAll(); - for (let arg of rawArgs) { - let b = yield liquidEngine.evalValue(arg, contextScope); - argArray.push(b); + 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); + } } const html = yield liquidEngine.renderer.renderTemplates(this.templates, ctx); let ret = yield shortcodeFn.call(Liquid.normalizeScope(ctx), html, ...argArray); - // emitter.write(ret); + return ret; }, }; diff --git a/src/UserConfig.js b/src/UserConfig.js index b2cd59b23..1c98933fc 100644 --- a/src/UserConfig.js +++ b/src/UserConfig.js @@ -31,6 +31,7 @@ class UserConfigError extends EleventyBaseError {} class UserConfig { #pluginExecution = false; #quietModeLocked = false; + #dataDeepMergeModified = false; constructor() { this._uniqueId = Math.random(); @@ -71,6 +72,7 @@ class UserConfig { filters: {}, shortcodes: {}, pairedShortcodes: {}, + parameterParsing: "legacy", // or builtin }; this.nunjucks = { @@ -748,6 +750,15 @@ class UserConfig { this.liquid.options = options; } + setLiquidParameterParsing(behavior) { + if (behavior !== "legacy" && behavior !== "builtin") { + throw new Error( + `Invalid argument passed to \`setLiquidParameterParsing\`. Expected one of "legacy" or "builtin".`, + ); + } + this.liquid.parameterParsing = behavior; + } + setNunjucksEnvironmentOptions(options) { this.nunjucks.environmentOptions = options; } @@ -765,12 +776,13 @@ class UserConfig { } setDataDeepMerge(deepMerge) { - this._dataDeepMergeModified = true; + this.#dataDeepMergeModified = true; this.dataDeepMerge = !!deepMerge; } + // Used by the Upgrade Helper Plugin isDataDeepMergeModified() { - return this._dataDeepMergeModified; + return this.#dataDeepMergeModified; } addWatchTarget(additionalWatchTargets) { @@ -902,6 +914,7 @@ class UserConfig { this.serverPassthroughCopyBehavior = behavior; } + // Url transforms change page.url and work good with server side content-negotiation (e.g. i18n plugin) addUrlTransform(callback) { this.urlTransforms.push(callback); } @@ -1085,6 +1098,7 @@ class UserConfig { liquidFilters: this.liquid.filters, liquidShortcodes: this.liquid.shortcodes, liquidPairedShortcodes: this.liquid.pairedShortcodes, + liquidParameterParsing: this.liquid.parameterParsing, // Nunjucks nunjucksEnvironmentOptions: this.nunjucks.environmentOptions, diff --git a/test/TemplateRenderLiquidTest.js b/test/TemplateRenderLiquidTest.js index 7e847e1ef..a6ab4ef65 100644 --- a/test/TemplateRenderLiquidTest.js +++ b/test/TemplateRenderLiquidTest.js @@ -1,6 +1,7 @@ import test from "ava"; import { Liquid, Drop } from "liquidjs"; +import Eleventy from "../src/Eleventy.js"; import TemplateRender from "../src/TemplateRender.js"; import EleventyExtensionMap from "../src/EleventyExtensionMap.js"; @@ -1040,3 +1041,41 @@ test("Liquid Parse for Symbols", async (t) => { t.deepEqual(engine.parseForSymbols("{{ collections.mine | test }}>"), ["collections.mine"]); }); + +test("Eleventy shortcode uses new built-in Liquid argument parsing behavior", async (t) => { + let elev = new Eleventy("./test/stubs-virtual/", undefined, { + config: eleventyConfig => { + eleventyConfig.setLiquidParameterParsing("builtin"); + eleventyConfig.addShortcode("test", (...args) => { + return JSON.stringify(args); + }) + eleventyConfig.addTemplate("index.liquid", `{% test abc def %}`, { + abc: 123, + def: 456 + }); + } + }); + elev.disableLogger(); + + let [result] = await elev.toJSON(); + t.deepEqual(result.content, "[123,456]"); +}); + +test("Eleventy paired shortcode uses new built-in Liquid argument parsing behavior", async (t) => { + let elev = new Eleventy("./test/stubs-virtual/", undefined, { + config: eleventyConfig => { + eleventyConfig.setLiquidParameterParsing("builtin"); + eleventyConfig.addPairedShortcode("test", (...args) => { + return JSON.stringify(args); + }) + eleventyConfig.addTemplate("index.liquid", `{% test abc def %}hi{% endtest %}`, { + abc: 123, + def: 456 + }); + } + }); + elev.disableLogger(); + + let [result] = await elev.toJSON(); + t.deepEqual(result.content, `["hi",123,456]`); +});