summaryrefslogtreecommitdiff
path: root/node_modules/@11ty/eleventy/src/Engines/Liquid.js
diff options
context:
space:
mode:
Diffstat (limited to 'node_modules/@11ty/eleventy/src/Engines/Liquid.js')
-rw-r--r--node_modules/@11ty/eleventy/src/Engines/Liquid.js331
1 files changed, 331 insertions, 0 deletions
diff --git a/node_modules/@11ty/eleventy/src/Engines/Liquid.js b/node_modules/@11ty/eleventy/src/Engines/Liquid.js
new file mode 100644
index 0000000..44fdab4
--- /dev/null
+++ b/node_modules/@11ty/eleventy/src/Engines/Liquid.js
@@ -0,0 +1,331 @@
+import moo from "moo";
+import { Tokenizer, TokenKind, evalToken, Liquid as LiquidJs } from "liquidjs";
+import { TemplatePath } from "@11ty/eleventy-utils";
+// import debugUtil from "debug";
+
+import TemplateEngine from "./TemplateEngine.js";
+import { augmentObject } from "./Util/ContextAugmenter.js";
+
+// const debug = debugUtil("Eleventy:Liquid");
+
+export default class Liquid extends TemplateEngine {
+ static argumentLexerOptions = {
+ number: /[0-9]+\.*[0-9]*/,
+ doubleQuoteString: /"(?:\\["\\]|[^\n"\\])*"/,
+ singleQuoteString: /'(?:\\['\\]|[^\n'\\])*'/,
+ keyword: /[a-zA-Z0-9.\-_]+/,
+ "ignore:whitespace": /[, \t]+/, // includes comma separator
+ };
+
+ constructor(name, eleventyConfig) {
+ super(name, eleventyConfig);
+
+ this.liquidOptions = this.config.liquidOptions || {};
+
+ this.setLibrary(this.config.libraryOverrides.liquid);
+
+ this.argLexer = moo.compile(Liquid.argumentLexerOptions);
+ }
+
+ get cacheable() {
+ return true;
+ }
+
+ setLibrary(override) {
+ // warning, the include syntax supported here does not exactly match what Jekyll uses.
+ this.liquidLib = override || new LiquidJs(this.getLiquidOptions());
+ this.setEngineLib(this.liquidLib, Boolean(this.config.libraryOverrides.liquid));
+
+ this.addFilters(this.config.liquidFilters);
+
+ // TODO these all go to the same place (addTag), add warnings for overwrites
+ this.addCustomTags(this.config.liquidTags);
+ this.addAllShortcodes(this.config.liquidShortcodes);
+ this.addAllPairedShortcodes(this.config.liquidPairedShortcodes);
+ }
+
+ getLiquidOptions() {
+ let defaults = {
+ root: [this.dirs.includes, this.dirs.input], // supplemented in compile with inputPath below
+ extname: ".liquid",
+ strictFilters: true,
+ // TODO?
+ // cache: true,
+ };
+
+ let options = Object.assign(defaults, this.liquidOptions || {});
+ // debug("Liquid constructor options: %o", options);
+
+ return options;
+ }
+
+ static wrapFilter(name, fn) {
+ /**
+ * @this {object}
+ */
+ return function (...args) {
+ // Set this.eleventy and this.page
+ if (typeof this.context?.get === "function") {
+ augmentObject(this, {
+ source: this.context,
+ getter: (key, context) => context.get([key]),
+
+ lazy: this.context.strictVariables,
+ });
+ }
+
+ // We *don’t* wrap this in an EleventyFilterError because Liquid has a better error message with line/column information in the template
+ return fn.call(this, ...args);
+ };
+ }
+
+ // Shortcodes
+ static normalizeScope(context) {
+ let obj = {};
+ if (context) {
+ obj.ctx = context; // Full context available on `ctx`
+
+ // Set this.eleventy and this.page
+ augmentObject(obj, {
+ source: context,
+ getter: (key, context) => context.get([key]),
+ lazy: context.strictVariables,
+ });
+ }
+
+ return obj;
+ }
+
+ addCustomTags(tags) {
+ for (let name in tags) {
+ this.addTag(name, tags[name]);
+ }
+ }
+
+ addFilters(filters) {
+ for (let name in filters) {
+ this.addFilter(name, filters[name]);
+ }
+ }
+
+ addFilter(name, filter) {
+ this.liquidLib.registerFilter(name, Liquid.wrapFilter(name, filter));
+ }
+
+ addTag(name, tagFn) {
+ let tagObj;
+ if (typeof tagFn === "function") {
+ tagObj = tagFn(this.liquidLib);
+ } else {
+ throw new Error(
+ "Liquid.addTag expects a callback function to be passed in: addTag(name, function(liquidEngine) { return { parse: …, render: … } })",
+ );
+ }
+ this.liquidLib.registerTag(name, tagObj);
+ }
+
+ addAllShortcodes(shortcodes) {
+ for (let name in shortcodes) {
+ this.addShortcode(name, shortcodes[name]);
+ }
+ }
+
+ addAllPairedShortcodes(shortcodes) {
+ for (let name in shortcodes) {
+ this.addPairedShortcode(name, shortcodes[name]);
+ }
+ }
+
+ static parseArguments(lexer, str) {
+ let argArray = [];
+
+ if (!lexer) {
+ lexer = moo.compile(Liquid.argumentLexerOptions);
+ }
+
+ if (typeof str === "string") {
+ lexer.reset(str);
+
+ let arg = lexer.next();
+ while (arg) {
+ /*{
+ type: 'doubleQuoteString',
+ value: '"test 2"',
+ text: '"test 2"',
+ toString: [Function: tokenToString],
+ offset: 0,
+ lineBreaks: 0,
+ line: 1,
+ col: 1 }*/
+ if (arg.type.indexOf("ignore:") === -1) {
+ // Push the promise into an array instead of awaiting it here.
+ // This forces the promises to run in order with the correct scope value for each arg.
+ // Otherwise they run out of order and can lead to undefined values for arguments in layout template shortcodes.
+ // console.log( arg.value, scope, engine );
+ argArray.push(arg.value);
+ }
+ arg = lexer.next();
+ }
+ }
+
+ return argArray;
+ }
+
+ static parseArgumentsBuiltin(args) {
+ let tokenizer = new Tokenizer(args);
+ let parsedArgs = [];
+
+ let value = tokenizer.readValue();
+ while (value) {
+ parsedArgs.push(value);
+ tokenizer.skipBlank();
+ if (tokenizer.peek() === ",") {
+ tokenizer.advance();
+ }
+ value = tokenizer.readValue();
+ }
+ tokenizer.end();
+
+ return parsedArgs;
+ }
+
+ addShortcode(shortcodeName, shortcodeFn) {
+ let _t = this;
+ this.addTag(shortcodeName, function (liquidEngine) {
+ return {
+ parse(tagToken) {
+ this.name = tagToken.name;
+ if (_t.config.liquidParameterParsing === "builtin") {
+ this.orderedArgs = Liquid.parseArgumentsBuiltin(tagToken.args);
+ // 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 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 ret = yield shortcodeFn.call(Liquid.normalizeScope(ctx), ...argArray);
+ return ret;
+ },
+ };
+ });
+ }
+
+ addPairedShortcode(shortcodeName, shortcodeFn) {
+ let _t = this;
+ this.addTag(shortcodeName, function (liquidEngine) {
+ return {
+ parse(tagToken, remainTokens) {
+ this.name = tagToken.name;
+
+ if (_t.config.liquidParameterParsing === "builtin") {
+ this.orderedArgs = Liquid.parseArgumentsBuiltin(tagToken.args);
+ // 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
+ .parseStream(remainTokens)
+ .on("template", (tpl) => this.templates.push(tpl))
+ .on("tag:end" + shortcodeName, () => stream.stop())
+ .on("end", () => {
+ throw new Error(`tag ${tagToken.raw} not closed`);
+ });
+
+ 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);
+ }
+ }
+
+ const html = yield liquidEngine.renderer.renderTemplates(this.templates, ctx);
+
+ let ret = yield shortcodeFn.call(Liquid.normalizeScope(ctx), html, ...argArray);
+
+ return ret;
+ },
+ };
+ });
+ }
+
+ parseForSymbols(str) {
+ if (!str) {
+ return [];
+ }
+
+ let tokenizer = new Tokenizer(str);
+ /** @type {Array} */
+ let tokens = tokenizer.readTopLevelTokens();
+ let symbols = tokens
+ .filter((token) => token.kind === TokenKind.Output)
+ .map((token) => {
+ // manually remove filters 😅
+ return token.content.split("|").map((entry) => entry.trim())[0];
+ });
+ return symbols;
+ }
+
+ // Don’t return a boolean if permalink is a function (see TemplateContent->renderPermalink)
+ /** @returns {boolean|undefined} */
+ permalinkNeedsCompilation(str) {
+ if (typeof str === "string") {
+ return this.needsCompilation(str);
+ }
+ }
+
+ needsCompilation(str) {
+ let options = this.liquidLib.options;
+
+ return (
+ str.indexOf(options.tagDelimiterLeft) !== -1 ||
+ str.indexOf(options.outputDelimiterLeft) !== -1
+ );
+ }
+
+ async compile(str, inputPath) {
+ let engine = this.liquidLib;
+ let tmplReady = engine.parse(str, inputPath);
+
+ // Required for relative includes
+ let options = {};
+ if (!inputPath || inputPath === "liquid" || inputPath === "md") {
+ // do nothing
+ } else {
+ options.root = [TemplatePath.getDirFromFilePath(inputPath)];
+ }
+
+ return async function (data) {
+ let tmpl = await tmplReady;
+
+ return engine.render(tmpl, data, options);
+ };
+ }
+}