diff --git a/docs/_plugins/shortcodes.cjs b/docs/_plugins/shortcodes.cjs
index dbda12db20..2b4f1a8c15 100644
--- a/docs/_plugins/shortcodes.cjs
+++ b/docs/_plugins/shortcodes.cjs
@@ -1,278 +1,28 @@
-// @ts-check
-const { readFile } = require('node:fs/promises');
-const Image = require('@11ty/eleventy-img');
-const sizeOf = require('image-size');
-const path = require('path');
+const Playground = require('./shortcodes/playground.cjs');
+const RepoStatus = require('./shortcodes/repoStatus.cjs');
+const RenderInstallation = require('./shortcodes/renderInstallation.cjs');
+const ExampleImage = require('./shortcodes/example.cjs');
+const Cta = require('./shortcodes/cta.cjs');
+const Alert = require('./shortcodes/alert.cjs');
+const Section = require('./shortcodes/section.cjs');
+const Demo = require('./shortcodes/demo.cjs');
+
+/** @typedef {import('@patternfly/pfe-tools/11ty/DocsPage').DocsPage} DocsPage */
+
+/**
+ * @typedef {object} EleventyContext
+ * @property {object} ctx
+ * @property {object} page
+ * @property {object} eleventy
+ */
-/** @param {import('@11ty/eleventy/src/UserConfig')} eleventyConfig */
module.exports = function(eleventyConfig) {
- /** Render a Call to Action */
- eleventyConfig.addPairedShortcode('cta', async function(content, {
- href = '#',
- target = null,
- } = {}) {
- const innerHTML = await eleventyConfig.javascriptFunctions?.renderTemplate(content, 'md');
- return /* html */`${innerHTML.replace(/^(.*)<\/p>$/m, '$1')}
`;
- });
-
- /** Render a Red Hat Alert */
- eleventyConfig.addPairedShortcode('alert', function(content, {
- state = 'info',
- title = 'Note:',
- style,
- level = 3,
- } = {}) {
- return /* html */`
-
-
- ${title}
-
- ${content}
-
-
-
-`;
- });
-
- /**
- * Section macro
- * Creates a section of the page with a heading
- *
- * @param {object} options
- * @param options.headline Text to go in the heading
- * @param options.palette Palette to apply, e.g. lightest, light see components/_section.scss
- * @param options.headingLevel The heading level, defaults to 2
- */
- eleventyConfig.addPairedShortcode('section', function(content, {
- headline,
- palette = 'default',
- headingLevel = '2',
- style,
- class: className,
- } = {}) {
- const slugify = eleventyConfig.getFilter('slugify');
- return /* html*/`
-${!headline ? '' : `
-
- ${headline}`}
-
-${content}
-
-
-
-`;
- });
-
- /**
- * Example
- * An example image or component
- *
- * @param {object} options
- * @param {string} options.alt Image alt text
- * @param {string} options.src Image url
- * @param {number} [options.width] width of the img
- * @param {string} [options.style] styles for the wrapper
- * @param {string} [options.wrapperClass] class names for container element
- * @param {string} [options.headline] Text to go in the heading
- * @param {string} [options.palette='light'] Palette to apply, e.g. lightest, light see components/_section.scss
- * @param {2|3|4|5|6} [headingLevel=3] The heading level
- */
- eleventyConfig.addShortcode('example', /** @this{EleventyContext}*/ async function({
- alt = '',
- src = '',
- style,
- width,
- headline,
- wrapperClass,
- palette = 'light',
- headingLevel = '3'
- } = {}) {
- const { page } = this.ctx || {};
- const srcHref = path.join('_site', page?.url, src);
- const slugify = eleventyConfig.getFilter('slugify');
- const imgStyle = width && `--example-img-max-width:${width}px;`;
- const imgDir = srcHref.replace(/\/[^/]+$/, '/');
- const urlPath = imgDir.replace(/^_site/, '');
- const outputDir = `./${imgDir}`;
- /* get default 2x width */
- const size = url => {
- try {
- return sizeOf(url);
- } catch (error) {
- return false;
- }
- };
- const width2x = size(srcHref)?.width;
- const width1x = width2x ? width2x / 2 : false;
- /* determine filenames of generated images */
- const filenameFormat = (id, src, width, format, options) => {
- const extension = path.extname(src);
- const name = path.basename(src, extension);
- // rewrite the default 2X image since we don't need two copies
- return width === width2x ? `${name}.${format}` : `${name}-${width}w.${format}`;
- };
- /* generate images and return metadata */
- const metadata = async url => {
- try {
- return await Image(srcHref, {
- widths: [width1x, width2x],
- formats: ['auto'],
- filenameFormat: filenameFormat,
- urlPath: urlPath,
- outputDir: outputDir
- });
- } catch (error) {
- return false;
- }
- };
- const img = await metadata(srcHref);
- const sizes = `(max-width: ${width1x}px) ${width1x}px, ${width2x}px`;
-
- const imgAttributes = {
- alt,
- sizes,
- style: [`width:${width1x}px;height:auto`, imgStyle].join(';'),
- loading: 'lazy',
- decoding: 'async',
- };
- /**
- */
- return /* html */`
-
${!headline ? '' : `
-
-
${headline}`}
- ${!img ? '' : Image.generateHTML(img, imgAttributes)}
-
`;
- });
-
- /**
- * Demo
- * A live component demo
- *
- * @param headline (Optional) Text to go in the heading
- * @param palette Palette to apply, e.g. lightest, light see components/_section.scss
- * @param headingLevel The heading level, defaults to 3
- */
- eleventyConfig.addPairedShortcode('demo', function demoShortcode(content, { headline, palette = 'light', headingLevel = '3' } = {}) {
- const slugify = eleventyConfig.getFilter('slugify');
- return /* html*/`
-
-${!headline ? '' : `
- ${headline}`}
-
-${content}
-
-
- View Code
-
-~~~html
-${content.trim()}
-~~~
-
-
-
-
-`;
- });
-
- /**
- * Reads component status data from global data (see above) and outputs a table for each component
- */
- eleventyConfig.addShortcode('repoStatus', /** @this {EleventyContext} */ function({ heading = 'Repo status', type = 'Pattern' } = {}) {
- const allStatuses = this.ctx.repoStatus ?? this.ctx._?.repoStatus ?? [];
- const title = this.ctx.title ?? this.ctx._?.title;
- const [header, ...repoStatus] = allStatuses;
- if (Array.isArray(header)) {
- header[0] = type;
- }
- const bodyRows = repoStatus.filter(([rowHeader]) =>
- rowHeader.replace(/^([\w\s]+) - (.*)$/, '$1') === title);
- if (!Array.isArray(bodyRows) || !bodyRows.length) {
- return '';
- } else {
- return /* html*/`
-
-
-
- ${heading}
- Learn more about our various code repos by visiting this page.
-
-
-
- ${header.map(x => `
- ${x} | `.trim()).join('\n').trim()}
-
-
- ${bodyRows.map(([title, ...columns]) => `
-
- ${title} |
- ${columns.map(x => `${x === 'x' ? '✓' : ''} | `.trim()).join('\n').trim()}
-
`.trim()).join('\n').trim()}
-
-
-
-
-
-`;
- }
- });
-
- eleventyConfig.addPairedNunjucksAsyncShortcode('playground', /** @this{EleventyContext}*/async function playground(_, { tagName } = {}) {
- /**
- * NB: since the data for this shortcode is no a POJO,
- * but a DocsPage instance, 11ty assigns it to this.ctx._
- * @see https://github.com/11ty/eleventy/blob/bf7c0c0cce1b2cb01561f57fdd33db001df4cb7e/src/Plugins/RenderPlugin.js#L89-L93
- * @type {import('@patternfly/pfe-tools/11ty/DocsPage').DocsPage}
- */
- const docsPage = this.ctx._;
- tagName ??= docsPage?.tagName;
- const { getPfeConfig } = await import('@patternfly/pfe-tools/config.js');
- const options = getPfeConfig();
- const { filePath } =
- docsPage.manifest
- .getDemoMetadata(tagName, options)
- ?.find(x => x.url === `https://ux.redhat.com/elements/${x.slug}/demo/`) ?? {};
- return /* html */`
-
-
-${!filePath ? '' : `
-
-~~~html
-${await readFile(filePath, 'utf8')}
-~~~`}
-
-`;
- });
-
- eleventyConfig.addPairedShortcode('renderInstallation', function(content) {
- /**
- * NB: since the data for this shortcode is no a POJO,
- * but a DocsPage instance, 11ty assigns it to this.ctx._
- * @see https://github.com/11ty/eleventy/blob/bf7c0c0cce1b2cb01561f57fdd33db001df4cb7e/src/Plugins/RenderPlugin.js#L89-L93
- * @type {import('@patternfly/pfe-tools/11ty/DocsPage').DocsPage}
- */
- const docsPage = this.ctx._;
- return /* html */`
-
-
- Installation
${!docsPage.manifest?.packageJson ? '' : `
-
-~~~shell
-npm install ${docsPage.manifest.packageJson.name}
-~~~`}
-
-~~~js
-import '@rhds/elements/${docsPage.tagName}/${docsPage.tagName}.js';
-~~~
-
-${content ?? ''}
-
-
-
- `;
- });
+ eleventyConfig.addPlugin(RepoStatus);
+ eleventyConfig.addPlugin(Playground);
+ eleventyConfig.addPlugin(RenderInstallation);
+ eleventyConfig.addPlugin(ExampleImage);
+ eleventyConfig.addPlugin(Cta);
+ eleventyConfig.addPlugin(Alert);
+ eleventyConfig.addPlugin(Section);
+ eleventyConfig.addPlugin(Demo);
};
diff --git a/docs/_plugins/shortcodes/alert.cjs b/docs/_plugins/shortcodes/alert.cjs
new file mode 100644
index 0000000000..45ea8758cd
--- /dev/null
+++ b/docs/_plugins/shortcodes/alert.cjs
@@ -0,0 +1,27 @@
+const { attrMap } = require('./helpers.cjs');
+
+module.exports = function(eleventyConfig) {
+ eleventyConfig.addPairedShortcode('alert',
+ /**
+ * Render a Red Hat Alert
+ * @param {string} content
+ * @param {Record} attrs
+ */
+ function alert(content, {
+ state = 'info',
+ title = 'Note:',
+ style = null,
+ level = 3,
+ } = {}) {
+ return /* html */`
+
+
+ ${title}
+
+ ${content}
+
+
+
+`;
+ });
+};
diff --git a/docs/_plugins/shortcodes/cta.cjs b/docs/_plugins/shortcodes/cta.cjs
new file mode 100644
index 0000000000..40cdeb1600
--- /dev/null
+++ b/docs/_plugins/shortcodes/cta.cjs
@@ -0,0 +1,18 @@
+const { attrMap } = require('./helpers.cjs');
+
+module.exports = function(eleventyConfig) {
+ eleventyConfig.addPairedShortcode('cta',
+ /**
+ * Render a Call to Action
+ * @param {string} content
+ * @param {Record} attrs
+ */
+ async function cta(content, {
+ href = '#',
+ target = null,
+ } = {}) {
+ const innerHTML = await eleventyConfig.javascriptFunctions?.renderTemplate(content, 'md');
+ const linkText = innerHTML.replace(/^(.*)<\/p>$/m, '$1').trim();
+ return /* html */`${linkText}`;
+ });
+};
diff --git a/docs/_plugins/shortcodes/demo.cjs b/docs/_plugins/shortcodes/demo.cjs
new file mode 100644
index 0000000000..8ecb8fb009
--- /dev/null
+++ b/docs/_plugins/shortcodes/demo.cjs
@@ -0,0 +1,37 @@
+module.exports = function(eleventyConfig) {
+ eleventyConfig.addPairedShortcode('demo',
+ /**
+ * Demo
+ * A live component demo
+ * @param {string} content
+ * @param {object} options
+ * @param {string} options.headline (Optional) Text to go in the heading
+ * @param {string} options.palette Palette to apply, e.g. lightest, light see components/_section.scss
+ * @param {string} options.headingLevel The heading level, defaults to 3
+ */
+ function demoShortcode(content, {
+ headline = null,
+ palette = 'light',
+ headingLevel = '3',
+ } = {}) {
+ const slugify = eleventyConfig.getFilter('slugify');
+ return /* html*/`
+
+
${!headline ? '' : `
+ ${headline}`}
+
+${content}
+
+
+ View Code
+
+~~~html
+${content.trim()}
+~~~
+
+
+
+
+`;
+ });
+};
diff --git a/docs/_plugins/shortcodes/example.cjs b/docs/_plugins/shortcodes/example.cjs
new file mode 100644
index 0000000000..4e61d5007b
--- /dev/null
+++ b/docs/_plugins/shortcodes/example.cjs
@@ -0,0 +1,101 @@
+// @ts-check
+
+const { attrMap } = require('./helpers.cjs');
+
+/** @typedef {import('../shortcodes.cjs').EleventyContext} EleventyContext */
+
+const { promisify } = require('node:util');
+const Image = require('@11ty/eleventy-img');
+const sizeOf = promisify(/** @type{import('image-size').default}*/(/** @type{unknown}*/(require('image-size') )));
+const path = require('path');
+
+/**
+ * generate images and return metadata
+ * @param {Image.ImageSource} url
+ * @param {'auto' | number | null} width1x
+ * @param {'auto' | number | null} width2x
+ * @param {string} outputDir
+ * @param {string} urlPath
+ */
+async function getImg(url, width1x, width2x, outputDir, urlPath) {
+ try {
+ return await Image(url, {
+ urlPath,
+ outputDir,
+ formats: ['auto'],
+ widths: [width1x, width2x],
+ filenameFormat(id, src, width, format) {
+ const extension = path.extname(src);
+ const name = path.basename(src, extension);
+ // rewrite the default 2X image since we don't need two copies
+ return width === width2x ? `${name}.${format}` : `${name}-${width}w.${format}`;
+ },
+ });
+ } catch (error) {
+ return false;
+ }
+}
+
+/**
+ * get default 2x width
+ * @param {string} url
+ */
+async function getImgSize(url) {
+ try {
+ const size = await sizeOf(url);
+ return size;
+ } catch (error) {
+ return null;
+ }
+}
+
+/** @param {import('@11ty/eleventy/src/UserConfig')} eleventyConfig */
+module.exports = function(eleventyConfig) {
+ eleventyConfig.addShortcode('example',
+ /**
+ * Example
+ * An example image or component
+ *
+ * @param {object} options
+ * @param {string} [options.alt] Image alt text
+ * @param {string} [options.src] Image url
+ * @param {number} [options.width] width of the img
+ * @param {string} [options.style] styles for the wrapper
+ * @param {string} [options.wrapperClass] class names for container element
+ * @param {string} [options.headline] Text to go in the heading
+ * @param {string} [options.palette='light'] Palette to apply, e.g. lightest, light see components/_section.scss
+ * @param {2|3|4|5|6} [options.headingLevel=3] The heading level
+ * @this {EleventyContext}
+ */
+ async function example({
+ alt = '',
+ src = '',
+ style = '',
+ headline = '',
+ wrapperClass = '',
+ palette = 'light',
+ headingLevel = 3,
+ } = {}) {
+ const { page } = this.ctx || {};
+ const srcHref = path.join('_site', page?.url, src);
+ const slugify = eleventyConfig.getFilter('slugify');
+ const imgDir = srcHref.replace(/\/[^/]+$/, '/');
+ const urlPath = imgDir.replace(/^_site/, '');
+ const outputDir = `./${imgDir}`;
+ const width2x = (await getImgSize(srcHref))?.width ?? null;
+ const width1x = width2x && width2x / 2;
+ const styles = [`width:${width1x}px`, `height:auto`].join(';');
+ const loading = 'lazy';
+ const decoding = 'async';
+ const img = await getImg(srcHref, width1x, width2x, outputDir, urlPath);
+ const sizes = `(max-width: ${width1x}px) ${width1x}px, ${width2x}px`;
+ const classes = `example example--palette-${palette} ${wrapperClass ?? ''}`;
+
+ return /* html */`
+${!headline ? '' : `
+
+
${headline}`}
+ ${!img ? '' : Image.generateHTML(img, { alt, sizes, style: styles, loading, decoding })}
+
`;
+ });
+};
diff --git a/docs/_plugins/shortcodes/helpers.cjs b/docs/_plugins/shortcodes/helpers.cjs
new file mode 100644
index 0000000000..521c691a33
--- /dev/null
+++ b/docs/_plugins/shortcodes/helpers.cjs
@@ -0,0 +1,22 @@
+
+/**
+ * @param {string} k
+ * @param {unknown} v
+ */
+function getAttrMapValue(k, v) {
+ switch (k) {
+ case 'style': return typeof v === 'string' ? v.replace('"', '\\"') : v;
+ default: return v;
+ }
+}
+
+/**
+ * @param {Record} attrObj object map of attribute name to attribute value. `null` values will be removed.
+ * @returns {string} html attributes
+ */
+exports.attrMap = function attrMap(attrObj) {
+ return Object.entries(attrObj)
+ .filter(([, v]) => v != null)
+ .map(([k, v]) => `${k}="${getAttrMapValue(k, v)}"`)
+ .join(' ');
+};
diff --git a/docs/_plugins/shortcodes/playground.cjs b/docs/_plugins/shortcodes/playground.cjs
new file mode 100644
index 0000000000..c77e57eaa7
--- /dev/null
+++ b/docs/_plugins/shortcodes/playground.cjs
@@ -0,0 +1,43 @@
+const { readFile } = require('node:fs/promises');
+
+/** @typedef {import('@patternfly/pfe-tools/11ty/DocsPage').DocsPage} DocsPage */
+
+/**
+ * @this {EleventyContext}
+ * @param {string} _
+ * @param {{ tagName?: string | null }} opts
+ */
+async function playground(_, {
+ tagName = null,
+} = {}) {
+ /**
+ * NB: since the data for this shortcode is no a POJO,
+ * but a DocsPage instance, 11ty assigns it to this.ctx._
+ * @see https://github.com/11ty/eleventy/blob/bf7c0c0cce1b2cb01561f57fdd33db001df4cb7e/src/Plugins/RenderPlugin.js#L89-L93
+ * @type {DocsPage}
+ */
+ const docsPage = this.ctx._;
+ tagName ??= docsPage?.tagName;
+ const { getPfeConfig } = await import('@patternfly/pfe-tools/config.js');
+ const options = getPfeConfig();
+ const { filePath } =
+ docsPage.manifest
+ .getDemoMetadata(tagName, options)
+ ?.find(x => x.url === `https://ux.redhat.com/elements/${x.slug}/demo/`) ?? {};
+ const content = filePath && await readFile(filePath, 'utf8');
+ return /* html*/`
+
+
+
+${!content ? '' : `
+
+~~~html
+${content}
+~~~`}
+
+`;
+}
+
+module.exports = function(eleventyConfig) {
+ eleventyConfig.addPairedShortcode('playground', playground);
+};
diff --git a/docs/_plugins/shortcodes/renderInstallation.cjs b/docs/_plugins/shortcodes/renderInstallation.cjs
new file mode 100644
index 0000000000..a28d35fe6e
--- /dev/null
+++ b/docs/_plugins/shortcodes/renderInstallation.cjs
@@ -0,0 +1,36 @@
+/** @typedef {import('@patternfly/pfe-tools/11ty/DocsPage').DocsPage} DocsPage */
+
+/**
+ * @param {string} content
+ */
+function renderInstallation(content) {
+ /**
+ * NB: since the data for this shortcode is no a POJO,
+ * but a DocsPage instance, 11ty assigns it to this.ctx._
+ * @see https://github.com/11ty/eleventy/blob/bf7c0c0cce1b2cb01561f57fdd33db001df4cb7e/src/Plugins/RenderPlugin.js#L89-L93
+ * @type {DocsPage}
+ */
+ const docsPage = this.ctx._;
+ return /* html */`
+
+
+ Installation
${!docsPage.manifest?.packageJson ? '' : `
+
+~~~shell
+npm install ${docsPage.manifest.packageJson.name}
+~~~`}
+
+~~~js
+import '@rhds/elements/${docsPage.tagName}/${docsPage.tagName}.js';
+~~~
+
+${content ?? ''}
+
+
+
+ `;
+}
+
+module.exports = function(eleventyConfig) {
+ eleventyConfig.addPairedShortcode('renderInstallation', renderInstallation);
+};
diff --git a/docs/_plugins/shortcodes/repoStatus.cjs b/docs/_plugins/shortcodes/repoStatus.cjs
new file mode 100644
index 0000000000..ad1fcc5a92
--- /dev/null
+++ b/docs/_plugins/shortcodes/repoStatus.cjs
@@ -0,0 +1,47 @@
+/**
+ * Reads component status data from global data (see above) and outputs a table for each component
+ * @this {EleventyContext}
+ */
+function repoStatus({ heading = 'Repo status', type = 'Pattern' } = {}) {
+ /** @type {string[][]} */
+ const allStatuses = this.ctx.repoStatus ?? this.ctx._?.repoStatus ?? [];
+ const title = this.ctx.title ?? this.ctx._?.title;
+ const [header, ...repoStatus] = allStatuses;
+ if (Array.isArray(header)) {
+ header[0] = type;
+ }
+ const bodyRows = repoStatus.filter(([rowHeader]) =>
+ rowHeader.replace(/^([\w\s]+) - (.*)$/, '$1') === title);
+ if (!Array.isArray(bodyRows) || !bodyRows.length) {
+ return '';
+ } else {
+ return /* html*/`
+
+
+
+ ${heading}
+ Learn more about our various code repos by visiting this page.
+
+
+
+ ${header.map(x => `
+ ${x} | `.trim()).join('\n').trim()}
+
+
+ ${bodyRows.map(([title, ...columns]) => `
+
+ ${title} |
+ ${columns.map(x => `${x === 'x' ? '✓' : ''} | `.trim()).join('\n').trim()}
+
`.trim()).join('\n').trim()}
+
+
+
+
+
+ `;
+ }
+}
+
+module.exports = function(eleventyConfig) {
+ eleventyConfig.addShortcode('repoStatus', repoStatus);
+};
diff --git a/docs/_plugins/shortcodes/section.cjs b/docs/_plugins/shortcodes/section.cjs
new file mode 100644
index 0000000000..6de9a487a7
--- /dev/null
+++ b/docs/_plugins/shortcodes/section.cjs
@@ -0,0 +1,35 @@
+const { attrMap } = require('./helpers.cjs');
+
+module.exports = function(eleventyConfig) {
+ eleventyConfig.addPairedShortcode('section',
+ /**
+ * Section macro
+ * Creates a section of the page with a heading
+ *
+ * @param {string} content
+ * @param {object} options
+ * @param {string} options.headline Text to go in the heading
+ * @param {string} options.palette Palette to apply, e.g. lightest, light see components/_section.scss
+ * @param {string} options.headingLevel The heading level, defaults to 2
+ */
+ function(content, {
+ headline = null,
+ palette = 'default',
+ headingLevel = '2',
+ style = null,
+ class: className = null,
+ } = {}) {
+ const slugify = eleventyConfig.getFilter('slugify');
+ const classes = `section section--palette-${palette} ${className ?? ''} container`;
+ return /* html*/`
+${!headline ? '' : `
+
+ ${headline}`}
+
+${content}
+
+
+
+`;
+ });
+};