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 {