Skip to content

Commit 77b12e4

Browse files
bloveclaude
andauthored
fix(docs): restore indentation in fenced code blocks (#396)
MdxRenderer's `Pre` mapping hands CodeSurface rehype-pretty-code's `<code>` and drops the `<pre>` that wrapped it, so nothing supplied `white-space: pre`. The `<code>` is `display: grid` (one row per line), so lines still broke correctly and only the leading indentation collapsed — 33 of 40 docs pages. An `<Example>`'s Code pane passes a full `<pre>` through, which is why it was never affected. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 61ad4b4 commit 77b12e4

3 files changed

Lines changed: 110 additions & 2 deletions

File tree

‎apps/website/app/components/docs/mdx/CodeSurface.tsx‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,17 @@ export function CodeSurface({
160160
</div>
161161
);
162162

163+
// The `<pre>` is load-bearing, not decorative. `MdxRenderer`'s `Pre` mapping
164+
// hands this surface rehype-pretty-code's `<code>` and drops the `<pre>` that
165+
// wrapped it, so nothing downstream supplies `white-space: pre` — every
166+
// fence rendered with its leading indentation collapsed, because the `<code>`
167+
// is `display: grid` (one row per line), so lines still broke correctly and
168+
// only the indent silently vanished. An example's `children` is already a
169+
// full `<pre>`, which is why examples were never affected.
163170
const code =
164171
variant === "fence" ? (
165-
<div className="docs-code-type overflow-x-auto px-4 py-3 font-mono">
166-
{children}
172+
<div className="overflow-x-auto">
173+
<pre className="docs-code-type m-0 px-4 py-3 font-mono">{children}</pre>
167174
</div>
168175
) : (
169176
children

‎apps/website/app/components/docs/mdx/__tests__/CodeBlock.test.tsx‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,27 @@ describe("CodeBlock", () => {
3030
expect(writeText).toHaveBeenCalledWith("hello");
3131
});
3232

33+
it("wraps the code in a <pre>, so indentation survives", () => {
34+
// `MdxRenderer`'s `Pre` mapping drops rehype-pretty-code's `<pre>` and
35+
// hands `CodeBlock` the bare `<code>`. Nothing else supplies
36+
// `white-space: pre`, and because that `<code>` is `display: grid` the
37+
// lines still break — so a collapsed indent is invisible to any assertion
38+
// about line count or text content. Assert the element itself.
39+
//
40+
// jsdom applies no UA stylesheet arithmetic worth trusting here, so this
41+
// deliberately checks the TAG rather than a computed style: the tag is
42+
// what carries `white-space: pre` in a real browser, and it is the thing
43+
// that regressed. `apps/website/e2e` covers the rendered geometry.
44+
const { container } = render(
45+
<CodeBlock raw={"const a = 1;\n const b = 2;"}>
46+
<code>{"const a = 1;\n const b = 2;"}</code>
47+
</CodeBlock>,
48+
);
49+
const pre = container.querySelector("pre");
50+
expect(pre).not.toBeNull();
51+
expect(pre?.querySelector("code")).not.toBeNull();
52+
});
53+
3354
it("shows the filename in the header when one is supplied", () => {
3455
render(
3556
<CodeBlock raw="const x = 1;" filename="brand.css">
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { expect, test } from "@playwright/test";
2+
3+
/**
4+
* Fenced code blocks must render their leading indentation.
5+
*
6+
* `MdxRenderer`'s `Pre` mapping hands `CodeSurface` rehype-pretty-code's
7+
* `<code>` and drops the `<pre>` that wrapped it. Without a `<pre>` nothing
8+
* supplies `white-space: pre`, so every leading space collapsed — and because
9+
* that `<code>` is `display: grid` (one grid row per line), the lines still
10+
* broke in the right places. Only the indent vanished, which is why nothing
11+
* caught it: line counts, text content and `toContainText` all still passed.
12+
*
13+
* So assert GEOMETRY, not markup. A line whose text starts with whitespace has
14+
* to paint further right than a line that starts at column zero. That is the
15+
* only form of this assertion that a collapsed indent can fail.
16+
*
17+
* Measure the first non-space GLYPH, not the line element's box. Every
18+
* `span[data-line]` is a grid item in the same grid column, so its own left
19+
* edge is identical on every line whether or not the indent renders — an
20+
* earlier version of this test compared line boxes and reported the bug as
21+
* still broken after it was fixed. Walk to the first non-whitespace character
22+
* and measure a Range around it.
23+
*/
24+
const PAGE = "/docs/headless/getting-started";
25+
26+
test("fenced code blocks preserve leading indentation", async ({ page }) => {
27+
await page.goto(PAGE, { waitUntil: "domcontentloaded" });
28+
29+
// Fences, not examples: an `<Example>`'s Code pane always had a real `<pre>`
30+
// and was never affected, so including it would let a passing example mask a
31+
// broken fence.
32+
const fences = page
33+
.locator("figure")
34+
.filter({ hasNot: page.getByRole("tablist", { name: "Example view" }) });
35+
36+
const count = await fences.count();
37+
expect(count, `${PAGE} should render fenced code blocks`).toBeGreaterThan(0);
38+
39+
let checked = 0;
40+
for (let i = 0; i < count; i++) {
41+
const offsets = await fences.nth(i).evaluate((fig) => {
42+
const glyphLeft = (line: Element): number | null => {
43+
const walker = document.createTreeWalker(line, NodeFilter.SHOW_TEXT);
44+
let node: Node | null;
45+
while ((node = walker.nextNode())) {
46+
const at = (node.textContent ?? "").search(/\S/);
47+
if (at < 0) continue;
48+
const range = document.createRange();
49+
range.setStart(node, at);
50+
range.setEnd(node, at + 1);
51+
return range.getBoundingClientRect().left;
52+
}
53+
return null;
54+
};
55+
const lines = [...fig.querySelectorAll("span[data-line]")];
56+
const flat = lines.find((l) => /^\S/.test(l.textContent ?? ""));
57+
const indented = lines.find((l) => /^[ \t]+\S/.test(l.textContent ?? ""));
58+
if (!flat || !indented) return null;
59+
const a = glyphLeft(flat);
60+
const b = glyphLeft(indented);
61+
if (a === null || b === null) return null;
62+
return { flat: a, indented: b };
63+
});
64+
if (!offsets) continue;
65+
checked++;
66+
expect(
67+
offsets.indented,
68+
`fence #${i}: an indented line must paint right of a column-zero line`,
69+
).toBeGreaterThan(offsets.flat);
70+
}
71+
72+
// Guard the guard: if no fence on this page happens to contain both a
73+
// column-zero line and an indented one, the loop above asserts nothing and
74+
// this test would pass vacuously forever.
75+
expect(
76+
checked,
77+
`${PAGE} no longer has a fence mixing indented and unindented lines — ` +
78+
`this test needs a different page, it is not passing on merit`,
79+
).toBeGreaterThan(0);
80+
});

0 commit comments

Comments
 (0)