11---
22import fs from ' node:fs'
33import path from ' node:path'
4- import { codeToHtml } from ' shiki'
54import { 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 '
87import { replaceConfigInText } from ' @libs/remark'
98
109interface 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
8896let 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
173138let highlightedTabs: Array <{ label: string ; code: string }> | null = null
174139if (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