From 1dcccecbac572711a336188d26df360bb02ec5bd Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Thu, 1 Jun 2023 15:25:27 +0300 Subject: [PATCH] docs: fix cta and alert shortcodes (#1015) refactors and fixes the cta and alert shortcodes removes the `width` attr from example shortcode --- docs/_plugins/shortcodes.cjs | 300 ++---------------- docs/_plugins/shortcodes/alert.cjs | 27 ++ docs/_plugins/shortcodes/cta.cjs | 18 ++ docs/_plugins/shortcodes/demo.cjs | 37 +++ docs/_plugins/shortcodes/example.cjs | 101 ++++++ docs/_plugins/shortcodes/helpers.cjs | 22 ++ docs/_plugins/shortcodes/playground.cjs | 43 +++ .../shortcodes/renderInstallation.cjs | 36 +++ docs/_plugins/shortcodes/repoStatus.cjs | 47 +++ docs/_plugins/shortcodes/section.cjs | 35 ++ 10 files changed, 391 insertions(+), 275 deletions(-) create mode 100644 docs/_plugins/shortcodes/alert.cjs create mode 100644 docs/_plugins/shortcodes/cta.cjs create mode 100644 docs/_plugins/shortcodes/demo.cjs create mode 100644 docs/_plugins/shortcodes/example.cjs create mode 100644 docs/_plugins/shortcodes/helpers.cjs create mode 100644 docs/_plugins/shortcodes/playground.cjs create mode 100644 docs/_plugins/shortcodes/renderInstallation.cjs create mode 100644 docs/_plugins/shortcodes/repoStatus.cjs create mode 100644 docs/_plugins/shortcodes/section.cjs 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*/` - -
- - -

Learn more about our various code repos by visiting this page.

-
- - - ${header.map(x => ` - `.trim()).join('\n').trim()} - - - ${bodyRows.map(([title, ...columns]) => ` - - - ${columns.map(x => ``.trim()).join('\n').trim()} - `.trim()).join('\n').trim()} - -
${x}
${title}${x === 'x' ? '✓' : ''}
-
-
- -`; - } - }); - - 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*/` + +
+ + +

Learn more about our various code repos by visiting this page.

+
+ + + ${header.map(x => ` + `.trim()).join('\n').trim()} + + + ${bodyRows.map(([title, ...columns]) => ` + + + ${columns.map(x => ``.trim()).join('\n').trim()} + `.trim()).join('\n').trim()} + +
${x}
${title}${x === 'x' ? '✓' : ''}
+
+
+ + `; + } +} + +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} + +
+ +`; + }); +};