Skip to content

Commit ead00c6

Browse files
authored
Improve code snippets (#42148)
* Improve code snippets * format/lint
1 parent eed1a6c commit ead00c6

9 files changed

Lines changed: 195 additions & 350 deletions

File tree

site/src/components/icons/Symbols.astro

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@
6666
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"
6767
></path>
6868
</symbol>
69+
<symbol id="copy" viewBox="0 0 16 16">
70+
<path fill-rule="evenodd" d="M4 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1zM2 5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-1h1v1a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h1v1z"/>
71+
</symbol>
6972
<symbol id="envelope" viewBox="0 0 16 16">
7073
<path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1zm13 2.383-4.708 2.825L15 11.105zm-.034 6.876-5.64-3.471L8 9.583l-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h12a1 1 0 0 0 .966-.741M1 11.105l4.708-2.897L1 5.383z"/>
7174
</symbol>

site/src/components/shortcodes/Code.astro

Lines changed: 46 additions & 187 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
---
22
import fs from 'node:fs'
33
import path from 'node:path'
4-
import { codeToHtml } from 'shiki'
54
import { transformerNotationDiff, transformerNotationHighlight } from '@shikijs/transformers'
6-
import bootstrapLight from 'bootstrap-vscode-theme/themes/bootstrap-light.json'
7-
import bootstrapDark from 'bootstrap-vscode-theme/themes/bootstrap-dark.json'
5+
import { getConfig } from '@libs/config'
6+
import { highlightCode } from '@libs/highlight'
87
import { replaceConfigInText } from '@libs/remark'
98
109
interface Tab {
@@ -52,6 +51,10 @@ interface Props {
5251
* This takes precedence over the `code` prop.
5352
*/
5453
filePath?: string
54+
/**
55+
* A file path to display in the highlight toolbar instead of the language label.
56+
*/
57+
file?: string
5558
/**
5659
* Defines if the `<Code>` component is nested inside an `<Example>` component or not.
5760
* @default false
@@ -75,6 +78,7 @@ const {
7578
code,
7679
containerClass,
7780
'data-language': dataLanguage,
81+
file,
7882
fileMatch,
7983
filePath,
8084
lang,
@@ -83,6 +87,10 @@ const {
8387
tabs
8488
} = Astro.props
8589
90+
const fileUrl = file
91+
? `${getConfig().repo}/blob/v${getConfig().current_version}/${file}`.replaceAll('\\', '/')
92+
: null
93+
8694
// Extract language from multiple possible sources (for markdown code blocks)
8795
// Priority: lang prop > data-language attribute > className pattern
8896
let detectedLang = lang || dataLanguage
@@ -121,219 +129,66 @@ if (filePath && fileMatch && codeToDisplay) {
121129
codeToDisplay = matches.map(m => m[0]).join('\n\n')
122130
}
123131
124-
// Add line wrapper for shell languages to support shell prompts
125-
const shouldWrapLines = detectedLang && ['bash', 'sh', 'powershell'].includes(detectedLang)
126-
127-
// Transformer to ensure class name is always 'astro-code' instead of 'shiki'
128-
const classTransformer = {
129-
name: 'class-name-transformer',
130-
pre(node: any) {
131-
// Force replace all 'shiki' classes with 'astro-code'
132-
const existingClasses = node.properties?.className || []
133-
const newClasses = existingClasses.map((cls: any) => {
134-
if (typeof cls === 'string') {
135-
return cls.replace(/shiki/g, 'astro-code')
136-
}
137-
return cls
138-
})
139-
node.properties.className = newClasses
140-
}
141-
}
142-
143-
const lineWrapperTransformer = {
144-
name: 'line-wrapper',
145-
line(node: any) {
146-
// Wrap non-comment lines in a span with .line class for shell prompt styling
147-
const hasOnlyComments = node.children.every((child: any) =>
148-
child.type === 'element' &&
149-
child.properties?.class &&
150-
Array.isArray(child.properties.class) &&
151-
child.properties.class.some((cls: any) => typeof cls === 'string' && cls.includes('comment'))
152-
)
153-
154-
if (!hasOnlyComments) {
155-
node.properties = node.properties || {}
156-
node.properties.class = node.properties.class
157-
? `${node.properties.class} line`
158-
: 'line'
159-
}
160-
}
161-
}
162-
163-
const transformers: any[] = [
164-
transformerNotationDiff(), // Supports // [!code ++] and // [!code --] notation
165-
transformerNotationHighlight(), // Supports line highlight notation
166-
classTransformer
132+
const diffTransformers = [
133+
transformerNotationDiff(),
134+
transformerNotationHighlight()
167135
]
168-
if (shouldWrapLines) {
169-
transformers.push(lineWrapperTransformer)
170-
}
171136
172137
// Process tabs if provided
173138
let highlightedTabs: Array<{ label: string; code: string }> | null = null
174139
if (tabs && tabs.length > 0) {
175140
highlightedTabs = await Promise.all(
176-
tabs.map(async (tab) => {
177-
// Replace config placeholders in the code
178-
const processedCode = replaceConfigInText(tab.code)
179-
180-
const tabLang = tab.lang || detectedLang || 'bash'
181-
const shouldWrapTabLines = ['bash', 'sh', 'powershell'].includes(tabLang)
182-
183-
const tabTransformers: any[] = [
184-
transformerNotationDiff(),
185-
transformerNotationHighlight(),
186-
classTransformer
187-
]
188-
if (shouldWrapTabLines) {
189-
tabTransformers.push(lineWrapperTransformer)
190-
}
191-
192-
let tabHighlighted = await codeToHtml(processedCode, {
193-
lang: tabLang,
194-
themes: {
195-
light: bootstrapLight,
196-
dark: bootstrapDark
197-
},
198-
transformers: tabTransformers
199-
})
200-
201-
// Replace 'shiki' with 'astro-code' in the generated HTML
202-
tabHighlighted = tabHighlighted.replace(/class=(["'])shiki(\s+)/g, 'class=$1astro-code$2')
203-
tabHighlighted = tabHighlighted.replace(/class=(["'])shiki(["'])/g, 'class=$1astro-code$2')
204-
tabHighlighted = tabHighlighted.replace(/shiki-themes/g, 'astro-code-themes')
205-
206-
return {
207-
label: tab.label,
208-
code: tabHighlighted
209-
}
210-
})
141+
tabs.map(async (tab) => ({
142+
label: tab.label,
143+
code: await highlightCode(
144+
replaceConfigInText(tab.code),
145+
tab.lang || detectedLang || 'bash',
146+
diffTransformers
147+
)
148+
}))
211149
)
212150
}
213151
214-
let highlightedCode = codeToDisplay && detectedLang
215-
? await codeToHtml(codeToDisplay, {
216-
lang: detectedLang,
217-
themes: {
218-
light: bootstrapLight,
219-
dark: bootstrapDark
220-
},
221-
transformers
222-
})
152+
const highlightedCode = codeToDisplay && detectedLang
153+
? await highlightCode(codeToDisplay, detectedLang, diffTransformers)
223154
: null
224-
225-
// Replace 'shiki' with 'astro-code' in the generated HTML
226-
if (highlightedCode) {
227-
// Replace class="shiki" or class='shiki' (preserving other classes like has-diff)
228-
highlightedCode = highlightedCode.replace(/class=(["'])shiki(\s+)/g, 'class=$1astro-code$2')
229-
highlightedCode = highlightedCode.replace(/class=(["'])shiki(["'])/g, 'class=$1astro-code$2')
230-
// Replace shiki-themes if it exists
231-
highlightedCode = highlightedCode.replace(/shiki-themes/g, 'astro-code-themes')
232-
}
233155
---
234156

235157
<script>
236-
import ClipboardJS from 'clipboard'
158+
import { initCopyButtons } from '@libs/clipboard'
237159

238-
const btnTitle = 'Copy to clipboard'
239-
const btnEdit = 'Edit on StackBlitz'
240-
241-
function snippetButtonTooltip(selector: string, title: string) {
242-
document.querySelectorAll(selector).forEach((btn) => {
243-
bootstrap.Tooltip.getOrCreateInstance(btn, { title })
244-
})
245-
}
246-
247-
snippetButtonTooltip('.btn-clipboard', btnTitle)
248-
snippetButtonTooltip('.btn-edit', btnEdit)
160+
// StackBlitz tooltip
161+
document.querySelectorAll('.btn-edit').forEach((btn) => {
162+
bootstrap.Tooltip.getOrCreateInstance(btn, { title: 'Edit on StackBlitz' })
163+
})
249164

250165
// Handle tab switching
251166
document.querySelectorAll('.code-tabs').forEach((tabContainer) => {
252167
const buttons = tabContainer.querySelectorAll('.code-tab-btn')
253-
// Find the parent container that holds both tabs and content
254-
// Could be .bd-code-snippet or .bd-example-snippet
255168
const parentContainer = tabContainer.closest('.bd-code-snippet') ||
256169
tabContainer.closest('.bd-example-snippet') ||
257170
tabContainer.parentElement?.parentElement
258171
const codeBlocks = parentContainer?.querySelectorAll('.code-tab-content')
259172

260173
buttons.forEach((button, index) => {
261174
button.addEventListener('click', () => {
262-
// Remove active class from all buttons and hide all code blocks
263175
buttons.forEach((btn) => btn.classList.remove('active'))
264176
codeBlocks?.forEach((block) => block.classList.remove('active'))
265177

266-
// Add active class to clicked button and show corresponding code block
267178
button.classList.add('active')
268179
codeBlocks?.[index]?.classList.add('active')
269180
})
270181
})
271182
})
272183

273-
const clipboard = new ClipboardJS('.btn-clipboard', {
274-
target: (trigger) => trigger.closest('.bd-code-snippet')?.querySelector('.astro-code')!,
275-
text: (trigger) => {
276-
// For tabbed code, find the active tab's code
277-
const snippet = trigger.closest('.bd-code-snippet')
278-
const activeTab = snippet?.querySelector('.code-tab-content.active .astro-code')
279-
if (activeTab) {
280-
return activeTab.textContent?.trim()!
281-
}
282-
// Trim text to workaround a Firefox issue where the structure of the DOM (uncontrolled) is relevant for the
283-
// copied text.
284-
// https://github.com/zenorocha/clipboard.js/issues/439#issuecomment-312344621
285-
return snippet?.querySelector('.astro-code')!.textContent?.trim()!
184+
initCopyButtons('.bd-code-snippet [data-bd-clipboard]', (trigger) => {
185+
const snippet = trigger.closest('.bd-code-snippet')
186+
const activeTab = snippet?.querySelector('.code-tab-content.active .astro-code')
187+
if (activeTab) {
188+
return activeTab.textContent?.trim() || ''
286189
}
287-
})
288-
289-
clipboard.on('success', (event) => {
290-
const iconFirstChild = event.trigger.querySelector('.bi')?.firstElementChild
291-
const tooltipBtn = bootstrap.Tooltip.getInstance(event.trigger)
292-
const namespace = 'http://www.w3.org/1999/xlink'
293-
const originalXhref = iconFirstChild?.getAttributeNS(namespace, 'href')
294-
const isCheckIconVisible = originalXhref === '#check2'
295-
296-
if (isCheckIconVisible) {
297-
return
298-
}
299-
300-
tooltipBtn?.setContent({ '.tooltip-inner': 'Copied!' })
301-
302-
event.trigger.addEventListener(
303-
'hidden.bs.tooltip',
304-
() => {
305-
tooltipBtn?.setContent({ '.tooltip-inner': btnTitle })
306-
},
307-
{ once: true }
308-
)
309-
310-
event.clearSelection()
311-
312-
if (originalXhref) {
313-
iconFirstChild?.setAttributeNS(namespace, 'href', originalXhref.replace('clipboard', 'check2'))
314-
}
315-
316-
setTimeout(() => {
317-
if (originalXhref) {
318-
iconFirstChild?.setAttributeNS(namespace, 'href', originalXhref)
319-
}
320-
}, 2000)
321-
})
322-
323-
clipboard.on('error', (event) => {
324-
const modifierKey = /mac/i.test(navigator.userAgent) ? '\u2318' : 'Ctrl-'
325-
const fallbackMsg = `Press ${modifierKey}C to copy`
326-
const tooltipBtn = bootstrap.Tooltip.getInstance(event.trigger)
327-
328-
tooltipBtn?.setContent({ '.tooltip-inner': fallbackMsg })
329190

330-
event.trigger.addEventListener(
331-
'hidden.bs.tooltip',
332-
() => {
333-
tooltipBtn?.setContent({ '.tooltip-inner': btnTitle })
334-
},
335-
{ once: true }
336-
)
191+
return snippet?.querySelector('.astro-code')?.textContent?.trim() || ''
337192
})
338193
</script>
339194

@@ -352,6 +207,8 @@ if (highlightedCode) {
352207
</button>
353208
))}
354209
</div>
210+
) : file ? (
211+
<a class="text-decoration-none font-monospace fs-xs fg-3" href={fileUrl} target="_blank" rel="noopener noreferrer">{file}</a>
355212
) : (
356213
<div class="font-monospace text-uppercase fs-xs fg-3">{displayLang}</div>
357214
)}
@@ -363,9 +220,9 @@ if (highlightedCode) {
363220
</svg>
364221
</button>
365222
)}
366-
<button type="button" class="btn-clipboard mt-0 me-0" title="Copy to clipboard">
367-
<svg class="bi" aria-hidden="true">
368-
<use href="#clipboard" />
223+
<button type="button" class="btn btn-xs btn-icon bg-transparent" title="Copy" data-bd-clipboard>
224+
<svg class="bi fs-md" aria-hidden="true">
225+
<use href="#copy" />
369226
</svg>
370227
</button>
371228
</div>
@@ -386,7 +243,7 @@ if (highlightedCode) {
386243
)}
387244
</>
388245
) : (
389-
<div class="bd-code-snippet">
246+
<div class:list={["bd-code-snippet", containerClass]}>
390247
{!noToolbar && (
391248
<div class="hstack highlight-toolbar align-items-center">
392249
{highlightedTabs ? (
@@ -400,13 +257,15 @@ if (highlightedCode) {
400257
</button>
401258
))}
402259
</div>
260+
) : file ? (
261+
<a class="text-decoration-none font-monospace fs-xs fg-3" href={fileUrl} target="_blank" rel="noopener noreferrer">{file}</a>
403262
) : (
404263
<div class="font-monospace text-uppercase fs-xs fg-3">{displayLang}</div>
405264
)}
406265
<div class="d-flex ms-auto">
407-
<button type="button" class="btn-clipboard mt-0 me-0" title="Copy to clipboard">
408-
<svg class="bi" aria-hidden="true">
409-
<use href="#clipboard" />
266+
<button type="button" class="btn btn-xs btn-icon bg-transparent" title="Copy" data-bd-clipboard>
267+
<svg class="bi fs-md" aria-hidden="true">
268+
<use href="#copy" />
410269
</svg>
411270
</button>
412271
</div>

0 commit comments

Comments
 (0)