From 321f53f9539a33c780f83bb147b28f8af45d4a07 Mon Sep 17 00:00:00 2001 From: Lea Verou Date: Mon, 9 Dec 2024 13:44:14 -0500 Subject: [PATCH] Add `` and `` and use them by default Co-authored-by: Cory LaViska --- .eslintrc.cjs | 2 +- docs/.eleventy.js | 4 +- docs/_includes/sidebar.njk | 6 + docs/_utils/code-examples.js | 209 ++++++---- docs/docs/components/code-demo.md | 215 +++++++++++ docs/docs/components/viewport-demo.md | 72 ++++ docs/docs/resources/contributing.md | 21 +- src/components/code-demo/code-demo.styles.ts | 205 ++++++++++ src/components/code-demo/code-demo.ts | 359 ++++++++++++++++++ .../viewport-demo/viewport-demo.styles.ts | 106 ++++++ src/components/viewport-demo/viewport-demo.ts | 358 +++++++++++++++++ src/internal/computedStyle.ts | 14 + 12 files changed, 1502 insertions(+), 69 deletions(-) create mode 100644 docs/docs/components/code-demo.md create mode 100644 docs/docs/components/viewport-demo.md create mode 100644 src/components/code-demo/code-demo.styles.ts create mode 100644 src/components/code-demo/code-demo.ts create mode 100644 src/components/viewport-demo/viewport-demo.styles.ts create mode 100644 src/components/viewport-demo/viewport-demo.ts create mode 100644 src/internal/computedStyle.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index f74337659..716aaa259 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -132,7 +132,7 @@ module.exports = { 'no-implicit-coercion': 'off', 'no-implicit-globals': 'error', 'no-implied-eval': 'error', - 'no-invalid-this': 'error', + 'no-invalid-this': 'off', 'no-labels': 'error', 'no-lone-blocks': 'error', 'no-new': 'error', diff --git a/docs/.eleventy.js b/docs/.eleventy.js index 3a449263c..5dbb6cb39 100644 --- a/docs/.eleventy.js +++ b/docs/.eleventy.js @@ -73,7 +73,9 @@ export default function (eleventyConfig) { eleventyConfig.addPlugin(currentLink()); // Add code examples for `` blocks - eleventyConfig.addPlugin(codeExamplesPlugin()); + eleventyConfig.addPlugin(codeExamplesPlugin, { + firstOpen: true + }); // Highlight code blocks with Prism eleventyConfig.addPlugin(highlightCodePlugin()); diff --git a/docs/_includes/sidebar.njk b/docs/_includes/sidebar.njk index b39f7b405..608e86a30 100644 --- a/docs/_includes/sidebar.njk +++ b/docs/_includes/sidebar.njk @@ -69,6 +69,9 @@
  • Checkbox
  • +
  • + Code Demo +
  • Color Picker
  • @@ -216,6 +219,9 @@ +
  • + Viewport Demo +
  • Visually Hidden
  • diff --git a/docs/_utils/code-examples.js b/docs/_utils/code-examples.js index 06dbe910c..c3f1aff5b 100644 --- a/docs/_utils/code-examples.js +++ b/docs/_utils/code-examples.js @@ -1,82 +1,163 @@ 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 }) { + const attributes = { + include: 'link[rel=stylesheet]', + open + }; + + if (code.hasAttribute('data-viewport')) { + attributes.viewport = code.getAttribute('data-viewport'); + } + + 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) { + preview = `
    ${code.textContent}
    `; + } + + return `${includes} + + ${preview} + ${pre.outerHTML} + + `; + } +}; + /** * Eleventy plugin to turn `` blocks into live examples. */ -export function codeExamplesPlugin(options = {}) { +export function codeExamplesPlugin(eleventyConfig, options = {}) { options = { container: 'body', + firstOpen: true, ...options }; - return function (eleventyConfig) { - eleventyConfig.addTransform('code-examples', content => { - const doc = parse(content, { blockTextElements: { code: true } }); - const container = doc.querySelector(options.container); + const stats = { + inputPaths: {}, + outputPaths: {} + }; - if (!container) { - return content; + 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 + open: options.firstOpen ? first : false // default to first open + }; + + 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; + } } - // Look for external links - container.querySelectorAll('code.example').forEach(code => { - const pre = code.closest('pre'); - const hasButtons = !code.classList.contains('no-buttons'); - const isOpen = code.classList.contains('open') || !hasButtons; - const noEdit = code.classList.contains('no-edit'); - const id = `code-example-${uuid().slice(-12)}`; - let preview = pre.textContent; + if (code.hasAttribute('data-viewport')) { + // viewport attribute only works on the new syntax + localOptions.new = true; + } - // 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(); + const template = localOptions.new ? 'new' : 'old'; + const codeExample = parse(templates[template](pre, code, localOptions)); - const codeExample = parse(` -
    -
    - ${preview} -
    -
    - ${pre.outerHTML} -
    - ${ - hasButtons - ? ` -
    - - - ${ - noEdit - ? '' - : ` - - ` - } - - ` - : '' - } -
    -
    - `); - - pre.replaceWith(codeExample); - }); - - return doc.toString(); + pre.replaceWith(codeExample); }); - }; + + return doc.toString(); + }); } diff --git a/docs/docs/components/code-demo.md b/docs/docs/components/code-demo.md new file mode 100644 index 000000000..a136d3ef3 --- /dev/null +++ b/docs/docs/components/code-demo.md @@ -0,0 +1,215 @@ +--- +title: Code Demo +description: Code demos can be used to render code examples as inline live demos. +layout: component +--- + +```html {.example} + +
    
    +    <button>Click me!</button>
    +    <wa-button>Click me!</wa-button>
    +  
    +
    +``` + +This component is used right here in the docs to render code examples. + + + + +Do not render untrusted content in a `` element. +This component renders the content as HTML, which introduces XSS vulnerabilities if used with untrusted content. + + + +## Examples + +### Open by default + +```html {.example} + +
    
    +    <button>Click me!</button>
    +    <wa-button>Click me!</wa-button>
    +  
    +
    +``` + +### Custom previews + +In some cases you may want to preprocess the code displayed, for example to sanitize HTML, remove irrelevant elements or attributes, fix whitespace, or do server-side rendering (SSR). +For these cases, you can slot in a custom preview: + +```html {.example} + + Click me! +
    
    +    <button>Click me!</button>
    +  
    +
    +``` + +Note that this means the preview will be in the light DOM, and can conflict with other things on the page. +To only render the custom preview within the component’s shadow DOM, or to display raw text, you can wrap it in a `