diff --git a/src/components/CodeBlock.tsx b/src/components/CodeBlock.tsx index 2f372f43f..19a463fdf 100644 --- a/src/components/CodeBlock.tsx +++ b/src/components/CodeBlock.tsx @@ -6,7 +6,6 @@ import type { Mermaid } from 'mermaid' import { transformerNotationDiff } from '@shikijs/transformers' import { createHighlighter, type HighlighterGeneric } from 'shiki' import { Button } from './Button' -import { ButtonGroup } from './ButtonGroup' // Language aliases mapping const LANG_ALIASES: Record = { @@ -99,6 +98,16 @@ export function CodeBlock({ isEmbedded?: boolean showTypeCopyButton?: boolean }) { + // Extract title from data-code-title attribute, handling both camelCase and kebab-case + const rawTitle = ((props as any)?.dataCodeTitle || + (props as any)?.['data-code-title']) as string | undefined + + // Filter out "undefined" strings, null, and empty strings + const title = + rawTitle && rawTitle !== 'undefined' && rawTitle.trim().length > 0 + ? rawTitle.trim() + : undefined + const childElement = props.children as | undefined | { props?: { className?: string; children?: string } } @@ -123,14 +132,9 @@ export function CodeBlock({ const code = children?.props.children const [codeElement, setCodeElement] = React.useState( - <> -
-        {lang === 'mermaid' ?  : code}
-      
-
-        {lang === 'mermaid' ?  : code}
-      
- , +
+      {lang === 'mermaid' ?  : code}
+    
, ) React[ @@ -189,18 +193,14 @@ export function CodeBlock({ )} style={props.style} > - {showTypeCopyButton ? ( - - {lang ? ( - {lang} - ) : null} + {(title || showTypeCopyButton) && ( +
+
+ {title || (lang?.toLowerCase() === 'bash' ? 'sh' : (lang ?? ''))} +
+ - - ) : null} +
+ )} {codeElement} ) diff --git a/src/components/FileTabs.tsx b/src/components/FileTabs.tsx new file mode 100644 index 000000000..a325e8a7f --- /dev/null +++ b/src/components/FileTabs.tsx @@ -0,0 +1,58 @@ +import * as React from 'react' + +export type FileTabDefinition = { + slug: string + name: string +} + +export type FileTabsProps = { + tabs: Array + children: Array | React.ReactNode + id: string +} + +export function FileTabs({ tabs, id, children }: FileTabsProps) { + const childrenArray = React.Children.toArray(children) + const [activeSlug, setActiveSlug] = React.useState(tabs[0]?.slug ?? '') + + if (tabs.length === 0) return null + + return ( +
+
+ {tabs.map((tab) => ( + + ))} +
+
+ {childrenArray.map((child, index) => { + const tab = tabs[index] + if (!tab) return null + return ( + + ) + })} +
+
+ ) +} diff --git a/src/components/FrameworkCodeBlock.tsx b/src/components/FrameworkCodeBlock.tsx new file mode 100644 index 000000000..8d065baef --- /dev/null +++ b/src/components/FrameworkCodeBlock.tsx @@ -0,0 +1,71 @@ +import * as React from 'react' +import { useLocalCurrentFramework } from './FrameworkSelect' +import { useCurrentUserQuery } from '~/hooks/useCurrentUser' +import { useParams } from '@tanstack/react-router' +import { FileTabs } from './FileTabs' +import type { Framework } from '~/libraries/types' + +type CodeBlockMeta = { + title: string + code: string + language: string +} + +type FrameworkCodeBlockProps = { + id: string + codeBlocksByFramework: Record + availableFrameworks: string[] + /** Pre-rendered React children for each framework (from domToReact) */ + panelsByFramework: Record +} + +/** + * Renders code blocks for the currently selected framework. + * - If no blocks for framework: shows nothing + * - If 1 block: shows just the code block (minimal style) + * - If multiple blocks: shows as FileTabs (file tabs with names) + */ +export function FrameworkCodeBlock({ + id, + codeBlocksByFramework, + panelsByFramework, +}: FrameworkCodeBlockProps) { + const { framework: paramsFramework } = useParams({ strict: false }) + const localCurrentFramework = useLocalCurrentFramework() + const userQuery = useCurrentUserQuery() + const userFramework = userQuery.data?.lastUsedFramework + + const actualFramework = (paramsFramework || + userFramework || + localCurrentFramework.currentFramework || + 'react') as Framework + + const normalizedFramework = actualFramework.toLowerCase() + + // Find the framework's code blocks + const frameworkBlocks = codeBlocksByFramework[normalizedFramework] + const frameworkPanel = panelsByFramework[normalizedFramework] + + if (!frameworkBlocks || frameworkBlocks.length === 0 || !frameworkPanel) { + return null + } + + if (frameworkBlocks.length === 1) { + return
{frameworkPanel}
+ } + + const tabs = frameworkBlocks.map((block, index) => ({ + slug: `file-${index}`, + name: block.title || 'Untitled', + })) + + const childrenArray = React.Children.toArray(frameworkPanel) + + return ( +
+ + {childrenArray} + +
+ ) +} diff --git a/src/components/Markdown.tsx b/src/components/Markdown.tsx index 19dc97a4b..cb1afd20e 100644 --- a/src/components/Markdown.tsx +++ b/src/components/Markdown.tsx @@ -15,6 +15,8 @@ import { Tabs } from '~/components/Tabs' import { CodeBlock } from './CodeBlock' import { PackageManagerTabs } from './PackageManagerTabs' import type { Framework } from '~/libraries/types' +import { FileTabs } from './FileTabs' +import { FrameworkCodeBlock } from './FrameworkCodeBlock' type HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' @@ -124,7 +126,6 @@ const options: HTMLReactParserOptions = { switch (componentName?.toLowerCase()) { case 'tabs': { - // Check if this is a package-manager tabs (has metadata) if (pmMeta) { try { const { packagesByFramework, mode } = JSON.parse(pmMeta) @@ -148,7 +149,73 @@ const options: HTMLReactParserOptions = { } } - // Default tabs variant + // Check if this is files variant + const filesMeta = domNode.attribs['data-files-meta'] + if (filesMeta) { + try { + const tabs = attributes.tabs || [] + const id = + attributes.id || + `files-tabs-${Math.random().toString(36).slice(2, 9)}` + + const panelElements = domNode.children?.filter( + (child): child is Element => + child instanceof Element && child.name === 'md-tab-panel', + ) + + const children = panelElements?.map((panel) => + domToReact(panel.children as any, options), + ) + + return ( + + ) + } catch { + // Fall through to default tabs if parsing fails + } + } + + const frameworkMeta = domNode.attribs['data-framework-meta'] + if (frameworkMeta) { + try { + const { codeBlocksByFramework } = JSON.parse(frameworkMeta) + const availableFrameworks = JSON.parse( + domNode.attribs['data-available-frameworks'] || '[]', + ) + const id = + attributes.id || + `framework-${Math.random().toString(36).slice(2, 9)}` + + const panelElements = domNode.children?.filter( + (child): child is Element => + child instanceof Element && child.name === 'md-tab-panel', + ) + + // Build panelsByFramework map + const panelsByFramework: Record = {} + panelElements?.forEach((panel) => { + const fw = panel.attribs['data-framework'] + if (fw) { + panelsByFramework[fw] = domToReact( + panel.children as any, + options, + ) + } + }) + + return ( + + ) + } catch { + // Fall through to default tabs if parsing fails + } + } + const tabs = attributes.tabs const id = attributes.id || `tabs-${Math.random().toString(36).slice(2, 9)}` @@ -162,9 +229,11 @@ const options: HTMLReactParserOptions = { child instanceof Element && child.name === 'md-tab-panel', ) - const children = panelElements?.map((panel) => - domToReact(panel.children as any, options), - ) + const children = panelElements?.map((panel) => { + const result = domToReact(panel.children as any, options) + // Wrap in fragment to ensure it's a single React node + return <>{result} + }) return } diff --git a/src/components/PackageManagerTabs.tsx b/src/components/PackageManagerTabs.tsx index bca44b2d6..61f2b1eb1 100644 --- a/src/components/PackageManagerTabs.tsx +++ b/src/components/PackageManagerTabs.tsx @@ -131,12 +131,14 @@ export function PackageManagerTabs({ }) return ( - setPackageManager(slug as PackageManager)} - /> +
+ setPackageManager(slug as PackageManager)} + /> +
) } diff --git a/src/components/Tabs.tsx b/src/components/Tabs.tsx index f422208c2..586bc3e31 100644 --- a/src/components/Tabs.tsx +++ b/src/components/Tabs.tsx @@ -9,28 +9,32 @@ export type TabDefinition = { } export type TabsProps = { - tabs: Array - children: Array + tabs?: Array + children?: Array | React.ReactNode id: string activeSlug?: string onTabChange?: (slug: string) => void } export function Tabs({ - tabs, + tabs: tabsProp = [], id, - children, + children: childrenProp, activeSlug: controlledActiveSlug, onTabChange, }: TabsProps) { + const childrenArray = React.Children.toArray(childrenProp) + const params = useParams({ strict: false }) - const framework = 'framework' in params ? params.framework : undefined + const framework = params?.framework ?? undefined - const [internalActiveSlug, setInternalActiveSlug] = React.useState( - () => tabs.find((tab) => tab.slug === framework)?.slug || tabs[0].slug, - ) + const [internalActiveSlug, setInternalActiveSlug] = React.useState(() => { + const match = framework + ? tabsProp.find((tab) => tab.slug === framework) + : undefined + return match?.slug ?? tabsProp[0]?.slug ?? '' + }) - // Use controlled state if provided, otherwise use internal state const activeSlug = controlledActiveSlug ?? internalActiveSlug const setActiveSlug = React.useCallback( (slug: string) => { @@ -43,10 +47,12 @@ export function Tabs({ [onTabChange], ) + if (tabsProp.length === 0) return null + return (
- {tabs.map((tab) => { + {tabsProp.map((tab) => { return (
- {children.map((child, index) => { - const tab = tabs[index] + {childrenArray.map((child, index) => { + const tab = tabsProp[index] if (!tab) return null return (
void -}) => { - const option = React.useMemo( - () => frameworkOptions.find((o) => o.value === tab.slug), - [tab.slug], - ) - return ( - - ) -} +const Tab = React.memo( + ({ + tab, + activeSlug, + setActiveSlug, + }: { + id?: string + tab: TabDefinition + activeSlug: string + setActiveSlug: (slug: string) => void + }) => { + const option = React.useMemo( + () => frameworkOptions.find((o) => o.value === tab.slug), + [tab.slug], + ) + + return ( + + ) + }, +) diff --git a/src/styles/app.css b/src/styles/app.css index ede8c6e0e..c2e3f92d8 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -343,8 +343,10 @@ pre { pre.shiki { overflow-x: auto; + @apply bg-white; + &.vitesse-dark { - @apply text-gray-400; + @apply text-gray-400 bg-gray-950; } } pre.shiki:hover .dim { @@ -408,7 +410,6 @@ pre.twoslash data-lsp:hover::before { position: absolute; transform: translate(0, 1rem); - background-color: #3f3f3f; color: #fff; text-align: left; padding: 5px 8px; @@ -861,3 +862,68 @@ mark { width: 1em; height: 1em; } + +/* Consecutive code blocks - no gap when under headings */ +[data-tab] .codeblock + .codeblock { + @apply mt-0 border-t-0 rounded-t-none; +} + +[data-tab] .codeblock:has(+ .codeblock) { + @apply rounded-b-none; +} +/* File tabs variant - minimal code blocks with floating copy button */ +.file-tabs-panel .codeblock { + @apply rounded-t-none border-t-0 my-0; +} + +/* Hide the title bar but keep copy button accessible */ +.file-tabs-panel .codeblock > div:first-child { + @apply absolute right-2 top-2 bg-transparent border-0 p-0 z-10; +} + +/* Hide the title text, keep only the button */ +.file-tabs-panel .codeblock > div:first-child > div:first-child { + @apply hidden; +} + +/* Package manager tabs - minimal code blocks with floating copy button */ +.package-manager-tabs [data-tab] .codeblock { + @apply rounded-t-none border-t-0 my-0; +} + +.package-manager-tabs [data-tab] .codeblock > div:first-child { + @apply absolute right-2 top-2 bg-transparent border-0 p-0 z-10; +} + +.package-manager-tabs + [data-tab] + .codeblock + > div:first-child + > div:first-child { + @apply hidden; +} + +/* Remove padding from tab content wrapper for package manager */ +.package-manager-tabs .not-prose > div:last-child { + @apply p-0 border-0 rounded-b-none bg-transparent; +} + +/* Restore bottom border radius on the code block itself */ +.package-manager-tabs [data-tab] .codeblock { + @apply rounded-b-md; +} + +/* Framework code blocks - minimal style for single code blocks */ +.framework-code-block > .codeblock { + @apply my-4 rounded-md; +} + +/* Hide the title bar but keep copy button accessible for single blocks */ +.framework-code-block > .codeblock > div:first-child { + @apply absolute right-2 top-2 bg-transparent border-0 p-0 z-10; +} + +/* Hide the title text, keep only the button */ +.framework-code-block > .codeblock > div:first-child > div:first-child { + @apply hidden; +} diff --git a/src/utils/dates.ts b/src/utils/dates.ts index 4409a54db..0a2b5f511 100644 --- a/src/utils/dates.ts +++ b/src/utils/dates.ts @@ -67,36 +67,10 @@ export function format(date: Date | number, formatStr: string): string { }) case 'MMM d, yyyy': - // "Apr 29, 2023" - return d.toLocaleDateString('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric', - }) - case 'MMM dd, yyyy': - // "Apr 29, 2023" (same as above, just different format string) - return d.toLocaleDateString('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric', - }) - - case 'MMMM d, yyyy': - // "April 29, 2023" + // "Apr 29, 2023" return d.toLocaleDateString('en-US', { year: 'numeric', - month: 'long', - day: 'numeric', - }) - - case 'yyyy-MM-dd': - // "2023-04-29" - return d.toISOString().split('T')[0] - - case 'MMM d': - // "Apr 29" - return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', }) diff --git a/src/utils/markdown/plugins/extractCodeMeta.ts b/src/utils/markdown/plugins/extractCodeMeta.ts new file mode 100644 index 000000000..a2c55b584 --- /dev/null +++ b/src/utils/markdown/plugins/extractCodeMeta.ts @@ -0,0 +1,56 @@ +import { visit } from 'unist-util-visit' + +export function extractCodeMeta() { + return (tree) => { + visit(tree, 'element', (node: any) => { + if (node && node.tagName === 'pre') { + const codeChild = Array.isArray(node.children) + ? node.children[0] + : undefined + + const metaString = + (codeChild && + ((codeChild.data && codeChild.data.meta) || + (codeChild.properties && codeChild.properties.metastring))) || + undefined + + let filename: string | undefined = undefined + let framework: string | undefined = undefined + + if (metaString && typeof metaString === 'string') { + const marker = 'title="' + const idx = metaString.indexOf(marker) + + if (idx !== -1) { + const rest = metaString.slice(idx + marker.length) + const end = rest.indexOf('"') + + if (end !== -1) { + filename = rest.slice(0, end) + } + } + + // Extract framework attribute + const frameworkMarker = 'framework="' + const frameworkIdx = metaString.indexOf(frameworkMarker) + + if (frameworkIdx !== -1) { + const rest = metaString.slice(frameworkIdx + frameworkMarker.length) + const end = rest.indexOf('"') + + if (end !== -1) { + framework = rest.slice(0, end).toLowerCase() + } + } + } + + node.properties = { + ...(node.properties || {}), + 'data-filename': filename, + ...(filename ? { 'data-code-title': filename } : {}), + ...(framework ? { 'data-framework': framework } : {}), + } + } + }) + } +} diff --git a/src/utils/markdown/plugins/transformTabsComponent.ts b/src/utils/markdown/plugins/transformTabsComponent.ts index 15eddc5a6..1c982e68f 100644 --- a/src/utils/markdown/plugins/transformTabsComponent.ts +++ b/src/utils/markdown/plugins/transformTabsComponent.ts @@ -2,6 +2,11 @@ import { toString } from 'hast-util-to-string' import { headingLevel, isHeading, slugify } from './helpers' +export type VariantHandler = ( + node: HastNode, + attributes: Record, +) => boolean + type InstallMode = 'install' | 'dev-install' type HastNode = { @@ -26,6 +31,34 @@ type PackageManagerExtraction = { mode: InstallMode } +type FilesExtraction = { + files: Array<{ + title: string + code: string + language: string + preNode: HastNode + }> +} + +type FrameworkExtraction = { + codeBlocksByFramework: Record< + string, + Array<{ + title: string + code: string + language: string + preNode: HastNode + }> + > +} + +type FrameworkCodeBlock = { + title: string + code: string + language: string + preNode: HastNode +} + function parseAttributes(node: HastNode): Record { const rawAttributes = node.properties?.['data-attributes'] if (typeof rawAttributes === 'string') { @@ -47,6 +80,19 @@ function normalizeFrameworkKey(key: string): string { return key.trim().toLowerCase() } +// Helper to extract text from nodes (used for code content) +function extractText(nodes: any[]): string { + let text = '' + for (const node of nodes) { + if (node.type === 'text') { + text += node.value + } else if (node.type === 'element' && node.children) { + text += extractText(node.children) + } + } + return text +} + /** * Parse a line like "react: @tanstack/react-query @tanstack/react-query-devtools" * Returns { framework: 'react', packages: '@tanstack/react-query @tanstack/react-query-devtools' } @@ -78,19 +124,6 @@ function extractPackageManagerData( const children = node.children ?? [] const packagesByFramework: Record = {} - // Recursively extract text from all children (including nested in

tags) - function extractText(nodes: any[]): string { - let text = '' - for (const node of nodes) { - if (node.type === 'text') { - text += node.value - } else if (node.type === 'element' && node.children) { - text += extractText(node.children) - } - } - return text - } - const allText = extractText(children) const lines = allText.split('\n') @@ -116,23 +149,124 @@ function extractPackageManagerData( return { packagesByFramework, mode } } -function createPackageManagerHeadings(): HastNode[] { - const packageManagers = ['npm', 'pnpm', 'yarn', 'bun'] - const nodes: HastNode[] = [] +/** + * Extract code block data (language, title, code) from a

 element.
+ * Extracts title from data-code-title (set by rehypeCodeMeta).
+ */
+function extractCodeBlockData(preNode: HastNode): {
+  language: string
+  title: string
+  code: string
+} | null {
+  // Find the  child
+  const codeNode = preNode.children?.find(
+    (c: HastNode) => c.type === 'element' && c.tagName === 'code',
+  )
+
+  if (!codeNode) return null
+
+  // Extract language from className
+  let language = 'plaintext'
+  const className = codeNode.properties?.className
+  if (Array.isArray(className)) {
+    const langClass = className.find((c) => String(c).startsWith('language-'))
+    if (langClass) {
+      language = String(langClass).replace('language-', '')
+    }
+  }
 
-  for (const pm of packageManagers) {
-    // Create heading for package manager
-    const heading: any = {
-      type: 'element',
-      tagName: 'h1',
-      properties: { id: pm },
-      children: [{ type: 'text', value: pm }],
+  let title = ''
+  const props = preNode.properties || {}
+  if (typeof props['dataCodeTitle'] === 'string') {
+    title = props['dataCodeTitle'] as string
+  } else if (typeof props['data-code-title'] === 'string') {
+    title = props['data-code-title']
+  } else if (typeof props['dataFilename'] === 'string') {
+    title = props['dataFilename'] as string
+  } else if (typeof props['data-filename'] === 'string') {
+    title = props['data-filename']
+  }
+
+  // Extract code content
+  const code = extractText(codeNode.children || [])
+
+  return { language, title, code }
+}
+
+/**
+ * Extract files data for variant="files" tabs.
+ * Parses consecutive code blocks and creates file tabs.
+ */
+function extractFilesData(node: HastNode): FilesExtraction | null {
+  const children = node.children ?? []
+  const files: FilesExtraction['files'] = []
+
+  for (const child of children) {
+    if (child.type === 'element' && child.tagName === 'pre') {
+      const codeBlockData = extractCodeBlockData(child)
+      if (!codeBlockData) continue
+
+      files.push({
+        title: codeBlockData.title || 'Untitled',
+        code: codeBlockData.code,
+        language: codeBlockData.language,
+        preNode: child,
+      })
+    }
+  }
+
+  if (files.length === 0) {
+    return null
+  }
+
+  return { files }
+}
+
+/**
+ * Extract framework-specific code blocks for variant="framework" tabs.
+ * Groups code blocks by their data-framework attribute.
+ */
+function extractFrameworkData(node: HastNode): FrameworkExtraction | null {
+  const children = node.children ?? []
+  const codeBlocksByFramework: Record = {}
+
+  let currentFramework: string | null = null
+
+  for (const child of children) {
+    if (isHeading(child)) {
+      currentFramework = toString(child as any)
+        .trim()
+        .toLowerCase()
+      continue
     }
 
-    nodes.push(heading)
+    // Look for 
 elements (code blocks) under current framework
+    if (
+      child.type === 'element' &&
+      child.tagName === 'pre' &&
+      currentFramework
+    ) {
+      const codeBlockData = extractCodeBlockData(child)
+      if (!codeBlockData) continue
+
+      if (!codeBlocksByFramework[currentFramework]) {
+        codeBlocksByFramework[currentFramework] = []
+      }
+
+      codeBlocksByFramework[currentFramework].push({
+        title: codeBlockData.title || 'Untitled',
+        code: codeBlockData.code,
+        language: codeBlockData.language,
+        preNode: child,
+      })
+    }
+  }
+
+  if (Object.keys(codeBlocksByFramework).length === 0) {
+    return null
   }
 
-  return nodes
+  return { codeBlocksByFramework }
 }
 
 function extractTabPanels(node: HastNode): TabExtraction | null {
@@ -211,8 +345,8 @@ export function transformTabsComponent(node: HastNode) {
       return
     }
 
-    // Replace children with package manager headings
-    node.children = createPackageManagerHeadings()
+    // Remove children so package managers don't show up in TOC
+    node.children = []
 
     // Store metadata for the React component
     node.properties = node.properties || {}
@@ -220,6 +354,87 @@ export function transformTabsComponent(node: HastNode) {
       packagesByFramework: result.packagesByFramework,
       mode: result.mode,
     })
+    return
+  }
+
+  // Handle files variant
+  if (variant === 'files') {
+    const result = extractFilesData(node)
+
+    if (!result) {
+      return
+    }
+
+    // Store metadata for the React component (without preNodes to avoid circular refs)
+    node.properties = node.properties || {}
+    node.properties['data-files-meta'] = JSON.stringify({
+      files: result.files.map((f) => ({
+        title: f.title,
+        code: f.code,
+        language: f.language,
+      })),
+    })
+
+    // Create tab headings from file titles
+    const tabs = result.files.map((file, index) => ({
+      slug: `file-${index}`,
+      name: file.title,
+    }))
+
+    node.properties['data-attributes'] = JSON.stringify({ tabs })
+
+    // Create panel elements with original preNodes
+    node.children = result.files.map((file, index) => ({
+      type: 'element',
+      tagName: 'md-tab-panel',
+      properties: {
+        'data-tab-slug': `file-${index}`,
+        'data-tab-index': String(index),
+      },
+      // Use the original preNode which already has data-code-title from rehypeCodeMeta
+      children: [file.preNode],
+    }))
+    return
+  }
+
+  if (variant === 'framework') {
+    const result = extractFrameworkData(node)
+
+    if (!result) {
+      return
+    }
+
+    node.properties = node.properties || {}
+    node.properties['data-framework-meta'] = JSON.stringify({
+      codeBlocksByFramework: Object.fromEntries(
+        Object.entries(result.codeBlocksByFramework).map(([fw, blocks]) => [
+          fw,
+          blocks.map((b) => ({
+            title: b.title,
+            code: b.code,
+            language: b.language,
+          })),
+        ]),
+      ),
+    })
+
+    // Store available frameworks for the component
+    const availableFrameworks = Object.keys(result.codeBlocksByFramework)
+    node.properties['data-available-frameworks'] =
+      JSON.stringify(availableFrameworks)
+
+    node.children = availableFrameworks.map((fw) => {
+      const blocks = result.codeBlocksByFramework[fw]
+      return {
+        type: 'element',
+        tagName: 'md-tab-panel',
+        properties: {
+          'data-framework': fw,
+        },
+        children: blocks.map((block) => block.preNode),
+      }
+    })
+    return
   }
 
   // Handle default tabs variant
diff --git a/src/utils/markdown/processor.ts b/src/utils/markdown/processor.ts
index 2ee538f0c..295ca3080 100644
--- a/src/utils/markdown/processor.ts
+++ b/src/utils/markdown/processor.ts
@@ -7,13 +7,13 @@ import rehypeRaw from 'rehype-raw'
 import rehypeSlug from 'rehype-slug'
 import rehypeAutolinkHeadings from 'rehype-autolink-headings'
 import rehypeStringify from 'rehype-stringify'
-import { visit } from 'unist-util-visit'
-import { toString } from 'hast-util-to-string'
+
 import {
   rehypeCollectHeadings,
   rehypeParseCommentComponents,
   rehypeTransformCommentComponents,
 } from '~/utils/markdown/plugins'
+import { extractCodeMeta } from '~/utils/markdown/plugins/extractCodeMeta'
 
 export type MarkdownHeading = {
   id: string
@@ -33,6 +33,7 @@ export function renderMarkdown(content: string): MarkdownRenderResult {
     .use(remarkParse)
     .use(remarkGfm)
     .use(remarkRehype, { allowDangerousHtml: true })
+    .use(extractCodeMeta)
     .use(rehypeRaw)
     .use(rehypeParseCommentComponents)
     .use(rehypeCallouts, {