From: Mark Otto Date: Wed, 11 Mar 2026 20:18:37 +0000 (-0700) Subject: Improve code snippets (#42148) X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=ead00c6b46570d08b4ccc72bc06eb884f26dbd61;p=thirdparty%2Fbootstrap.git Improve code snippets (#42148) * Improve code snippets * format/lint --- diff --git a/site/src/components/icons/Symbols.astro b/site/src/components/icons/Symbols.astro index f6d6351c36..9e81308970 100644 --- a/site/src/components/icons/Symbols.astro +++ b/site/src/components/icons/Symbols.astro @@ -66,6 +66,9 @@ d="M5.854 4.854a.5.5 0 1 0-.708-.708l-3.5 3.5a.5.5 0 0 0 0 .708l3.5 3.5a.5.5 0 0 0 .708-.708L2.707 8l3.147-3.146zm4.292 0a.5.5 0 0 1 .708-.708l3.5 3.5a.5.5 0 0 1 0 .708l-3.5 3.5a.5.5 0 0 1-.708-.708L13.293 8l-3.147-3.146z" > + + + diff --git a/site/src/components/shortcodes/Code.astro b/site/src/components/shortcodes/Code.astro index a376bf8d8d..e8bb904867 100644 --- a/site/src/components/shortcodes/Code.astro +++ b/site/src/components/shortcodes/Code.astro @@ -1,10 +1,9 @@ --- import fs from 'node:fs' import path from 'node:path' -import { codeToHtml } from 'shiki' import { transformerNotationDiff, transformerNotationHighlight } from '@shikijs/transformers' -import bootstrapLight from 'bootstrap-vscode-theme/themes/bootstrap-light.json' -import bootstrapDark from 'bootstrap-vscode-theme/themes/bootstrap-dark.json' +import { getConfig } from '@libs/config' +import { highlightCode } from '@libs/highlight' import { replaceConfigInText } from '@libs/remark' interface Tab { @@ -52,6 +51,10 @@ interface Props { * This takes precedence over the `code` prop. */ filePath?: string + /** + * A file path to display in the highlight toolbar instead of the language label. + */ + file?: string /** * Defines if the `` component is nested inside an `` component or not. * @default false @@ -75,6 +78,7 @@ const { code, containerClass, 'data-language': dataLanguage, + file, fileMatch, filePath, lang, @@ -83,6 +87,10 @@ const { tabs } = Astro.props +const fileUrl = file + ? `${getConfig().repo}/blob/v${getConfig().current_version}/${file}`.replaceAll('\\', '/') + : null + // Extract language from multiple possible sources (for markdown code blocks) // Priority: lang prop > data-language attribute > className pattern let detectedLang = lang || dataLanguage @@ -121,137 +129,42 @@ if (filePath && fileMatch && codeToDisplay) { codeToDisplay = matches.map(m => m[0]).join('\n\n') } -// Add line wrapper for shell languages to support shell prompts -const shouldWrapLines = detectedLang && ['bash', 'sh', 'powershell'].includes(detectedLang) - -// Transformer to ensure class name is always 'astro-code' instead of 'shiki' -const classTransformer = { - name: 'class-name-transformer', - pre(node: any) { - // Force replace all 'shiki' classes with 'astro-code' - const existingClasses = node.properties?.className || [] - const newClasses = existingClasses.map((cls: any) => { - if (typeof cls === 'string') { - return cls.replace(/shiki/g, 'astro-code') - } - return cls - }) - node.properties.className = newClasses - } -} - -const lineWrapperTransformer = { - name: 'line-wrapper', - line(node: any) { - // Wrap non-comment lines in a span with .line class for shell prompt styling - const hasOnlyComments = node.children.every((child: any) => - child.type === 'element' && - child.properties?.class && - Array.isArray(child.properties.class) && - child.properties.class.some((cls: any) => typeof cls === 'string' && cls.includes('comment')) - ) - - if (!hasOnlyComments) { - node.properties = node.properties || {} - node.properties.class = node.properties.class - ? `${node.properties.class} line` - : 'line' - } - } -} - -const transformers: any[] = [ - transformerNotationDiff(), // Supports // [!code ++] and // [!code --] notation - transformerNotationHighlight(), // Supports line highlight notation - classTransformer +const diffTransformers = [ + transformerNotationDiff(), + transformerNotationHighlight() ] -if (shouldWrapLines) { - transformers.push(lineWrapperTransformer) -} // Process tabs if provided let highlightedTabs: Array<{ label: string; code: string }> | null = null if (tabs && tabs.length > 0) { highlightedTabs = await Promise.all( - tabs.map(async (tab) => { - // Replace config placeholders in the code - const processedCode = replaceConfigInText(tab.code) - - const tabLang = tab.lang || detectedLang || 'bash' - const shouldWrapTabLines = ['bash', 'sh', 'powershell'].includes(tabLang) - - const tabTransformers: any[] = [ - transformerNotationDiff(), - transformerNotationHighlight(), - classTransformer - ] - if (shouldWrapTabLines) { - tabTransformers.push(lineWrapperTransformer) - } - - let tabHighlighted = await codeToHtml(processedCode, { - lang: tabLang, - themes: { - light: bootstrapLight, - dark: bootstrapDark - }, - transformers: tabTransformers - }) - - // Replace 'shiki' with 'astro-code' in the generated HTML - tabHighlighted = tabHighlighted.replace(/class=(["'])shiki(\s+)/g, 'class=$1astro-code$2') - tabHighlighted = tabHighlighted.replace(/class=(["'])shiki(["'])/g, 'class=$1astro-code$2') - tabHighlighted = tabHighlighted.replace(/shiki-themes/g, 'astro-code-themes') - - return { - label: tab.label, - code: tabHighlighted - } - }) + tabs.map(async (tab) => ({ + label: tab.label, + code: await highlightCode( + replaceConfigInText(tab.code), + tab.lang || detectedLang || 'bash', + diffTransformers + ) + })) ) } -let highlightedCode = codeToDisplay && detectedLang - ? await codeToHtml(codeToDisplay, { - lang: detectedLang, - themes: { - light: bootstrapLight, - dark: bootstrapDark - }, - transformers - }) +const highlightedCode = codeToDisplay && detectedLang + ? await highlightCode(codeToDisplay, detectedLang, diffTransformers) : null - -// Replace 'shiki' with 'astro-code' in the generated HTML -if (highlightedCode) { - // Replace class="shiki" or class='shiki' (preserving other classes like has-diff) - highlightedCode = highlightedCode.replace(/class=(["'])shiki(\s+)/g, 'class=$1astro-code$2') - highlightedCode = highlightedCode.replace(/class=(["'])shiki(["'])/g, 'class=$1astro-code$2') - // Replace shiki-themes if it exists - highlightedCode = highlightedCode.replace(/shiki-themes/g, 'astro-code-themes') -} --- @@ -352,6 +207,8 @@ if (highlightedCode) { ))} + ) : file ? ( + {file} ) : (
{displayLang}
)} @@ -363,9 +220,9 @@ if (highlightedCode) { )} - ))} + ) : file ? ( + {file} ) : (
{displayLang}
)}
-
diff --git a/site/src/components/shortcodes/CodeCopy.astro b/site/src/components/shortcodes/CodeCopy.astro index b583e9a3b1..888bf5294f 100644 --- a/site/src/components/shortcodes/CodeCopy.astro +++ b/site/src/components/shortcodes/CodeCopy.astro @@ -1,7 +1,5 @@ --- -import { codeToHtml } from 'shiki' -import bootstrapLight from 'bootstrap-vscode-theme/themes/bootstrap-light.json' -import bootstrapDark from 'bootstrap-vscode-theme/themes/bootstrap-dark.json' +import { highlightCode } from '@libs/highlight' interface Props { /** @@ -17,136 +15,22 @@ interface Props { const { code, lang = 'bash' } = Astro.props -// Add line wrapper for shell languages to support shell prompts -const shouldWrapLines = ['bash', 'sh', 'powershell'].includes(lang) - -const classTransformer = { - name: 'class-name-transformer', - pre(node: any) { - const existingClasses = node.properties?.className || [] - const newClasses = existingClasses.map((cls: any) => { - if (typeof cls === 'string') { - return cls.replace(/shiki/g, 'astro-code') - } - return cls - }) - node.properties.className = newClasses - } -} - -const lineWrapperTransformer = { - name: 'line-wrapper', - line(node: any) { - const hasOnlyComments = node.children.every((child: any) => - child.type === 'element' && - child.properties?.class && - Array.isArray(child.properties.class) && - child.properties.class.some((cls: any) => typeof cls === 'string' && cls.includes('comment')) - ) - - if (!hasOnlyComments) { - node.properties = node.properties || {} - node.properties.class = node.properties.class - ? `${node.properties.class} line` - : 'line' - } - } -} - -const transformers: any[] = [classTransformer] -if (shouldWrapLines) { - transformers.push(lineWrapperTransformer) -} - -let highlightedCode = await codeToHtml(code, { - lang, - themes: { - light: bootstrapLight, - dark: bootstrapDark - }, - transformers -}) - -// Replace 'shiki' with 'astro-code' -highlightedCode = highlightedCode.replace(/class=(["'])shiki(\s+)/g, 'class=$1astro-code$2') -highlightedCode = highlightedCode.replace(/class=(["'])shiki(["'])/g, 'class=$1astro-code$2') -highlightedCode = highlightedCode.replace(/shiki-themes/g, 'astro-code-themes') +const highlightedCode = await highlightCode(code, lang) ---
-
@@ -172,7 +56,7 @@ highlightedCode = highlightedCode.replace(/shiki-themes/g, 'astro-code-themes') background-color: transparent !important; } - .code-copy .btn-clipboard { + .code-copy :global([data-bd-clipboard]) { position: static; display: flex; align-items: center; diff --git a/site/src/components/shortcodes/Example.astro b/site/src/components/shortcodes/Example.astro index 7003e285f1..081737924e 100644 --- a/site/src/components/shortcodes/Example.astro +++ b/site/src/components/shortcodes/Example.astro @@ -22,6 +22,10 @@ interface Props { * The CSS class(es) to be added to the preview wrapping `div` element. */ class?: string + /** + * A file path to display in the highlight toolbar instead of the language label. + */ + file?: string /** * The preview wrapping `div` element ID. */ @@ -48,6 +52,7 @@ const { code, customMarkup, class: className, + file, id, lang = 'html', showMarkup = true, @@ -80,6 +85,6 @@ const simplifiedMarkup = sourceMarkup )} {showMarkup && ( - + )} diff --git a/site/src/components/shortcodes/JsDocs.astro b/site/src/components/shortcodes/JsDocs.astro index dfdd67aa26..de32c788b7 100644 --- a/site/src/components/shortcodes/JsDocs.astro +++ b/site/src/components/shortcodes/JsDocs.astro @@ -1,6 +1,5 @@ --- import fs from 'node:fs' -import { getConfig } from '@libs/config' import Code from '@shortcodes/Code.astro' // Prints everything between `// js-docs-start "name"` and `// js-docs-end "name"` @@ -52,20 +51,4 @@ try { } --- - -
- - {file} - -
- -
-
-
+ diff --git a/site/src/components/shortcodes/ScssDocs.astro b/site/src/components/shortcodes/ScssDocs.astro index ac89991a9b..59b3bd2f3d 100644 --- a/site/src/components/shortcodes/ScssDocs.astro +++ b/site/src/components/shortcodes/ScssDocs.astro @@ -1,6 +1,5 @@ --- import fs from 'node:fs' -import { getConfig } from '@libs/config' import Code from '@shortcodes/Code.astro' // Prints everything between `// scss-docs-start "name"` and `// scss-docs-end "name"` @@ -54,20 +53,4 @@ try { } --- - -
- - {file} - -
- -
-
-
+ diff --git a/site/src/libs/clipboard.ts b/site/src/libs/clipboard.ts new file mode 100644 index 0000000000..77850ebf66 --- /dev/null +++ b/site/src/libs/clipboard.ts @@ -0,0 +1,63 @@ +import ClipboardJS from 'clipboard' + +declare const bootstrap: any + +const btnTitle = 'Copy' + +export function initCopyButtons(selector: string, textFn: (trigger: Element) => string): ClipboardJS { + document.querySelectorAll(selector).forEach((btn) => { + bootstrap.Tooltip.getOrCreateInstance(btn, { title: btnTitle }) + }) + + const clipboard = new ClipboardJS(selector, { text: textFn }) + + clipboard.on('success', (event) => { + const useEl = event.trigger.querySelector('.bi use') + const tooltipBtn = bootstrap.Tooltip.getInstance(event.trigger) + const originalHref = useEl?.getAttribute('href') + + if (originalHref === '#check2') { + return + } + + tooltipBtn?.setContent({ '.tooltip-inner': 'Copied!' }) + + event.trigger.addEventListener( + 'hidden.bs.tooltip', + () => { + tooltipBtn?.setContent({ '.tooltip-inner': btnTitle }) + }, + { once: true } + ) + + event.clearSelection() + + if (useEl) { + useEl.setAttribute('href', '#check2') + } + + setTimeout(() => { + if (useEl && originalHref) { + useEl.setAttribute('href', originalHref) + } + }, 2000) + }) + + clipboard.on('error', (event) => { + const modifierKey = /mac/i.test(navigator.userAgent) ? '\u2318' : 'Ctrl-' + const fallbackMsg = `Press ${modifierKey}C to copy` + const tooltipBtn = bootstrap.Tooltip.getInstance(event.trigger) + + tooltipBtn?.setContent({ '.tooltip-inner': fallbackMsg }) + + event.trigger.addEventListener( + 'hidden.bs.tooltip', + () => { + tooltipBtn?.setContent({ '.tooltip-inner': btnTitle }) + }, + { once: true } + ) + }) + + return clipboard +} diff --git a/site/src/libs/highlight.ts b/site/src/libs/highlight.ts new file mode 100644 index 0000000000..bb6ae009d6 --- /dev/null +++ b/site/src/libs/highlight.ts @@ -0,0 +1,65 @@ +import { codeToHtml, type ShikiTransformer } from 'shiki' +import bootstrapLight from 'bootstrap-vscode-theme/themes/bootstrap-light.json' +import bootstrapDark from 'bootstrap-vscode-theme/themes/bootstrap-dark.json' + +const classTransformer: ShikiTransformer = { + name: 'class-name-transformer', + pre(node) { + const existingClasses = (node.properties?.className as string[]) || [] + node.properties.className = existingClasses.map((cls) => { + if (typeof cls === 'string') { + return cls.replace(/shiki/g, 'astro-code') + } + return cls + }) + } +} + +const lineWrapperTransformer: ShikiTransformer = { + name: 'line-wrapper', + line(node) { + const hasOnlyComments = node.children.every( + (child) => + child.type === 'element' && + child.properties?.class && + Array.isArray(child.properties.class) && + child.properties.class.some((cls) => typeof cls === 'string' && cls.includes('comment')) + ) + + if (!hasOnlyComments) { + node.properties = node.properties || {} + node.properties.class = node.properties.class ? `${node.properties.class} line` : 'line' + } + } +} + +function replaceShikiClasses(html: string): string { + return html + .replace(/class=(["'])shiki(\s+)/g, 'class=$1astro-code$2') + .replace(/class=(["'])shiki(["'])/g, 'class=$1astro-code$2') + .replace(/shiki-themes/g, 'astro-code-themes') +} + +export async function highlightCode( + code: string, + lang: string, + extraTransformers: ShikiTransformer[] = [] +): Promise { + const shouldWrapLines = ['bash', 'sh', 'powershell'].includes(lang) + + const transformers: ShikiTransformer[] = [...extraTransformers, classTransformer] + if (shouldWrapLines) { + transformers.push(lineWrapperTransformer) + } + + const highlighted = await codeToHtml(code, { + lang, + themes: { + light: bootstrapLight as any, + dark: bootstrapDark as any + }, + transformers + }) + + return replaceShikiClasses(highlighted) +} diff --git a/site/src/scss/_component-examples.scss b/site/src/scss/_component-examples.scss index db6466cec2..1218783bfa 100644 --- a/site/src/scss/_component-examples.scss +++ b/site/src/scss/_component-examples.scss @@ -42,7 +42,7 @@ } .highlight-toolbar { - padding-block: .375rem; + padding-block: .5rem; padding-inline-start: var(--bd-example-padding); padding-inline-end: calc(var(--bd-example-padding) - .5em); background-color: var(--bs-bg-1);