diff options
Diffstat (limited to 'node_modules/@11ty/eleventy/src/Engines')
10 files changed, 2025 insertions, 0 deletions
diff --git a/node_modules/@11ty/eleventy/src/Engines/Custom.js b/node_modules/@11ty/eleventy/src/Engines/Custom.js new file mode 100644 index 0000000..17a0da1 --- /dev/null +++ b/node_modules/@11ty/eleventy/src/Engines/Custom.js @@ -0,0 +1,339 @@ +import TemplateEngine from "./TemplateEngine.js"; +import getJavaScriptData from "../Util/GetJavaScriptData.js"; + +export default class CustomEngine extends TemplateEngine { + constructor(name, eleventyConfig) { + super(name, eleventyConfig); + + this.entry = this.getExtensionMapEntry(); + this.needsInit = "init" in this.entry && typeof this.entry.init === "function"; + + this.setDefaultEngine(undefined); + } + + getExtensionMapEntry() { + if ("extensionMap" in this.config) { + let name = this.name.toLowerCase(); + // Iterates over only the user config `addExtension` entries + for (let entry of this.config.extensionMap) { + let entryKey = (entry.aliasKey || entry.key || "").toLowerCase(); + if (entryKey === name) { + return entry; + } + } + } + + throw Error( + `Could not find a custom extension for ${this.name}. Did you add it to your config file?`, + ); + } + + setDefaultEngine(defaultEngine) { + this._defaultEngine = defaultEngine; + } + + get cacheable() { + // Enable cacheability for this template + if (this.entry?.compileOptions?.cache !== undefined) { + return this.entry.compileOptions.cache; + } else if (this.needsToReadFileContents()) { + return true; + } else if (this._defaultEngine?.cacheable !== undefined) { + return this._defaultEngine.cacheable; + } + + return super.cacheable; + } + + async getInstanceFromInputPath(inputPath) { + if ( + "getInstanceFromInputPath" in this.entry && + typeof this.entry.getInstanceFromInputPath === "function" + ) { + // returns Promise + return this.entry.getInstanceFromInputPath(inputPath); + } + + // aliased upstream type + if ( + this._defaultEngine && + "getInstanceFromInputPath" in this._defaultEngine && + typeof this._defaultEngine.getInstanceFromInputPath === "function" + ) { + // returns Promise + return this._defaultEngine.getInstanceFromInputPath(inputPath); + } + + return false; + } + + /** + * Whether to use the module loader directly + * + * @override + */ + useJavaScriptImport() { + if ("useJavaScriptImport" in this.entry) { + return this.entry.useJavaScriptImport; + } + + if ( + this._defaultEngine && + "useJavaScriptImport" in this._defaultEngine && + typeof this._defaultEngine.useJavaScriptImport === "function" + ) { + return this._defaultEngine.useJavaScriptImport(); + } + + return false; + } + + /** + * @override + */ + needsToReadFileContents() { + if ("read" in this.entry) { + return this.entry.read; + } + + // Handle aliases to `11ty.js` templates, avoid reading files in the alias, see #2279 + // Here, we are short circuiting fallback to defaultRenderer, does not account for compile + // functions that call defaultRenderer explicitly + if (this._defaultEngine && "needsToReadFileContents" in this._defaultEngine) { + return this._defaultEngine.needsToReadFileContents(); + } + + return true; + } + + // If we init from multiple places, wait for the first init to finish before continuing on. + async _runningInit() { + if (this.needsInit) { + if (!this._initing) { + this._initBench = this.benchmarks.aggregate.get(`Engine (${this.name}) Init`); + this._initBench.before(); + this._initing = this.entry.init.bind({ + config: this.config, + bench: this.benchmarks.aggregate, + })(); + } + await this._initing; + this.needsInit = false; + + if (this._initBench) { + this._initBench.after(); + this._initBench = undefined; + } + } + } + + async getExtraDataFromFile(inputPath) { + if (this.entry.getData === false) { + return; + } + + if (!("getData" in this.entry)) { + // Handle aliases to `11ty.js` templates, use upstream default engine data fetch, see #2279 + if (this._defaultEngine && "getExtraDataFromFile" in this._defaultEngine) { + return this._defaultEngine.getExtraDataFromFile(inputPath); + } + + return; + } + + await this._runningInit(); + + if (typeof this.entry.getData === "function") { + let dataBench = this.benchmarks.aggregate.get( + `Engine (${this.name}) Get Data From File (Function)`, + ); + dataBench.before(); + let data = this.entry.getData(inputPath); + dataBench.after(); + return data; + } + + let keys = new Set(); + if (this.entry.getData === true) { + keys.add("data"); + } else if (Array.isArray(this.entry.getData)) { + for (let key of this.entry.getData) { + keys.add(key); + } + } + + let dataBench = this.benchmarks.aggregate.get(`Engine (${this.name}) Get Data From File`); + dataBench.before(); + + let inst = await this.getInstanceFromInputPath(inputPath); + + if (inst === false) { + dataBench.after(); + + return Promise.reject( + new Error( + `\`getInstanceFromInputPath\` callback missing from '${this.name}' template engine plugin. It is required when \`getData\` is in use. You can set \`getData: false\` to opt-out of this.`, + ), + ); + } + + // override keys set at the plugin level in the individual template + if (inst.eleventyDataKey) { + keys = new Set(inst.eleventyDataKey); + } + + let mixins; + if (this.config) { + // Object.assign usage: see TemplateRenderCustomTest.js: `JavaScript functions should not be mutable but not *that* mutable` + mixins = Object.assign({}, this.config.javascriptFunctions); + } + + let promises = []; + for (let key of keys) { + promises.push( + getJavaScriptData(inst, inputPath, key, { + mixins, + isObjectRequired: key === "data", + }), + ); + } + + let results = await Promise.all(promises); + let data = {}; + for (let result of results) { + Object.assign(data, result); + } + dataBench.after(); + + return data; + } + + async compile(str, inputPath, ...args) { + await this._runningInit(); + let defaultCompilationFn; + if (this._defaultEngine) { + defaultCompilationFn = async (data) => { + const renderFn = await this._defaultEngine.compile(str, inputPath, ...args); + return renderFn(data); + }; + } + + // Fall back to default compiler if the user does not provide their own + if (!this.entry.compile) { + if (defaultCompilationFn) { + return defaultCompilationFn; + } else { + throw new Error( + `Missing \`compile\` property for custom template syntax definition eleventyConfig.addExtension("${this.name}"). This is not necessary when aliasing to an existing template syntax.`, + ); + } + } + + // TODO generalize this (look at JavaScript.js) + let compiledFn = this.entry.compile.bind({ + config: this.config, + addDependencies: (from, toArray = []) => { + this.config.uses.addDependency(from, toArray); + }, + defaultRenderer: defaultCompilationFn, // bind defaultRenderer to compile function + })(str, inputPath); + + // Support `undefined` to skip compile/render + if (compiledFn) { + // Bind defaultRenderer to render function + if ("then" in compiledFn && typeof compiledFn.then === "function") { + // Promise, wait to bind + return compiledFn.then((fn) => { + if (typeof fn === "function") { + return fn.bind({ + defaultRenderer: defaultCompilationFn, + }); + } + return fn; + }); + } else if ("bind" in compiledFn && typeof compiledFn.bind === "function") { + return compiledFn.bind({ + defaultRenderer: defaultCompilationFn, + }); + } + } + + return compiledFn; + } + + get defaultTemplateFileExtension() { + return this.entry.outputFileExtension ?? "html"; + } + + // Whether or not to wrap in Eleventy layouts + useLayouts() { + // TODO future change fallback to `this.defaultTemplateFileExtension === "html"` + return this.entry.useLayouts ?? true; + } + + hasDependencies(inputPath) { + if (this.config.uses.getDependencies(inputPath) === false) { + return false; + } + return true; + } + + isFileRelevantTo(inputPath, comparisonFile, includeLayouts) { + return this.config.uses.isFileRelevantTo(inputPath, comparisonFile, includeLayouts); + } + + getCompileCacheKey(str, inputPath) { + let lastModifiedFile = this.eleventyConfig.getPreviousBuildModifiedFile(); + // Return this separately so we know whether or not to use the cached version + // but still return a key to cache this new render for next time + let isRelevant = this.isFileRelevantTo(inputPath, lastModifiedFile, false); + let useCache = !isRelevant; + + if (this.entry.compileOptions && "getCacheKey" in this.entry.compileOptions) { + if (typeof this.entry.compileOptions.getCacheKey !== "function") { + throw new Error( + `\`compileOptions.getCacheKey\` must be a function in addExtension for the ${this.name} type`, + ); + } + + return { + useCache, + key: this.entry.compileOptions.getCacheKey(str, inputPath), + }; + } + + let { key } = super.getCompileCacheKey(str, inputPath); + return { + useCache, + key, + }; + } + + permalinkNeedsCompilation(/*str*/) { + if (this.entry.compileOptions && "permalink" in this.entry.compileOptions) { + let p = this.entry.compileOptions.permalink; + if (p === "raw") { + return false; + } + + // permalink: false is aliased to permalink: () => false + if (p === false) { + return () => false; + } + + return this.entry.compileOptions.permalink; + } + + // Breaking: default changed from `true` to `false` in 3.0.0-alpha.13 + // Note: `false` is the same as "raw" here. + return false; + } + + static shouldSpiderJavaScriptDependencies(entry) { + if (entry.compileOptions && "spiderJavaScriptDependencies" in entry.compileOptions) { + return entry.compileOptions.spiderJavaScriptDependencies; + } + + return false; + } +} diff --git a/node_modules/@11ty/eleventy/src/Engines/FrontMatter/JavaScript.js b/node_modules/@11ty/eleventy/src/Engines/FrontMatter/JavaScript.js new file mode 100644 index 0000000..b91ba36 --- /dev/null +++ b/node_modules/@11ty/eleventy/src/Engines/FrontMatter/JavaScript.js @@ -0,0 +1,34 @@ +import { RetrieveGlobals } from "node-retrieve-globals"; + +// `javascript` Front Matter Type +export default function (frontMatterCode, context = {}) { + let { filePath } = context; + + // context.language would be nice as a guard, but was unreliable + if (frontMatterCode.trimStart().startsWith("{")) { + return context.engines.jsLegacy.parse(frontMatterCode, context); + } + + let vm = new RetrieveGlobals(frontMatterCode, { + filePath, + // ignored if vm.Module is stable (or --experimental-vm-modules) + transformEsmImports: true, + }); + + // Future warning until vm.Module is stable: + // If the frontMatterCode uses `import` this uses the `experimentalModuleApi` + // option in node-retrieve-globals to workaround https://github.com/zachleat/node-retrieve-globals/issues/2 + let data = { + page: { + // Theoretically fileSlug and filePathStem could be added here but require extensionMap + inputPath: filePath, + }, + }; + + // this is async, but it’s handled in Eleventy upstream. + return vm.getGlobalContext(data, { + reuseGlobal: true, + dynamicImport: true, + // addRequire: true, + }); +} diff --git a/node_modules/@11ty/eleventy/src/Engines/Html.js b/node_modules/@11ty/eleventy/src/Engines/Html.js new file mode 100644 index 0000000..a0f4101 --- /dev/null +++ b/node_modules/@11ty/eleventy/src/Engines/Html.js @@ -0,0 +1,33 @@ +import TemplateEngine from "./TemplateEngine.js"; + +export default class Html extends TemplateEngine { + constructor(name, eleventyConfig) { + super(name, eleventyConfig); + } + + get cacheable() { + return true; + } + + async #getPreEngine(preTemplateEngine) { + return this.engineManager.getEngine(preTemplateEngine, this.extensionMap); + } + + async compile(str, inputPath, preTemplateEngine) { + if (preTemplateEngine) { + let engine = await this.#getPreEngine(preTemplateEngine); + let fnReady = engine.compile(str, inputPath); + + return async function (data) { + let fn = await fnReady; + + return fn(data); + }; + } + + return function () { + // do nothing with data if preTemplateEngine is falsy + return str; + }; + } +} diff --git a/node_modules/@11ty/eleventy/src/Engines/JavaScript.js b/node_modules/@11ty/eleventy/src/Engines/JavaScript.js new file mode 100644 index 0000000..29b3b7c --- /dev/null +++ b/node_modules/@11ty/eleventy/src/Engines/JavaScript.js @@ -0,0 +1,240 @@ +import { TemplatePath, isPlainObject } from "@11ty/eleventy-utils"; + +import TemplateEngine from "./TemplateEngine.js"; +import EleventyBaseError from "../Errors/EleventyBaseError.js"; +import getJavaScriptData from "../Util/GetJavaScriptData.js"; +import { EleventyImport } from "../Util/Require.js"; +import { augmentFunction, augmentObject } from "./Util/ContextAugmenter.js"; + +class JavaScriptTemplateNotDefined extends EleventyBaseError {} + +export default class JavaScript extends TemplateEngine { + constructor(name, templateConfig) { + super(name, templateConfig); + this.instances = {}; + + this.config.events.on("eleventy#templateModified", (inputPath, metadata = {}) => { + let { usedByDependants, relevantLayouts } = metadata; + // Remove from cached instances when modified + let instancesToDelete = [ + inputPath, + ...(usedByDependants || []), + ...(relevantLayouts || []), + ].map((entry) => TemplatePath.addLeadingDotSlash(entry)); + for (let inputPath of instancesToDelete) { + if (inputPath in this.instances) { + delete this.instances[inputPath]; + } + } + }); + } + + get cacheable() { + return false; + } + + normalize(result) { + if (Buffer.isBuffer(result)) { + return result.toString(); + } + + return result; + } + + // String, Buffer, Promise + // Function, Class + // Object + // Module + _getInstance(mod) { + let noop = function () { + return ""; + }; + + let originalModData = mod?.data; + + if (typeof mod === "object" && mod.default && this.eleventyConfig.getIsProjectUsingEsm()) { + mod = mod.default; + } + + if (typeof mod === "string" || mod instanceof Buffer || mod.then) { + return { render: () => mod }; + } else if (typeof mod === "function") { + if (mod.prototype?.data || mod.prototype?.render) { + if (!("render" in mod.prototype)) { + mod.prototype.render = noop; + } + + if (!("data" in mod.prototype) && !mod.data && originalModData) { + mod.prototype.data = originalModData; + } + + return new mod(); + } else { + return { + ...(originalModData ? { data: originalModData } : undefined), + render: mod, + }; + } + } else if ("data" in mod || "render" in mod) { + if (!mod.render) { + mod.render = noop; + } + if (!mod.data && originalModData) { + mod.data = originalModData; + } + return mod; + } + } + + async #getInstanceFromInputPath(inputPath) { + let mod; + let relativeInputPath = + this.eleventyConfig.directories.getInputPathRelativeToInputDirectory(inputPath); + if (this.eleventyConfig.userConfig.isVirtualTemplate(relativeInputPath)) { + mod = this.eleventyConfig.userConfig.virtualTemplates[relativeInputPath].content; + } else { + let isEsm = this.eleventyConfig.getIsProjectUsingEsm(); + let cacheBust = !this.cacheable || !this.config.useTemplateCache; + mod = await EleventyImport(inputPath, isEsm ? "esm" : "cjs", { + cacheBust, + }); + } + + let inst = this._getInstance(mod); + if (inst) { + this.instances[inputPath] = inst; + } else { + throw new JavaScriptTemplateNotDefined( + `No JavaScript template returned from ${inputPath}. Did you assign module.exports (CommonJS) or export (ESM)?`, + ); + } + return inst; + } + + async getInstanceFromInputPath(inputPath) { + if (!this.instances[inputPath]) { + this.instances[inputPath] = this.#getInstanceFromInputPath(inputPath); + } + + return this.instances[inputPath]; + } + + /** + * JavaScript files defer to the module loader rather than read the files to strings + * + * @override + */ + needsToReadFileContents() { + return false; + } + + /** + * Use the module loader directly + * + * @override + */ + useJavaScriptImport() { + return true; + } + + async getExtraDataFromFile(inputPath) { + let inst = await this.getInstanceFromInputPath(inputPath); + return getJavaScriptData(inst, inputPath); + } + + getJavaScriptFunctions(inst) { + let fns = {}; + let configFns = this.config.javascriptFunctions; + + for (let key in configFns) { + // prefer pre-existing `page` javascriptFunction, if one exists + fns[key] = augmentFunction(configFns[key], { + source: inst, + overwrite: false, + }); + } + return fns; + } + + // Backwards compat + static wrapJavaScriptFunction(inst, fn) { + return augmentFunction(fn, { + source: inst, + }); + } + + addExportsToBundles(inst, url) { + let cfg = this.eleventyConfig.userConfig; + if (!("getBundleManagers" in cfg)) { + return; + } + + let managers = cfg.getBundleManagers(); + for (let name in managers) { + let mgr = managers[name]; + let key = mgr.getBundleExportKey(); + if (!key) { + continue; + } + + if (typeof inst[key] === "string") { + // export const css = ``; + mgr.addToPage(url, inst[key]); + } else if (isPlainObject(inst[key])) { + if (typeof inst[key][name] === "string") { + // Object with bundle names: + // export const bundle = { + // css: `` + // }; + mgr.addToPage(url, inst[key][name]); + } else if (isPlainObject(inst[key][name])) { + // Object with bucket names: + // export const bundle = { + // css: { + // default: `` + // } + // }; + for (let bucketName in inst[key][name]) { + mgr.addToPage(url, inst[key][name][bucketName], bucketName); + } + } + } + } + } + + async compile(str, inputPath) { + let inst; + if (str) { + // When str has a value, it's being used for permalinks in data + inst = this._getInstance(str); + } else { + // For normal templates, str will be falsy. + inst = await this.getInstanceFromInputPath(inputPath); + } + + if (inst?.render) { + return (data = {}) => { + // TODO does this do anything meaningful for non-classes? + // `inst` should have a normalized `render` function from _getInstance + + // Map exports to bundles + if (data.page?.url) { + this.addExportsToBundles(inst, data.page.url); + } + + augmentObject(inst, { + source: data, + overwrite: false, + }); + + Object.assign(inst, this.getJavaScriptFunctions(inst)); + + return this.normalize(inst.render.call(inst, data)); + }; + } + } + + static shouldSpiderJavaScriptDependencies() { + return true; + } +} 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); + }; + } +} diff --git a/node_modules/@11ty/eleventy/src/Engines/Markdown.js b/node_modules/@11ty/eleventy/src/Engines/Markdown.js new file mode 100644 index 0000000..ec1e1f6 --- /dev/null +++ b/node_modules/@11ty/eleventy/src/Engines/Markdown.js @@ -0,0 +1,100 @@ +import markdownIt from "markdown-it"; + +import TemplateEngine from "./TemplateEngine.js"; + +export default class Markdown extends TemplateEngine { + constructor(name, eleventyConfig) { + super(name, eleventyConfig); + + this.markdownOptions = {}; + + this.setLibrary(this.config.libraryOverrides.md); + } + + get cacheable() { + return true; + } + + setLibrary(mdLib) { + this.mdLib = mdLib || markdownIt(this.getMarkdownOptions()); + + // Overrides a highlighter set in `markdownOptions` + // This is separate so devs can pass in a new mdLib and still use the official eleventy plugin for markdown highlighting + if (this.config.markdownHighlighter && typeof this.mdLib.set === "function") { + this.mdLib.set({ + highlight: this.config.markdownHighlighter, + }); + } + + if (typeof this.mdLib.disable === "function") { + // Disable indented code blocks by default (Issue #2438) + this.mdLib.disable("code"); + } + + this.setEngineLib(this.mdLib, Boolean(this.config.libraryOverrides.md)); + } + + setMarkdownOptions(options) { + this.markdownOptions = options; + } + + getMarkdownOptions() { + // work with "mode" presets https://github.com/markdown-it/markdown-it#init-with-presets-and-options + if (typeof this.markdownOptions === "string") { + return this.markdownOptions; + } + + return Object.assign( + { + html: true, + }, + this.markdownOptions || {}, + ); + } + + // TODO use preTemplateEngine to help inform this + // needsCompilation() { + // return super.needsCompilation(); + // } + + async #getPreEngine(preTemplateEngine) { + if (typeof preTemplateEngine === "string") { + return this.engineManager.getEngine(preTemplateEngine, this.extensionMap); + } + + return preTemplateEngine; + } + + async compile(str, inputPath, preTemplateEngine, bypassMarkdown) { + let mdlib = this.mdLib; + + if (preTemplateEngine) { + let engine = await this.#getPreEngine(preTemplateEngine); + let fnReady = engine.compile(str, inputPath); + + if (bypassMarkdown) { + return async function (data) { + let fn = await fnReady; + return fn(data); + }; + } else { + return async function (data) { + let fn = await fnReady; + let preTemplateEngineRender = await fn(data); + let finishedRender = mdlib.render(preTemplateEngineRender, data); + return finishedRender; + }; + } + } else { + if (bypassMarkdown) { + return function () { + return str; + }; + } else { + return function (data) { + return mdlib.render(str, data); + }; + } + } + } +} diff --git a/node_modules/@11ty/eleventy/src/Engines/Nunjucks.js b/node_modules/@11ty/eleventy/src/Engines/Nunjucks.js new file mode 100755 index 0000000..70cca17 --- /dev/null +++ b/node_modules/@11ty/eleventy/src/Engines/Nunjucks.js @@ -0,0 +1,482 @@ +import NunjucksLib from "nunjucks"; +import debugUtil from "debug"; +import { TemplatePath } from "@11ty/eleventy-utils"; + +import TemplateEngine from "./TemplateEngine.js"; +import EleventyBaseError from "../Errors/EleventyBaseError.js"; +import { augmentObject } from "./Util/ContextAugmenter.js"; +import { withResolvers } from "../Util/PromiseUtil.js"; + +const debug = debugUtil("Eleventy:Nunjucks"); + +class EleventyNunjucksError extends EleventyBaseError {} + +export default class Nunjucks extends TemplateEngine { + constructor(name, eleventyConfig) { + super(name, eleventyConfig); + + this.nunjucksEnvironmentOptions = this.config.nunjucksEnvironmentOptions || { dev: true }; + + this.nunjucksPrecompiledTemplates = this.config.nunjucksPrecompiledTemplates || {}; + this._usingPrecompiled = Object.keys(this.nunjucksPrecompiledTemplates).length > 0; + + this.setLibrary(this.config.libraryOverrides.njk); + } + + // v3.1.0-alpha.1 we’ve moved to use Nunjucks’ internal cache instead of Eleventy’s + // get cacheable() { + // return false; + // } + + #getFileSystemDirs() { + let paths = new Set(); + paths.add(super.getIncludesDir()); + paths.add(TemplatePath.getWorkingDir()); + + // Filter out undefined paths + return Array.from(paths).filter(Boolean); + } + + #setEnv(override) { + if (override) { + this.njkEnv = override; + } else if (this._usingPrecompiled) { + // Precompiled templates to avoid eval! + const NodePrecompiledLoader = function () {}; + + NodePrecompiledLoader.prototype.getSource = (name) => { + // https://github.com/mozilla/nunjucks/blob/fd500902d7c88672470c87170796de52fc0f791a/nunjucks/src/precompiled-loader.js#L5 + return { + src: { + type: "code", + obj: this.nunjucksPrecompiledTemplates[name], + }, + // Maybe add this? + // path, + // noCache: true + }; + }; + + this.njkEnv = new NunjucksLib.Environment( + new NodePrecompiledLoader(), + this.nunjucksEnvironmentOptions, + ); + } else { + let fsLoader = new NunjucksLib.FileSystemLoader(this.#getFileSystemDirs()); + this.njkEnv = new NunjucksLib.Environment(fsLoader, this.nunjucksEnvironmentOptions); + } + + this.config.events.emit("eleventy.engine.njk", { + nunjucks: NunjucksLib, + environment: this.njkEnv, + }); + } + + setLibrary(override) { + this.#setEnv(override); + + // Note that a new Nunjucks engine instance is created for subsequent builds + // Eleventy Nunjucks is set to `cacheable` false above to opt out of Eleventy cache + this.config.events.on("eleventy#templateModified", (templatePath) => { + // NunjucksEnvironment: + // loader.pathToNames: {'ABSOLUTE_PATH/src/_includes/components/possum-home.css': 'components/possum-home.css'} + // loader.cache: { 'components/possum-home.css': [Template] } + // Nunjucks stores these as Operating System native paths + let absTmplPath = TemplatePath.normalizeOperatingSystemFilePath( + TemplatePath.absolutePath(templatePath), + ); + for (let loader of this.njkEnv.loaders) { + let nunjucksName = loader.pathsToNames[absTmplPath]; + if (nunjucksName) { + debug( + "Match found in Nunjucks cache via templateModified for %o, clearing this entry", + templatePath, + ); + delete loader.pathsToNames[absTmplPath]; + delete loader.cache[nunjucksName]; + } + } + + // Behavior prior to v3.1.0-alpha.1: + // this.njkEnv.invalidateCache(); + }); + + this.setEngineLib(this.njkEnv, Boolean(this.config.libraryOverrides.njk)); + + this.addFilters(this.config.nunjucksFilters); + this.addFilters(this.config.nunjucksAsyncFilters, true); + + // TODO these all go to the same place (addTag), add warnings for overwrites + // TODO(zachleat): variableName should work with quotes or without quotes (same as {% set %}) + this.addPairedShortcode("setAsync", function (content, variableName) { + this.ctx[variableName] = content; + return ""; + }); + + this.addCustomTags(this.config.nunjucksTags); + this.addAllShortcodes(this.config.nunjucksShortcodes); + this.addAllShortcodes(this.config.nunjucksAsyncShortcodes, true); + this.addAllPairedShortcodes(this.config.nunjucksPairedShortcodes); + this.addAllPairedShortcodes(this.config.nunjucksAsyncPairedShortcodes, true); + this.addGlobals(this.config.nunjucksGlobals); + } + + addFilters(filters, isAsync) { + for (let name in filters) { + this.njkEnv.addFilter(name, Nunjucks.wrapFilter(name, filters[name]), isAsync); + } + } + + static wrapFilter(name, fn) { + return function (...args) { + try { + augmentObject(this, { + source: this.ctx, + lazy: false, // context.env?.opts.throwOnUndefined, + }); + + return fn.call(this, ...args); + } catch (e) { + throw new EleventyNunjucksError( + `Error in Nunjucks Filter \`${name}\`${this.page ? ` (${this.page.inputPath})` : ""}`, + e, + ); + } + }; + } + + // Shortcodes + static normalizeContext(context) { + let obj = {}; + if (context.ctx) { + obj.ctx = context.ctx; + obj.env = context.env; + + augmentObject(obj, { + source: context.ctx, + lazy: false, // context.env?.opts.throwOnUndefined, + }); + } + return obj; + } + + addCustomTags(tags) { + for (let name in tags) { + this.addTag(name, tags[name]); + } + } + + addTag(name, tagFn) { + let tagObj; + if (typeof tagFn === "function") { + tagObj = tagFn(NunjucksLib, this.njkEnv); + } else { + throw new Error( + "Nunjucks.addTag expects a callback function to be passed in: addTag(name, function(nunjucksEngine) {})", + ); + } + + this.njkEnv.addExtension(name, tagObj); + } + + addGlobals(globals) { + for (let name in globals) { + this.addGlobal(name, globals[name]); + } + } + + addGlobal(name, globalFn) { + this.njkEnv.addGlobal(name, globalFn); + } + + addAllShortcodes(shortcodes, isAsync = false) { + for (let name in shortcodes) { + this.addShortcode(name, shortcodes[name], isAsync); + } + } + + addAllPairedShortcodes(shortcodes, isAsync = false) { + for (let name in shortcodes) { + this.addPairedShortcode(name, shortcodes[name], isAsync); + } + } + + _getShortcodeFn(shortcodeName, shortcodeFn, isAsync = false) { + return function ShortcodeFunction() { + this.tags = [shortcodeName]; + + this.parse = function (parser, nodes) { + let args; + let tok = parser.nextToken(); + + args = parser.parseSignature(true, true); + + // Nunjucks bug with non-paired custom tags bug still exists even + // though this issue is closed. Works fine for paired. + // https://github.com/mozilla/nunjucks/issues/158 + if (args.children.length === 0) { + args.addChild(new nodes.Literal(0, 0, "")); + } + + parser.advanceAfterBlockEnd(tok.value); + if (isAsync) { + return new nodes.CallExtensionAsync(this, "run", args); + } + return new nodes.CallExtension(this, "run", args); + }; + + this.run = function (...args) { + let resolve; + if (isAsync) { + resolve = args.pop(); + } + + let [context, ...argArray] = args; + + if (isAsync) { + let ret = shortcodeFn.call(Nunjucks.normalizeContext(context), ...argArray); + + // #3286 error messaging when the shortcode is not a promise + if (!ret?.then) { + resolve( + new EleventyNunjucksError( + `Error with Nunjucks shortcode \`${shortcodeName}\`: it was defined as asynchronous but was actually synchronous. This is important for Nunjucks.`, + ), + ); + } + + ret.then( + function (returnValue) { + resolve(null, new NunjucksLib.runtime.SafeString("" + returnValue)); + }, + function (e) { + resolve( + new EleventyNunjucksError(`Error with Nunjucks shortcode \`${shortcodeName}\``, e), + ); + }, + ); + } else { + try { + let ret = shortcodeFn.call(Nunjucks.normalizeContext(context), ...argArray); + return new NunjucksLib.runtime.SafeString("" + ret); + } catch (e) { + throw new EleventyNunjucksError( + `Error with Nunjucks shortcode \`${shortcodeName}\``, + e, + ); + } + } + }; + }; + } + + _getPairedShortcodeFn(shortcodeName, shortcodeFn, isAsync = false) { + return function PairedShortcodeFunction() { + this.tags = [shortcodeName]; + + this.parse = function (parser, nodes) { + var tok = parser.nextToken(); + + var args = parser.parseSignature(true, true); + parser.advanceAfterBlockEnd(tok.value); + + var body = parser.parseUntilBlocks("end" + shortcodeName); + parser.advanceAfterBlockEnd(); + + return new nodes.CallExtensionAsync(this, "run", args, [body]); + }; + + this.run = function (...args) { + let resolve = args.pop(); + let body = args.pop(); + let [context, ...argArray] = args; + + body(function (e, bodyContent) { + if (e) { + resolve( + new EleventyNunjucksError( + `Error with Nunjucks paired shortcode \`${shortcodeName}\``, + e, + ), + ); + } + + if (isAsync) { + let ret = shortcodeFn.call( + Nunjucks.normalizeContext(context), + bodyContent, + ...argArray, + ); + + // #3286 error messaging when the shortcode is not a promise + if (!ret?.then) { + throw new EleventyNunjucksError( + `Error with Nunjucks shortcode \`${shortcodeName}\`: it was defined as asynchronous but was actually synchronous. This is important for Nunjucks.`, + ); + } + + ret.then( + function (returnValue) { + resolve(null, new NunjucksLib.runtime.SafeString(returnValue)); + }, + function (e) { + resolve( + new EleventyNunjucksError( + `Error with Nunjucks paired shortcode \`${shortcodeName}\``, + e, + ), + ); + }, + ); + } else { + try { + resolve( + null, + new NunjucksLib.runtime.SafeString( + shortcodeFn.call(Nunjucks.normalizeContext(context), bodyContent, ...argArray), + ), + ); + } catch (e) { + resolve( + new EleventyNunjucksError( + `Error with Nunjucks paired shortcode \`${shortcodeName}\``, + e, + ), + ); + } + } + }); + }; + }; + } + + addShortcode(shortcodeName, shortcodeFn, isAsync = false) { + let fn = this._getShortcodeFn(shortcodeName, shortcodeFn, isAsync); + this.njkEnv.addExtension(shortcodeName, new fn()); + } + + addPairedShortcode(shortcodeName, shortcodeFn, isAsync = false) { + let fn = this._getPairedShortcodeFn(shortcodeName, shortcodeFn, isAsync); + this.njkEnv.addExtension(shortcodeName, new fn()); + } + + // Don’t return a boolean if permalink is a function (see TemplateContent->renderPermalink) + permalinkNeedsCompilation(str) { + if (typeof str === "string") { + return this.needsCompilation(str); + } + } + + needsCompilation(str) { + // Defend against syntax customisations: + // https://mozilla.github.io/nunjucks/api.html#customizing-syntax + let optsTags = this.njkEnv.opts.tags || {}; + let blockStart = optsTags.blockStart || "{%"; + let variableStart = optsTags.variableStart || "{{"; + let commentStart = optsTags.variableStart || "{#"; + + return ( + str.indexOf(blockStart) !== -1 || + str.indexOf(variableStart) !== -1 || + str.indexOf(commentStart) !== -1 + ); + } + + _getParseExtensions() { + if (this._parseExtensions) { + return this._parseExtensions; + } + + // add extensions so the parser knows about our custom tags/blocks + let ext = []; + for (let name in this.config.nunjucksTags) { + let fn = this._getShortcodeFn(name, () => {}); + ext.push(new fn()); + } + for (let name in this.config.nunjucksShortcodes) { + let fn = this._getShortcodeFn(name, () => {}); + ext.push(new fn()); + } + for (let name in this.config.nunjucksAsyncShortcodes) { + let fn = this._getShortcodeFn(name, () => {}, true); + ext.push(new fn()); + } + for (let name in this.config.nunjucksPairedShortcodes) { + let fn = this._getPairedShortcodeFn(name, () => {}); + ext.push(new fn()); + } + for (let name in this.config.nunjucksAsyncPairedShortcodes) { + let fn = this._getPairedShortcodeFn(name, () => {}, true); + ext.push(new fn()); + } + + this._parseExtensions = ext; + return ext; + } + + /* Outputs an Array of lodash get selectors */ + parseForSymbols(str) { + if (!str) { + return []; + } + const { parser, nodes } = NunjucksLib; + let obj = parser.parse(str, this._getParseExtensions()); + if (!obj) { + return []; + } + let linesplit = str.split("\n"); + let values = obj.findAll(nodes.Value); + let symbols = obj.findAll(nodes.Symbol).map((entry) => { + let name = [entry.value]; + let nestedIndex = -1; + for (let val of values) { + if (nestedIndex > -1) { + /* deep.object.syntax */ + if (linesplit[val.lineno].charAt(nestedIndex) === ".") { + name.push(val.value); + nestedIndex += val.value.length + 1; + } else { + nestedIndex = -1; + } + } else if ( + val.lineno === entry.lineno && + val.colno === entry.colno && + val.value === entry.value + ) { + nestedIndex = entry.colno + entry.value.length; + } + } + return name.join("."); + }); + + let uniqueSymbols = Array.from(new Set(symbols)); + return uniqueSymbols; + } + + async compile(str, inputPath) { + let tmpl; + + // *All* templates are precompiled to avoid runtime eval + if (this._usingPrecompiled) { + tmpl = this.njkEnv.getTemplate(str, true); + } else if (!inputPath || inputPath === "njk" || inputPath === "md") { + tmpl = new NunjucksLib.Template(str, this.njkEnv, null, false); + } else { + tmpl = new NunjucksLib.Template(str, this.njkEnv, inputPath, false); + } + + return function (data) { + let { promise, resolve, reject } = withResolvers(); + + tmpl.render(data, (error, result) => { + if (error) { + reject(error); + } else { + resolve(result); + } + }); + + return promise; + }; + } +} diff --git a/node_modules/@11ty/eleventy/src/Engines/TemplateEngine.js b/node_modules/@11ty/eleventy/src/Engines/TemplateEngine.js new file mode 100644 index 0000000..234aa4e --- /dev/null +++ b/node_modules/@11ty/eleventy/src/Engines/TemplateEngine.js @@ -0,0 +1,206 @@ +import debugUtil from "debug"; +import EleventyBaseError from "../Errors/EleventyBaseError.js"; + +class TemplateEngineConfigError extends EleventyBaseError {} + +const debug = debugUtil("Eleventy:TemplateEngine"); + +const AMENDED_INSTANCES = new Set(); + +export default class TemplateEngine { + #extensionMap; + #engineManager; + #benchmarks; + + constructor(name, eleventyConfig) { + this.name = name; + + this.engineLib = null; + + if (!eleventyConfig) { + throw new TemplateEngineConfigError("Missing `eleventyConfig` argument."); + } + this.eleventyConfig = eleventyConfig; + } + + get cacheable() { + return false; + } + + get dirs() { + return this.eleventyConfig.directories; + } + + get inputDir() { + return this.dirs.input; + } + + get includesDir() { + return this.dirs.includes; + } + + get config() { + if (this.eleventyConfig.constructor.name !== "TemplateConfig") { + throw new Error("Expecting a TemplateConfig instance."); + } + + return this.eleventyConfig.getConfig(); + } + + get benchmarks() { + if (!this.#benchmarks) { + this.#benchmarks = { + aggregate: this.config.benchmarkManager.get("Aggregate"), + }; + } + return this.#benchmarks; + } + + get engineManager() { + return this.#engineManager; + } + + set engineManager(manager) { + this.#engineManager = manager; + } + + get extensionMap() { + if (!this.#extensionMap) { + throw new Error("Internal error: missing `extensionMap` in TemplateEngine."); + } + return this.#extensionMap; + } + + set extensionMap(map) { + this.#extensionMap = map; + } + + get extensions() { + if (!this._extensions) { + this._extensions = this.extensionMap.getExtensionsFromKey(this.name); + } + return this._extensions; + } + + get extensionEntries() { + if (!this._extensionEntries) { + this._extensionEntries = this.extensionMap.getExtensionEntriesFromKey(this.name); + } + return this._extensionEntries; + } + + getName() { + return this.name; + } + + // Backwards compat + getIncludesDir() { + return this.includesDir; + } + + /** + * @protected + */ + setEngineLib(engineLib, isOverrideViaSetLibrary = false) { + this.engineLib = engineLib; + + // Run engine amendments (via issue #2438) + // Issue #3816: this isn’t ideal but there is no other way to reset a markdown instance if it was also overridden by addLibrary + if (AMENDED_INSTANCES.has(engineLib)) { + return; + } + + if (isOverrideViaSetLibrary) { + AMENDED_INSTANCES.add(engineLib); + } + debug( + "Running amendLibrary for %o (number of amendments: %o)", + this.name, + this.config.libraryAmendments[this.name]?.length, + ); + + for (let amendment of this.config.libraryAmendments[this.name] || []) { + // TODO it’d be nice if this were async friendly + amendment(engineLib); + } + } + + getEngineLib() { + return this.engineLib; + } + + async _testRender(str, data) { + // @ts-ignore + let fn = await this.compile(str); + return fn(data); + } + + useJavaScriptImport() { + return false; + } + + // JavaScript files defer to the module loader rather than read the files to strings + needsToReadFileContents() { + return true; + } + + getExtraDataFromFile() { + return {}; + } + + getCompileCacheKey(str, inputPath) { + // Changing to use inputPath and contents, using only file contents (`str`) caused issues when two + // different files had identical content (2.0.0-canary.16) + + // Caches are now segmented based on inputPath so using inputPath here is superfluous (2.0.0-canary.19) + // But we do want a non-falsy value here even if `str` is an empty string. + return { + useCache: true, + key: inputPath + str, + }; + } + + get defaultTemplateFileExtension() { + return "html"; + } + + // Whether or not to wrap in Eleventy layouts + useLayouts() { + return true; + } + + /** @returns {boolean|undefined} */ + permalinkNeedsCompilation(str) { + return this.needsCompilation(); + } + + // whether or not compile is needed or can we return the plaintext? + needsCompilation(str) { + return true; + } + + /** + * Make sure compile is implemented downstream. + * @abstract + * @return {Promise} + */ + async compile() { + throw new Error("compile() must be implemented by engine"); + } + + // See https://v3.11ty.dev/docs/watch-serve/#watch-javascript-dependencies + static shouldSpiderJavaScriptDependencies() { + return false; + } + + hasDependencies(inputPath) { + if (this.config.uses.getDependencies(inputPath) === false) { + return false; + } + return true; + } + + isFileRelevantTo(inputPath, comparisonFile) { + return this.config.uses.isFileRelevantTo(inputPath, comparisonFile); + } +} diff --git a/node_modules/@11ty/eleventy/src/Engines/TemplateEngineManager.js b/node_modules/@11ty/eleventy/src/Engines/TemplateEngineManager.js new file mode 100644 index 0000000..913a803 --- /dev/null +++ b/node_modules/@11ty/eleventy/src/Engines/TemplateEngineManager.js @@ -0,0 +1,193 @@ +import debugUtil from "debug"; +import EleventyBaseError from "../Errors/EleventyBaseError.js"; + +const debug = debugUtil("Eleventy:TemplateEngineManager"); + +class TemplateEngineManager { + constructor(eleventyConfig) { + if (!eleventyConfig || eleventyConfig.constructor.name !== "TemplateConfig") { + throw new EleventyBaseError("Missing or invalid `config` argument."); + } + this.eleventyConfig = eleventyConfig; + + this.engineCache = {}; + this.importCache = {}; + } + + get config() { + return this.eleventyConfig.getConfig(); + } + + static isAlias(entry) { + if (entry.aliasKey) { + return true; + } + + return entry.key !== entry.extension; + } + + static isSimpleAlias(entry) { + if (!this.isAlias(entry)) { + return false; + } + + // has keys other than key, extension, and aliasKey + return ( + Object.keys(entry).some((key) => { + return key !== "key" && key !== "extension" && key !== "aliasKey"; + }) === false + ); + } + + get keyToClassNameMap() { + if (!this._keyToClassNameMap) { + this._keyToClassNameMap = { + md: "Markdown", + html: "Html", + njk: "Nunjucks", + liquid: "Liquid", + "11ty.js": "JavaScript", + }; + + // Custom entries *can* overwrite default entries above + if ("extensionMap" in this.config) { + for (let entry of this.config.extensionMap) { + // either the key does not already exist or it is not a simple alias and is an override: https://v3.11ty.dev/docs/languages/custom/#overriding-an-existing-template-language + let existingTarget = this._keyToClassNameMap[entry.key]; + let isAlias = TemplateEngineManager.isAlias(entry); + + if (!existingTarget && isAlias) { + throw new Error( + `An attempt to alias ${entry.aliasKey} to ${entry.key} was made, but ${entry.key} is not a recognized template syntax.`, + ); + } + + if (isAlias) { + // only `key` and `extension`, not `compile` or other options + if (!TemplateEngineManager.isSimpleAlias(entry)) { + this._keyToClassNameMap[entry.aliasKey] = "Custom"; + } else { + this._keyToClassNameMap[entry.aliasKey] = this._keyToClassNameMap[entry.key]; + } + } else { + // not an alias, so `key` and `extension` are the same here. + // *can* override a built-in extension! + this._keyToClassNameMap[entry.key] = "Custom"; + } + } + } + } + + return this._keyToClassNameMap; + } + + reset() { + this.engineCache = {}; + } + + getClassNameFromTemplateKey(key) { + return this.keyToClassNameMap[key]; + } + + hasEngine(name) { + return !!this.getClassNameFromTemplateKey(name); + } + + async getEngineClassByExtension(extension) { + if (this.importCache[extension]) { + return this.importCache[extension]; + } + + let promise; + + // We include these as raw strings (and not more readable variables) so they’re parsed by a bundler. + if (extension === "md") { + promise = import("./Markdown.js").then((mod) => mod.default); + } else if (extension === "html") { + promise = import("./Html.js").then((mod) => mod.default); + } else if (extension === "njk") { + promise = import("./Nunjucks.js").then((mod) => mod.default); + } else if (extension === "liquid") { + promise = import("./Liquid.js").then((mod) => mod.default); + } else if (extension === "11ty.js") { + promise = import("./JavaScript.js").then((mod) => mod.default); + } else { + promise = this.getCustomEngineClass(); + } + + this.importCache[extension] = promise; + + return promise; + } + + async getCustomEngineClass() { + if (!this._CustomEngine) { + this._CustomEngine = import("./Custom.js").then((mod) => mod.default); + } + return this._CustomEngine; + } + + async #getEngine(name, extensionMap) { + let cls = await this.getEngineClassByExtension(name); + let instance = new cls(name, this.eleventyConfig); + instance.extensionMap = extensionMap; + instance.engineManager = this; + + let extensionEntry = extensionMap.getExtensionEntry(name); + + // Override a built-in extension (md => md) + // If provided a "Custom" engine using addExtension, but that engine's instance is *not* custom, + // The user must be overriding a built-in engine i.e. addExtension('md', { ...overrideBehavior }) + let className = this.getClassNameFromTemplateKey(name); + + if (className === "Custom" && instance.constructor.name !== "CustomEngine") { + let CustomEngine = await this.getCustomEngineClass(); + let overrideCustomEngine = new CustomEngine(name, this.eleventyConfig); + + // Keep track of the "default" engine 11ty would normally use + // This allows the user to access the default engine in their override + overrideCustomEngine.setDefaultEngine(instance); + + instance = overrideCustomEngine; + // Alias to a built-in extension (11ty.tsx => 11ty.js) + } else if ( + instance.constructor.name === "CustomEngine" && + TemplateEngineManager.isAlias(extensionEntry) + ) { + // add defaultRenderer for complex aliases with their own compile functions. + let originalEngineInstance = await this.getEngine(extensionEntry.key, extensionMap); + instance.setDefaultEngine(originalEngineInstance); + } + + return instance; + } + + isEngineRemovedFromCore(name) { + return ["ejs", "hbs", "mustache", "haml", "pug"].includes(name) && !this.hasEngine(name); + } + + async getEngine(name, extensionMap) { + // Bundled engine deprecation + if (this.isEngineRemovedFromCore(name)) { + throw new Error( + `Per the 11ty Community Survey (2023), the "${name}" template language was moved from core to an officially supported plugin in v3.0. These plugins live here: https://github.com/11ty/eleventy-plugin-template-languages and are documented on their respective template language docs at https://v3.11ty.dev/docs/languages/ You are also empowered to implement *any* template language yourself using https://v3.11ty.dev/docs/languages/custom/`, + ); + } + + if (!this.hasEngine(name)) { + throw new Error(`Template Engine ${name} does not exist in getEngine()`); + } + // TODO these cached engines should be based on extensions not name, then we can remove the error in + // "Double override (not aliases) throws an error" test in TemplateRenderCustomTest.js + if (!this.engineCache[name]) { + debug("Engine cache miss %o (should only happen once per engine type)", name); + // Make sure cache key is based on name and not path + // Custom class is used for all plugins, cache once per plugin + this.engineCache[name] = this.#getEngine(name, extensionMap); + } + + return this.engineCache[name]; + } +} + +export default TemplateEngineManager; diff --git a/node_modules/@11ty/eleventy/src/Engines/Util/ContextAugmenter.js b/node_modules/@11ty/eleventy/src/Engines/Util/ContextAugmenter.js new file mode 100644 index 0000000..dd5fbc6 --- /dev/null +++ b/node_modules/@11ty/eleventy/src/Engines/Util/ContextAugmenter.js @@ -0,0 +1,67 @@ +const DATA_KEYS = ["page", "eleventy"]; + +function augmentFunction(fn, options = {}) { + let t = typeof fn; + if (t !== "function") { + throw new Error( + "Invalid type passed to `augmentFunction`. A function was expected and received: " + t, + ); + } + + /** @this {object} */ + return function (...args) { + let context = augmentObject(this || {}, options); + return fn.call(context, ...args); + }; +} + +function augmentObject(targetObject, options = {}) { + options = Object.assign( + { + source: undefined, // where to copy from + overwrite: true, + lazy: false, // lazily fetch the property + // getter: function() {}, + }, + options, + ); + + for (let key of DATA_KEYS) { + // Skip if overwrite: false and prop already exists on target + if (!options.overwrite && targetObject[key]) { + continue; + } + + if (options.lazy) { + let value; + if (typeof options.getter == "function") { + value = () => options.getter(key, options.source); + } else { + value = () => options.source?.[key]; + } + + // lazy getter important for Liquid strictVariables support + Object.defineProperty(targetObject, key, { + writable: true, + configurable: true, + enumerable: true, + value, + }); + } else { + let value; + if (typeof options.getter == "function") { + value = options.getter(key, options.source); + } else { + value = options.source?.[key]; + } + + if (value) { + targetObject[key] = value; + } + } + } + + return targetObject; +} + +export { DATA_KEYS as augmentKeys, augmentFunction, augmentObject }; |
