diff --git a/README.md b/README.md index 3e36123..7c09eff 100644 --- a/README.md +++ b/README.md @@ -21,23 +21,27 @@ This package adds a `.twig` template engine to Eleventy that lets you use the pu ## Features -- **Built-in Shortcodes**: Uses [`twig.extendFunction()`](https://twig.symfony.com/doc/2.x/advanced.html) to extend Twig -- **Twig Namespaces**: Uses `Twig` built-in loaders to provide [namespaces](https://twig.symfony.com/doc/3.x/api.html#built-in-loaders) -- **Responsive Images**: Uses [`@11ty/eleventy-img`](https://github.com/11ty/eleventy-img) plugin to autogenerate responsive images -- **Hashed Assets**: If you have generated a manifest (e.g. with [`@factorial/eleventy-plugin-fstack`](https://github.com/factorial-io/eleventy-plugin-fstack)) you could let Eleventy replace unhashed assets like `css/js` automatically with their hashed versions +- Use **functions** and [`twig.extendFunction()`](https://twig.symfony.com/doc/2.x/advanced.html#functions) to extend Twig with custom functions +- Use **filters** and [`twig.extendFilter()`](https://twig.symfony.com/doc/2.x/advanced.html#filters) to extend Twig with custom filters +- Uses `Twig` built-in loaders to provide **[namespaces](https://twig.symfony.com/doc/3.x/api.html#built-in-loaders)** + +Furthermore please take a look at some of the **sample implementations** for functions and filters to showcase how [Eleventy](https://www.11ty.dev/docs/credits/) and [Twig.js](https://github.com/twigjs/twig.js/) can work together: + +- **[Responsive Images](lib/functions/README.md)**: Uses [`@11ty/eleventy-img`](https://github.com/11ty/eleventy-img) plugin to autogenerate responsive images +- **[Hashed Assets](lib/functions/README.md)**: If you have generated a manifest (e.g. with [`@factorial/eleventy-plugin-fstack`](https://github.com/factorial-io/eleventy-plugin-fstack)) you could let Eleventy replace unhashed assets like `css/js` automatically with their hashed versions ## Getting Started -Install the latest `@factorial/eleventy-plugin-twig` release as well as `twig` and optionally `@11ty/eleventy-img` as node modules with `yarn`: +Install the latest `@factorial/eleventy-plugin-twig` release as well as `twig` as node modules with `yarn`: ```sh -yarn add --dev @factorial/eleventy-plugin-twig @11ty/eleventy-img twig +yarn add --dev @factorial/eleventy-plugin-twig twig ``` or `npm`: ```sh -npm install --save-dev @factorial/eleventy-plugin-twig @11ty/eleventy-img twig +npm install --save-dev @factorial/eleventy-plugin-twig twig ``` ## Usage @@ -58,49 +62,27 @@ As mentioned in the `eleventyConfig.addPlugin(eleventy-plugin-twig, USER_OPTIONS ```js /** - * @typedef {object} ELEVENTY_DIRECTORIES - * @property {string} input - Eleventy template path - * @property {string} output - Eleventy build path - * @property {string} [includes] - Eleventy includes path relativ to input - * @property {string} [layouts] - Eleventy separate layouts path relative to input - * @property {string} [watch] - add more watchTargets to Eleventy - */ - -/** - * @typedef {object} ASSETS - * @property {string} root - path to the root folder from projects root (e.g. src) - * @property {string} base - base path for assets relative to the root folder (e.g. assets) - * @property {string} css - path to the css folder relative to the base (e.g. css) - * @property {string} js - path to the js folder relative to the base (e.g. js) - * @property {string} images - path to the image folder relative to the base (e.g. images) - */ - -/** - * @typedef {object} IMAGES - * @property {Array} widths - those image sizes will be autogenereated / aspect-ratio will be respected - * @property {Array} formats - jpeg/avif/webp/png/gif - * @property {string} additionalAttributes - those attributes will be added to the image element + * @typedef {object} FUNCTION + * @property {string} symbol - method name for twig to register + * @property {function(import("@11ty/eleventy").UserConfig, USER_OPTIONS, ...* ):any} callback - callback which is called by twig */ /** - * @typedef {object} SHORTCODE - * @property {string} symbol - method name for twig to register - * @property {function(import("@11ty/eleventy").UserConfig, USER_OPTIONS, ...* ):any} callback - callback which is called by twig + * @typedef {object} FILTER + * @property {string} symbol - filter name for twig to register + * @property {function(import("@11ty/eleventy").UserConfig, USER_OPTIONS, ...* ):any} callback - callback which is invoked by the filter */ /** * @typedef {object} TWIG_OPTIONS - * @property {SHORTCODE[]} [shortcodes] - array of shortcodes + * @property {Function[]} [functions] - array of functions to extend twig + * @property {FILTER[]} [filter] - array of filter to extend twig * @property {boolean} [cache] - you could enable the twig cache for whatever reasons here * @property {Object} [namespaces] - define namespaces to include/extend templates more easily by "@name" */ /** * @typedef {object} USER_OPTIONS - * @property {string} mixManifest - path to the mixManifest file relative to the build folder - * @property {ASSETS} [assets] - where to find all the assets relative to the build folder - * @property {IMAGES} [images] - options for Eleventys image processing - * @property {ELEVENTY_DIRECTORIES} dir - Eleventy folder decisions * @property {TWIG_OPTIONS} [twig] - twig options */ ``` @@ -114,72 +96,26 @@ You could use this as a starting point and customize to your individual needs: const USER_OPTIONS = { twig: { namespaces: { - elements: "src/include/elements", - patterns: "src/include/patterns", - "template-components": "src/include/template-components", - templates: "src/include/templates", + // for example: + // elements: "src/include/elements", + // patterns: "src/include/patterns", + // "template-components": "src/include/template-components", + // templates: "src/include/templates", }, - }, - mixManifest: "mix-manifest.json", - assets: { - root: "src", - base: "assets", - css: "css", - js: "js", - images: "images", - }, - images: { - widths: [300, 600, 900], - formats: ["webp", "avif", "jpeg"], - additionalAttributes: "", - }, - dir: { - output: "build", - src: "src", - input: "src/include/templates", - layouts: "src/layouts", - watch: "src/**/*.{css,js,twig}", + filters: [ + // see filters/README.md + ], + functions: [ + // see functions/README.md + ], }, }; ``` -## Shortcodes - -### `mix` - -If you've generated a mixManifest and add the path to it to the `USER_OPTIONS` then it's possible to add the non hashed files to a template e.g.: - -```twig -{{ mix("/path/to/unhashed/asset.css") }} --> will result in /path/to/hashed/asset.hash-1234.css -``` - -Please provide a path relative so that `userOptions.assets.root + userOptions.base + providedPath` reaches the asset from your projects root. - -### `asset_path` - -This is a simple helper shortcode to make your defined asset path `userOptions.assets.base` available in a template: - -```twig -{{ asset_path() }} --> will result in /userOptions.assets.base like "/assets" -``` - -### `image` - -This uses `@11ty/eleventy-img` to generate responsive images in defined formats (`userOptions.images.formats`) and sizes (`userOptions.images.widths`). You could also provide certain additionalAttributes via config for lazyloading etc. - -```twig -{{ image("src", "alt", "classes") }} --> will result in a proper element with different elements for each format and defined widths -``` - -- `src`: this has to be relative to the `userOptions.assets.images` folder -- `alt`: mandatory! (`""` is possible) -- optional `classes`: `Array` - ## To be done - Proper caching -- Make features optional -- ... +- Make `twig.exports.extendTag()` possible ## Acknowledgements diff --git a/lib/filters/README.md b/lib/filters/README.md new file mode 100644 index 0000000..5661dd5 --- /dev/null +++ b/lib/filters/README.md @@ -0,0 +1,24 @@ +# Extend filter examples + +

+Eleventy Logo+ +Twig.js Logo +

+ +## Side note: + +Please read the [Twig](https://twig.symfony.com/doc/2.x/advanced.html#extending-twig) Documentation about how to extend twig properly first. Checkout the [Twig.js] documentation for `extendFunction()` as well. Also notice that functions should be used for content generation and frequent use whereas filters are more for value transformations in general. + +_...in the making_ diff --git a/lib/filters/filters.js b/lib/filters/filters.js new file mode 100644 index 0000000..05074f4 --- /dev/null +++ b/lib/filters/filters.js @@ -0,0 +1,31 @@ +const twig = require("twig"); + +/** + * This utilizes twigs extendFilter + * + * @param {import("@11ty/eleventy").UserConfig} eleventyConfig + * @param {import("../plugin").USER_OPTIONS} userOptions + * @param {import("../plugin").FILTER} filter + */ +const extendTwig = (eleventyConfig, userOptions, filter) => { + twig.extendFilter(filter.symbol, (...args) => { + return filter.callback(eleventyConfig, userOptions, ...args); + }); +}; + +/** + * Iterates over all filters and add symbols with + * their corresponding callbacks to twig + * + * @param {import("@11ty/eleventy").UserConfig} eleventyConfig + * @param {import("../plugin").USER_OPTIONS} userOptions + */ +module.exports = (eleventyConfig, userOptions) => { + (userOptions.twig?.filter || []).forEach((filter) => { + try { + extendTwig(eleventyConfig, userOptions, filter); + } catch (error) { + console.log(error); + } + }); +}; diff --git a/lib/functions/README.md b/lib/functions/README.md new file mode 100644 index 0000000..52afd0c --- /dev/null +++ b/lib/functions/README.md @@ -0,0 +1,126 @@ +# Extend function examples + +

+Eleventy Logo+ +Twig.js Logo +

+ +## Side note: + +Please read the [Twig](https://twig.symfony.com/doc/2.x/advanced.html#extending-twig) Documentation about how to extend twig properly first. Checkout the [Twig.js] documentation for `extendFunction()` as well. Also notice that functions should be used for content generation and frequent use whereas filters are more for value transformations in general. + +## `mix` + +Its most likely that your project uses hashed assets. Therefore you have to include them somehow dynamically and Eleventy as well as Twig should be aware of this. + +Lets define a function `mix()` which returns the hashed path to the given non hashed asset like: + +```twig +{{ mix("/path/to/unhased/asset.extension") }} --> will result in /path/to/hashed/asset.hash.extension +``` + +There is an example implementation in `examples/mix.js`. To activate that implementation you have to add this module to the `userOptions.twig.functions` in your Eleventy configuration file and define the `userOptions.mixManifest` as well as the `userOptions.dir.output` property optionally like: + +```js +const mix = require("@factorial/eleventy-plugin-twig/functions/examples/mix"); + +const userOptions = { + twig: { + functions: [ + symbol: "mix", + callback: mix, + ] + } + mixManifest: "mixManifest.json" // path relative to the output directory + dir: { + output: "build" // optionally, eleventy has a default output folder "_site", see https://www.11ty.dev/docs/config/#output-directory + } +} +``` + +## `asset_path` + +If you have to reference lots of assets (e.g. images etc...) or have different environments like storybook / miyagi / prod / staging / dev etc. then its sometimes helpful to define a helper function like `asset_path()` with returns the path to your assets folder in the specific environment. + +For this to work lets define a `asset_path()` function which prefixes a given path with the `userOptions.assets.base` path like: + +```twig +{{ asset_path()/subfolder/filename.extension }} --> will result in /path/to/your/assets/subfolder/filename.extension +``` + +There is an example implementation in `examples/assetPath.js`. To activate that implementation you have to add this module to the `userOptions.twig.functions` in your Eleventy configuration file and define the `userOptions.assets.base` property like: + +```js +const assetPath = require("@factorial/eleventy-plugin-twig/functions/examples/assetPath"); + +const userOptions = { + twig: { + functions: [ + symbol: "asset_path", + callback: assetPath, + ] + }, + assets: { + base: "assets" // base folder relative to the build folder; could be defined by environmetal variables for different szenarios as well + } +} +``` + +## `image` + +Eleventy comes with a great responsive image plugin called [@11ty/eleventy-img](https://github.com/11ty/eleventy-img). This autogenerates different file formats and sizes for those images included in your templates. + +Lets define a `image()` function which returns a proper `` element with `` elements: + +```twig +{{ image("src", "alt", "classes") }} --> will result in a element with different elements for each format and defined widths +``` + +First install the latest `@11ty/eleventy-img` release as a node module with `yarn`: + +```sh +yarn add --dev @11ty/eleventy-img +``` + +or `npm`: + +```sh +npm install --save-dev @11ty/eleventy-img +``` + +You can find an example implementation in `examples/image.js`. To activate that implementation you have to add this module to the `userOptions.twig.functions` in your eleventy configuration file, as well as a couple of other necessary properties like: + +```js +const image = require("@factorial/eleventy-plugin-twig/functions/examples/image"); + +const userOptions = { + twig: { + function: [ + symbol: "image", + callback: image, + ] + }, + assets: { + root: "src", // path to the root folder from projects root (e.g. src) + base: "assets", // base path for assets relative to the root folder (e.g. assets) + images: "images", // path to the image folder relative to the base (e.g. images) + }, + images: { + widths: [300, 600, 900], // those image sizes will be autogenereated / aspect-ratio will be respected + formats: ["webp", "avif"], // jpeg/avif/webp/png/gif + additionalAttributes: "" // optionally - those attributes will be added to the image element + } +} +``` diff --git a/lib/shortcodes/assetPath.js b/lib/functions/examples/assetPath.js similarity index 100% rename from lib/shortcodes/assetPath.js rename to lib/functions/examples/assetPath.js diff --git a/lib/shortcodes/image.js b/lib/functions/examples/image.js similarity index 100% rename from lib/shortcodes/image.js rename to lib/functions/examples/image.js diff --git a/lib/shortcodes/mix.js b/lib/functions/examples/mix.js similarity index 100% rename from lib/shortcodes/mix.js rename to lib/functions/examples/mix.js diff --git a/lib/functions/functions.js b/lib/functions/functions.js new file mode 100644 index 0000000..012da96 --- /dev/null +++ b/lib/functions/functions.js @@ -0,0 +1,31 @@ +const twig = require("twig"); + +/** + * This utilizes twigs extendFunction + * + * @param {import("@11ty/eleventy").UserConfig} eleventyConfig + * @param {import("../plugin").USER_OPTIONS} userOptions + * @param {import("../plugin").FUNCTION} func + */ +const extendTwig = (eleventyConfig, userOptions, func) => { + twig.extendFunction(func.symbol, (...args) => { + return func.callback(eleventyConfig, userOptions, ...args); + }); +}; + +/** + * Iterates over all functions and add symbols with + * their corresponding callbacks to twig + * + * @param {import("@11ty/eleventy").UserConfig} eleventyConfig + * @param {import("../plugin").USER_OPTIONS} userOptions + */ +module.exports = (eleventyConfig, userOptions) => { + (userOptions.twig?.functions || []).forEach((func) => { + try { + extendTwig(eleventyConfig, userOptions, func); + } catch (error) { + console.error(error); + } + }); +}; diff --git a/lib/plugin.js b/lib/plugin.js index e99b7f7..c4fca70 100644 --- a/lib/plugin.js +++ b/lib/plugin.js @@ -4,52 +4,31 @@ const twig = require("twig"); const { TemplatePath } = require("@11ty/eleventy-utils"); -const registerShortcodes = require("./shortcodes/shortcodes"); +const registerFunctions = require("./functions/functions"); +const registerFilters = require("./filters/filters"); /** - * @typedef {object} ELEVENTY_DIRECTORIES - * @property {string} input - Eleventy template path - * @property {string} output - Eleventy build path - * @property {string} [includes] - Eleventy includes path relativ to input - * @property {string} [layouts] - Eleventy separate layouts path relative to input - * @property {string} [watch] - add more watchTargets to Eleventy - */ - -/** - * @typedef {object} ASSETS - * @property {string} root - path to the root folder from projects root (e.g. src) - * @property {string} base - base path for assets relative to the root folder (e.g. assets) - * @property {string} css - path to the css folder relative to the base (e.g. css) - * @property {string} js - path to the js folder relative to the base (e.g. js) - * @property {string} images - path to the image folder relative to the base (e.g. images) - */ - -/** - * @typedef {object} IMAGES - * @property {Array} widths - those image sizes will be autogenereated / aspect-ratio will be respected - * @property {Array} formats - jpeg/avif/webp/png/gif - * @property {string} additionalAttributes - those attributes will be added to the image element + * @typedef {object} FUNCTION + * @property {string} symbol - method name for twig to register + * @property {function(import("@11ty/eleventy").UserConfig, USER_OPTIONS, ...* ):any} callback - callback which is called by twig */ /** - * @typedef {object} SHORTCODE - * @property {string} symbol - method name for twig to register - * @property {function(import("@11ty/eleventy").UserConfig, USER_OPTIONS, ...* ):any} callback - callback which is called by twig + * @typedef {object} FILTER + * @property {string} symbol - filter name for twig to register + * @property {function(import("@11ty/eleventy").UserConfig, USER_OPTIONS, ...* ):any} callback - callback which is invoked by the filter */ /** * @typedef {object} TWIG_OPTIONS - * @property {SHORTCODE[]} [shortcodes] - array of shortcodes + * @property {Function[]} [functions] - array of functions to extend twig + * @property {FILTER[]} [filter] - array of filter to extend twig * @property {boolean} [cache] - you could enable the twig cache for whatever reasons here * @property {Object} [namespaces] - define namespaces to include/extend templates more easily by "@name" */ /** * @typedef {object} USER_OPTIONS - * @property {string} mixManifest - path to the mixManifest file relative to the build folder - * @property {ASSETS} [assets] - where to find all the assets relative to the build folder - * @property {IMAGES} [images] - options for Eleventys image processing - * @property {ELEVENTY_DIRECTORIES} dir - Eleventy folder decisions * @property {TWIG_OPTIONS} [twig] - twig options */ @@ -57,39 +36,21 @@ const registerShortcodes = require("./shortcodes/shortcodes"); * Throws errors if certain required options are not part of the * userOptions object * + * @param {import("@11ty/eleventy").UserConfig} eleventyConfig * @param {USER_OPTIONS} userOptions */ -const handleErrors = (userOptions) => { +const handleErrors = (eleventyConfig, userOptions) => { const errors = []; - if (typeof userOptions !== "object" && userOptions == null) { - errors.push( - "Missing userOptions option. Please provide a proper configuration object." - ); - } - if (!userOptions.mixManifest) { - errors.push("userOptions.mixManifest is not defined."); - } - - if (!userOptions.mixManifest?.match(/^[\w-_]+.json$/)) { + if (typeof eleventyConfig !== "object" && eleventyConfig == null) { errors.push( - "userOptions.mixManifest does not provide a valid filename (for example 'foobar.json')." + "Missing eleventyConfig option. Please provide a proper configuration object." ); } - if (userOptions.mixManifest && !userOptions.assets?.base) { - errors.push( - "userOptions.mixManifest requires userOptions.assets.base to be defined." - ); - } - - if (!userOptions.dir) { - errors.push("userOptions.dir is not defined."); - } - - if (userOptions.dir && !userOptions.dir.output) { + if (typeof userOptions !== "object" && userOptions == null) { errors.push( - "userOptions.dir requires userOptions.dir.output to be defined." + "Missing userOptions option. Please provide a proper configuration object." ); } @@ -106,9 +67,10 @@ const handleErrors = (userOptions) => { * @param {USER_OPTIONS} userOptions */ module.exports = (eleventyConfig, userOptions) => { - handleErrors(userOptions); + handleErrors(eleventyConfig, userOptions); - registerShortcodes(eleventyConfig, userOptions); + registerFunctions(eleventyConfig, userOptions); + registerFilters(eleventyConfig, userOptions); twig.cache(userOptions.twig?.cache ?? false); diff --git a/lib/shortcodes/shortcodes.js b/lib/shortcodes/shortcodes.js deleted file mode 100644 index 7244baf..0000000 --- a/lib/shortcodes/shortcodes.js +++ /dev/null @@ -1,57 +0,0 @@ -const twig = require("twig"); -const image = require("./image"); -const mix = require("./mix"); -const assetPath = require("./assetPath"); - -/** - * Default shortcodes - * - * @type {import("../plugin").TWIG_OPTIONS["shortcodes"]} - */ -const defaultShortcodes = [ - { - symbol: "mix", - callback: mix, - }, - { - symbol: "asset_path", - callback: assetPath, - }, - { - symbol: "image", - callback: image, - }, -]; - -/** - * This utilize twigs extendFunction to implement the shortcodes after - * it checks if all options for a given shortcode are defined - * - * @param {import("@11ty/eleventy").UserConfig} eleventyConfig - * @param {import("../plugin").USER_OPTIONS} userOptions - * @param {import("../plugin").SHORTCODE} shortcode - */ -const extendTwig = (eleventyConfig, userOptions, shortcode) => { - twig.extendFunction(shortcode.symbol, (...args) => { - return shortcode.callback(eleventyConfig, userOptions, ...args); - }); -}; - -/** - * Iterates over all shortcodes and add symbols with - * their corresponding callbacks for twig - * - * @param {import("@11ty/eleventy").UserConfig} eleventyConfig - * @param {import("../plugin").USER_OPTIONS} userOptions - */ -module.exports = (eleventyConfig, userOptions) => { - [...defaultShortcodes, ...(userOptions.twig?.shortcodes || [])].forEach( - (shortcode) => { - try { - extendTwig(eleventyConfig, userOptions, shortcode); - } catch (error) { - console.error(error); - } - } - ); -}; diff --git a/package.json b/package.json index b3a6da1..fe170fb 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "prettier": "^2.6.0" }, "peerDependencies": { - "@11ty/eleventy-img": "^2.0.1", "twig": "^1.15.4" } }