import { parse } from 'node-html-parser'; import { v4 as uuid } from 'uuid'; const templates = { old(pre, code, { open, buttons, edit }) { const id = `code-example-${uuid().slice(-12)}`; let preview = code.textContent; // Run preview scripts as modules to prevent collisions const root = parse(preview, { blockTextElements: { script: true } }); root.querySelectorAll('script').forEach(script => script.setAttribute('type', 'module')); preview = root.toString(); return `
${preview}
${pre.outerHTML}
${ buttons ? `
${ edit ? ` ` : '' } ` : '' }
`; }, new(pre, code, { open, first, attributes }) { attributes = { open, include: `link[rel=stylesheet][href^='/dist/']`, ...attributes, }; const attributesString = Object.entries(attributes) .map(([key, value]) => { if (value === true) { return key; } if (value === false || value === null) { return ''; } return `${key}="${value}"`; }) .join(' '); let includes = ''; if (first) { includes = ` `; } let preview = ''; if (attributes.viewport === undefined) { // Slot in pre-rendered preview preview = `
${code.textContent}
`; // Run preview scripts as modules to prevent collisions const root = parse(preview, { blockTextElements: { script: true } }); root.querySelectorAll('script').forEach(script => script.setAttribute('type', 'module')); preview = root.toString(); } return `${includes} ${preview} ${pre.outerHTML} `; }, }; /** * Eleventy plugin to turn `` blocks into live examples. */ export function codeExamplesPlugin(eleventyConfig, options = {}) { const defaultOptions = { container: 'body', defaultOpen: () => false, }; options = { ...defaultOptions, ...options }; const stats = { inputPaths: {}, outputPaths: {}, }; eleventyConfig.addTransform('code-examples', function (content) { const { inputPath, outputPath } = this.page; const doc = parse(content, { blockTextElements: { code: true } }); const container = doc.querySelector(options.container); if (!container) { return content; } // Look for external links container.querySelectorAll('code.example').forEach(code => { stats.inputPaths[inputPath] ??= 0; stats.outputPaths[outputPath] ??= 0; stats.inputPaths[inputPath]++; stats.outputPaths[outputPath]++; const pre = code.closest('pre'); const first = stats.inputPaths[inputPath] === 1; const localOptions = { ...options, first, // Modifier defaults edit: true, buttons: true, new: true, // comment this line to default back to the old demos attributes: {}, }; for (const prop of ['new', 'open', 'buttons', 'edit']) { if (code.classList.contains(prop)) { localOptions[prop] = true; } else if (code.classList.contains(`no-${prop}`)) { localOptions[prop] = false; } } for (const attribute of ['viewport', 'include']) { if (code.hasAttribute(attribute)) { localOptions.attributes[attribute] = code.getAttribute(attribute); code.removeAttribute(attribute); } } if (Object.keys(localOptions.attributes).length > 0) { // attributes only work on the new syntax localOptions.new = true; } if (localOptions.open === undefined) { if (localOptions.defaultOpen === true) { localOptions.open = localOptions.defaultOpen; } else if (typeof localOptions.defaultOpen === 'function') { localOptions.open = localOptions.defaultOpen(code, { pre, inputPathIndex: stats.inputPaths[inputPath], outputPathIndex: stats.outputPaths[outputPath], }); } } const template = localOptions.new ? 'new' : 'old'; const codeExample = parse(templates[template](pre, code, localOptions)); pre.replaceWith(codeExample); }); return doc.toString(); }); }