diff options
| author | Shipwreckt <me@shipwreckt.co.uk> | 2025-10-31 20:02:14 +0000 |
|---|---|---|
| committer | Shipwreckt <me@shipwreckt.co.uk> | 2025-10-31 20:02:14 +0000 |
| commit | 7a52ddeba2a68388b544f529d2d92104420f77b0 (patch) | |
| tree | 15ddd47457a2cb4a96060747437d36474e4f6b4e /node_modules/@11ty/eleventy/src/Engines/Liquid.js | |
| parent | 53d6ae2b5568437afa5e4995580a3fb679b7b91b (diff) | |
Changed from static to 11ty!
Diffstat (limited to 'node_modules/@11ty/eleventy/src/Engines/Liquid.js')
| -rw-r--r-- | node_modules/@11ty/eleventy/src/Engines/Liquid.js | 331 |
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); + }; + } +} |
