diff --git a/.changeset/1-eleventy-options.md b/.changeset/1-eleventy-options.md new file mode 100644 index 0000000..433ab95 --- /dev/null +++ b/.changeset/1-eleventy-options.md @@ -0,0 +1,38 @@ +--- +'eleventy-plugin-vento': major +--- + +Adds complete support for Eleventy shortcodes and filters within Vento. Shortcodes are now loaded by default as Vento tags and will no longer be exposed as functions in your page data. + +The implementation of single shortcodes remains largely similar, just replace function-like calls with Vento tags. + +```diff +- {{ nametag('Noel', 'Forte') }} ++ {{ nametag 'Noel', 'Forte' }} +``` + +Of course, the big news is that **paired** shortcodes are now officially supported by this plugin!! Prior to this release, paired shortcodes were exposed just like regular shortcodes, but were plain JS functions. With this release you can now use paired shortcodes just like any other tag. + +```hbs + +{{ blockquote("

Call me Ishmael. Some years ago—never mind how long precisely—having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world.

\n

It is a way I have of driving off the spleen and regulating the circulation.

", "Herman Melville", "1851") }} + + +{{ blockquote 'Herman Melville', '1851' }} +

Call me Ishmael. Some years ago—never mind how long precisely—having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world.

+

It is a way I have of driving off the spleen and regulating the circulation.

+{{ /blockquote }} +``` + +This functionality became possible with the addition of the `getShortcodes()` and `getPairedShortcodes()` functions that were added in [Eleventy v3.0.0-alpha.15](https://github.com/11ty/eleventy/releases/tag/v3.0.0-alpha.15). Because of dependence on these features, Eleventy 3.0.0-alpha.15 is now the **minimum required version** in order to use this plugin. + +Because these changes removed direct dependence on Eleventy JavaScript functions, the `addHelpers` option has been replaced with 3 new options: `shortcodes`, `pairedShortcodes` and `filters`. **All of them are enabled by default** but can be disabled in your plugin config like so. + +```diff +eleventyConfig.addPlugin(VentoPlugin, { +- addHelpers: false, ++ shortcodes: false, ++ pairedShortcodes: false, ++ filters: false, +}); +``` diff --git a/src/engine.js b/src/engine.js index 8ab838e..15e69ba 100644 --- a/src/engine.js +++ b/src/engine.js @@ -1,27 +1,30 @@ /** * @file Class to facilitate ventojs processing * - * @typedef {{eleventy?: Record, page?: Record}} Context - * @typedef {Context & {[K: string]: unknown}} PageData + * @typedef {{eleventy?: Record, page?: Record}} EleventyContext + * @typedef {Context & Record} EleventyData + * @typedef {(...args: unknown[]) => unknown} EleventyFunction + * @typedef {import('ventojs/src/environment.js').Environment} VentoEnvironment + * @typedef {VentoEnvironment & { _11ty: { ctx: Context, tags: Record }}} EleventyVentoEnvironment */ // External library -import vento from 'ventojs'; +import ventojs from 'ventojs'; -// Local modules -import { createVentoFilter } from './modules/create-filter.js'; +// Internal modules +import { createVentoTag } from './modules/create-vento-tag.js'; // TODO: Update this if it becomes possible to import `augmentKeys` from Eleventy. const DATA_KEYS = ['page', 'eleventy']; export class VentoEngine { - /** @type {Context} */ - #context = {}; #env; - /** @param {import('ventojs').Options} options */ + /** @param {import('ventojs/src/environment.js').Options} options */ constructor(options) { - this.#env = vento(options); + /** Initialize vento @type {EleventyVentoEnvironment} */ + this.#env = ventojs(options); + this.#env._11ty = { ctx: {}, functions: {} }; } /** @param {string?} key */ @@ -36,17 +39,29 @@ export class VentoEngine { } } - /** @param {Record} filters */ + /** @param {PageData} newContext */ + loadContext(newContext) { + // Loop through allowed keys and load those into the context + for (const key of DATA_KEYS) { + this.#env._11ty.ctx[key] = newContext[key]; + } + } + + /** @param {Record} filters */ loadFilters(filters) { - for (const name in filters) { - this.#env.filters[name] = createVentoFilter(filters[name], this.#context); + for (const [name, fn] of Object.entries(filters)) { + this.#env.filters[name] = (...args) => fn.apply(this.#env._11ty.ctx, args); } } - /** @param {PageData} newContext */ - loadContext(newContext) { - for (const key of DATA_KEYS) { - this.#context[key] = newContext[key]; + /** + * @param {Record} shortcodes + * @param {boolean} paired + */ + loadShortcodes(shortcodes, paired = false) { + for (const [name, fn] of Object.entries(shortcodes)) { + this.#env._11ty.functions[name] = fn; + this.#env.tags.push(createVentoTag(name, paired)); } } @@ -63,7 +78,7 @@ export class VentoEngine { const result = await this.#env.runString(content, data, path); // Clear the cache for this path if the input doesn't match - if (data.page?.rawInput !== content) this.#env.cache.clear(); + if (data.page?.rawInput !== content) this.#env.cache.clear(path); return result.content; } diff --git a/src/index.js b/src/index.js index 155a742..65d074e 100644 --- a/src/index.js +++ b/src/index.js @@ -67,17 +67,30 @@ export function VentoPlugin(eleventyConfig, userOptions) { options.plugins.push(autotrimPlugin({ tags: [...tagSet] })); } - // Add preserve tag plugin if enabled - if (options.ignoreTag) options.plugins.push(ignoreTagPlugin); + // Add ignore tag plugin if enabled + if (options.ignoreTag) { + options.plugins.push(ignoreTagPlugin); + } // Create the vento engine instance const vento = new VentoEngine(options.ventoOptions); - vento.emptyCache(); // Ensure cache is empty - vento.loadPlugins(options.plugins); // Load plugin functions + // Ensure cache is empty + vento.emptyCache(); + + // Load plugins + vento.loadPlugins(options.plugins); - if (options.filters) vento.loadFilters(eleventyConfig.getFilters()); - if (options.shortcodes) vento.loadShortcodes(eleventyConfig.getShortcodes()); + // Add filters, single and paired shortcodes if enabled + if (options.filters) { + vento.loadFilters(eleventyConfig.getFilters()); + } + if (options.shortcodes) { + vento.loadShortcodes(eleventyConfig.getShortcodes(), false); + } + if (options.pairedShortcodes) { + vento.loadShortcodes(eleventyConfig.getPairedShortcodes(), true); + } // Add vto as a template format eleventyConfig.addTemplateFormats('vto'); diff --git a/src/modules/create-filter.js b/src/modules/create-filter.js deleted file mode 100644 index 4e6b539..0000000 --- a/src/modules/create-filter.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * @file Create a vento filter from an Eleventy filter - * - * @param {(...params) => unknown} fn - * @param {Record} context - * @returns {import('ventojs/src/environment.js').Filter} - */ -export function createVentoFilter(fn, context) { - return (...args) => fn.apply(context, args); -} diff --git a/src/modules/create-vento-tag.js b/src/modules/create-vento-tag.js new file mode 100644 index 0000000..89b3c99 --- /dev/null +++ b/src/modules/create-vento-tag.js @@ -0,0 +1,54 @@ +/** + * @file Helper function that creates vento tags from eleventy functions + * + * @param {string} name + * @param {boolean} paired + */ + +export function createVentoTag(name, paired) { + /** @type {import("ventojs/src/environment.js").Tag} */ + const tag = (env, code, output, tokens) => { + if (!code.startsWith(name)) return; + + // Declare helper object path strings + const fn = `__env._11ty.functions.${name}`; + const ctx = '__env._11ty.ctx'; + let args = [code.replace(name, '').trim()]; + + const varname = output.startsWith('__shortcode_content') + ? `${output}_precomp` + : '__shortcode_content'; + + // Create an array to hold compiled template code + const compiled = []; + + if (paired) { + args.unshift(env.compileFilters(tokens, varname, env.options.autoescape)); + compiled.push( + `{ // START paired-${name}`, + `let ${varname} = "";`, + ...env.compileTokens(tokens, varname, [`/${name}`]) + ); + if (tokens.length > 0 && (tokens[0][0] !== 'tag' || tokens[0][1] !== `/${name}`)) { + throw new Error(`Missing closing tag for ${name} tag: ${code}`); + } + tokens.shift(); + } + + // Compile arguments into a string + args = args.some(Boolean) ? `, ${args.filter(Boolean).join(', ')}` : ''; + + compiled.push( + `{ // START ${name}`, + `const __shortcode_result = await ${fn}.call(${ctx + args});`, + `${output} += ${env.compileFilters(tokens, '__shortcode_result', env.options.autoescape)}`, + `} // END ${name}` + ); + + if (paired) compiled.push(`} // END paired-${name}`); + + return compiled.join('\n'); + }; + + return Object.defineProperty(tag, 'name', { value: paired ? `${name}PairedTag` : `${name}Tag` }); +}