diff --git a/packages/components/src/components/code-block/code-block.stories.tsx b/packages/components/src/components/code-block/code-block.stories.tsx index b7dc7ca3..bb303593 100644 --- a/packages/components/src/components/code-block/code-block.stories.tsx +++ b/packages/components/src/components/code-block/code-block.stories.tsx @@ -464,3 +464,43 @@ export const WithCustomClassName: Story = { ), }; + +export const MDXIndents: Story = { + render: () => ( + + {`import { + a, + b, + } from 'pkg'; + + async function main() { + console.log('hello'); + }`} + + ), +}; + +export const MDXIndentsDeeplyNested: Story = { + render: () => ( + + {`function outer() { + function middle() { + function inner() { + function deepest() { + if (true) { + for (let i = 0; i < 10; i++) { + while (i > 0) { + return 42; + } + } + } + } + return deepest(); + } + return inner(); + } + return middle(); + }`} + + ), +}; diff --git a/packages/components/src/components/code-block/code-block.tsx b/packages/components/src/components/code-block/code-block.tsx index 61ac6772..047d1e0c 100644 --- a/packages/components/src/components/code-block/code-block.tsx +++ b/packages/components/src/components/code-block/code-block.tsx @@ -2,8 +2,8 @@ import type { ReactNode, RefObject } from "react"; import { Classes } from "@/constants/selectors"; import { cn } from "@/utils/cn"; -import { getNodeText } from "@/utils/get-node-text"; import type { CodeBlockTheme, CodeStyling } from "@/utils/shiki/code-styling"; +import { getCodeString } from "@/utils/shiki/lib"; import { BaseCodeBlock } from "./base-code-block"; import { CodeHeader } from "./code-header"; @@ -98,7 +98,7 @@ const CodeBlock = function CodeBlock(params: CodeBlockProps) { copyButtonProps, } = params; - const codeString = getNodeText(children); + const codeString = getCodeString(children, className, true); const hasGrayBackgroundContainer = !!filename || !!icon; return ( diff --git a/packages/components/src/components/code-group/code-group.tsx b/packages/components/src/components/code-group/code-group.tsx index 6f285ccd..c41ce8ad 100644 --- a/packages/components/src/components/code-group/code-group.tsx +++ b/packages/components/src/components/code-group/code-group.tsx @@ -18,8 +18,8 @@ import { import { Icon as ComponentIcon } from "@/components/icon"; import { Classes } from "@/constants/selectors"; import { cn } from "@/utils/cn"; -import { getNodeText } from "@/utils/get-node-text"; import type { CodeBlockTheme, CodeStyling } from "@/utils/shiki/code-styling"; +import { getCodeString } from "@/utils/shiki/lib"; import { LanguageDropdown } from "./language-dropdown"; @@ -220,7 +220,11 @@ const CodeGroup = ({ {feedbackButton && feedbackButton} {askAiButton && askAiButton} diff --git a/packages/components/src/utils/shiki/lib.ts b/packages/components/src/utils/shiki/lib.ts index 99c9bab0..0accfe7d 100644 --- a/packages/components/src/utils/shiki/lib.ts +++ b/packages/components/src/utils/shiki/lib.ts @@ -3,6 +3,14 @@ import { type ReactNode, useMemo } from "react"; import { getNodeText } from "@/utils/get-node-text"; import { SHIKI_CLASSNAME } from "@/utils/shiki/constants"; +const lineIndentRegex = /^( *)/; +const closingStructureRegex = /^([}\])]|<\/)/; + +function getIndent(line: string): number { + const match = line.match(lineIndentRegex); + return match ? match[1].length : 0; +} + function findShikiClassName(children: unknown): boolean { if (!children || typeof children !== "object") { return false; @@ -37,6 +45,49 @@ function findShikiClassName(children: unknown): boolean { return false; } +function dedentCode(code: string): string { + const lines = code.split("\n"); + if (lines.length <= 1) { + return code; + } + + const relevantLines = lines.filter((line) => line.trim() !== ""); + if (relevantLines.length === 0) { + return code; + } + + const firstLine = relevantLines[0]; + const lastLine = relevantLines.at(-1) ?? firstLine; + const firstIndent = getIndent(firstLine); + const lastIndent = getIndent(lastLine); + const isTemplatePolluted = + firstIndent < lastIndent && closingStructureRegex.test(lastLine.trim()); + + if (isTemplatePolluted) { + const firstNonEmptyIndex = lines.findIndex((line) => line.trim() !== ""); + const tail = relevantLines.slice(1); + if (tail.length === 0) { + return code; + } + const minIndent = Math.min(...tail.map(getIndent)); + if (minIndent === 0) { + return code; + } + return lines + .map((line, i) => + i <= firstNonEmptyIndex ? line : line.slice(minIndent) + ) + .join("\n"); + } + + const minIndent = Math.min(...relevantLines.map(getIndent)); + if (minIndent === 0) { + return code; + } + + return lines.map((line) => line.slice(minIndent)).join("\n"); +} + function getCodeString( children: ReactNode, className?: string, @@ -50,7 +101,7 @@ function getCodeString( const codeString = getNodeText(children); - return codeString; + return dedentCode(codeString); } function calculateCodeLinesFromHtml(html: string | undefined): number {