Skip to content

Commit 20be250

Browse files
authored
feat: custom prettier plugin to correctly format inline MDX components (#29770)
1 parent e8f112e commit 20be250

File tree

11 files changed

+247
-104
lines changed

11 files changed

+247
-104
lines changed

.prettierrc.mjs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
// @ts-check
22
/** @type {import("prettier").Config} */
33
export default {
4-
plugins: ["prettier-plugin-astro", "prettier-plugin-tailwindcss"],
4+
plugins: [
5+
"prettier-plugin-astro",
6+
"prettier-plugin-tailwindcss",
7+
"./plugins/prettier-plugin-mdx-inline/index.mjs",
8+
],
59
useTabs: true,
610
overrides: [
711
{
@@ -10,5 +14,18 @@ export default {
1014
parser: "astro",
1115
},
1216
},
17+
// Prettier's MDX formatter wraps inline JSX elements (like <code> and
18+
// <GlossaryTooltip>) onto new lines, which causes MDX v2+ to inject <p>
19+
// tags inside them — breaking the rendered HTML. This custom plugin
20+
// prevents that by keeping configured elements on a single line.
21+
// This may become unnecessary once prettier adds MDX v3 support:
22+
// https://github.com/prettier/prettier/issues/12209
23+
{
24+
files: "*.mdx",
25+
options: {
26+
parser: "mdx-inline",
27+
mdxInlineElements: "code,GlossaryTooltip",
28+
},
29+
},
1330
],
1431
};
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/**
2+
* prettier-plugin-mdx-inline
3+
*
4+
* Prevents prettier from reformatting specific JSX elements in MDX files.
5+
*
6+
* Problem: Prettier's MDX formatter treats standalone JSX elements (like
7+
* `<code>`) as block-level and wraps their children
8+
* onto new lines when they exceed printWidth. MDX v2+ then interprets those
9+
* newlines as markdown paragraph boundaries, injecting <p> tags inside inline
10+
* elements — producing broken HTML like `<code><p>...</p></code>`.
11+
*
12+
* Solution: This plugin intercepts the parsed MDX AST and converts matching
13+
* JSX nodes to opaque HTML nodes that prettier outputs verbatim. It also
14+
* collapses any existing multi-line formatting back to a single line.
15+
*
16+
* Configuration (.prettierrc.mjs):
17+
*
18+
* export default {
19+
* plugins: ["./prettier-plugin-mdx-inline/index.mjs"],
20+
* overrides: [{
21+
* files: "*.mdx",
22+
* options: { parser: "mdx-inline" },
23+
* }],
24+
* };
25+
*
26+
* You must specify which elements to protect via `mdxInlineElements`:
27+
*
28+
* overrides: [{
29+
* files: "*.mdx",
30+
* options: {
31+
* parser: "mdx-inline",
32+
* mdxInlineElements: "code,GlossaryTooltip",
33+
* },
34+
* }],
35+
*/
36+
37+
/**
38+
* Extract the element name from the start of a JSX string.
39+
* e.g., "<code>" → "code", "<GlossaryTooltip term="x">" → "GlossaryTooltip"
40+
*/
41+
function getElementName(value) {
42+
const match = value.trim().match(/^<([a-zA-Z][a-zA-Z0-9]*)/);
43+
return match ? match[1] : null;
44+
}
45+
46+
/**
47+
* Collapse a multi-line JSX element value onto a single line.
48+
*
49+
* Handles:
50+
* - {" "} spacer expressions inserted by prettier
51+
* - Newlines with surrounding whitespace from indentation
52+
* - Multiple consecutive spaces from collapsing
53+
* - Trailing content after the closing tag (e.g., in list items)
54+
*
55+
* Preserves:
56+
* - Attribute values (strings in the opening tag)
57+
* - Self-closing tags within content (e.g., <Type text="..." />)
58+
*/
59+
function collapseInlineJsx(value) {
60+
// Remove {" "} spacers — these are prettier artifacts for preserving spaces
61+
let result = value.replace(/\{" "\}/g, " ");
62+
63+
const elementName = getElementName(result);
64+
if (!elementName) return result;
65+
66+
// Find the end of the opening tag by tracking string context.
67+
// We need to skip over attribute values that may contain '>' characters.
68+
let inString = false;
69+
let stringChar = "";
70+
let openTagEnd = -1;
71+
72+
for (let i = 0; i < result.length; i++) {
73+
const ch = result[i];
74+
if (inString) {
75+
if (ch === stringChar && result[i - 1] !== "\\") {
76+
inString = false;
77+
}
78+
} else if (ch === '"' || ch === "'") {
79+
inString = true;
80+
stringChar = ch;
81+
} else if (ch === ">") {
82+
openTagEnd = i;
83+
break;
84+
}
85+
}
86+
87+
if (openTagEnd === -1) return result;
88+
89+
// Find the matching closing tag — it may not be at the very end of the
90+
// value if there is trailing content (e.g., in a list item where the
91+
// description text follows the </code> within the same JSX node).
92+
const closeTag = `</${elementName}>`;
93+
const closeTagIndex = result.indexOf(closeTag, openTagEnd);
94+
if (closeTagIndex === -1) return result;
95+
96+
const openTag = result.substring(0, openTagEnd + 1);
97+
const content = result.substring(openTagEnd + 1, closeTagIndex);
98+
const trailing = result.substring(closeTagIndex + closeTag.length);
99+
100+
// Collapse whitespace in the content between tags
101+
const collapsed = content
102+
.replace(/\n\s*/g, " ") // newlines + indentation → single space
103+
.replace(/\s{2,}/g, " ") // multiple spaces → single space
104+
.trim();
105+
106+
return openTag + collapsed + closeTag + trailing;
107+
}
108+
109+
/**
110+
* Parse the configured element list from the options.
111+
*/
112+
function getInlineElements(options) {
113+
const configured = options.mdxInlineElements;
114+
if (!configured || typeof configured !== "string") {
115+
return [];
116+
}
117+
return configured
118+
.split(",")
119+
.map((s) => s.trim())
120+
.filter(Boolean);
121+
}
122+
123+
/**
124+
* Check if a JSX node value starts with one of the inline element names.
125+
*/
126+
function isInlineElement(value, elements) {
127+
const trimmed = value.trim();
128+
for (const el of elements) {
129+
if (trimmed.startsWith(`<${el}>`) || trimmed.startsWith(`<${el} `)) {
130+
return true;
131+
}
132+
}
133+
return false;
134+
}
135+
136+
/**
137+
* Walk the AST and convert matching JSX nodes to HTML nodes.
138+
*/
139+
function transformAst(ast, elements) {
140+
function walk(node) {
141+
if (node.type === "jsx" && isInlineElement(node.value, elements)) {
142+
// Convert to HTML type so prettier outputs it verbatim
143+
node.type = "html";
144+
// Collapse multi-line content back to a single line
145+
node.value = collapseInlineJsx(node.value);
146+
}
147+
if (node.children) {
148+
node.children.forEach(walk);
149+
}
150+
}
151+
walk(ast);
152+
return ast;
153+
}
154+
155+
/** @type {import("prettier").Plugin} */
156+
const plugin = {
157+
options: {
158+
mdxInlineElements: {
159+
type: "string",
160+
category: "MDX",
161+
default: "",
162+
description:
163+
"Comma-separated list of JSX element names that should not be reformatted.",
164+
},
165+
},
166+
167+
parsers: {
168+
"mdx-inline": {
169+
async parse(text, options) {
170+
// Delegate to the built-in MDX parser via prettier's stable plugin export
171+
const { parsers } = await import("prettier/plugins/markdown");
172+
const ast = await parsers.mdx.parse(text, options);
173+
174+
// Transform matching JSX nodes to prevent reformatting
175+
const elements = getInlineElements(options);
176+
transformAst(ast, elements);
177+
178+
return ast;
179+
},
180+
// Use the built-in mdast printer — we only modify the AST
181+
astFormat: "mdast",
182+
locStart: (node) => node.position?.start?.offset ?? 0,
183+
locEnd: (node) => node.position?.end?.offset ?? 0,
184+
},
185+
},
186+
};
187+
188+
export default plugin;

src/content/docs/api-shield/security/jwt-validation/index.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import {
1414
DashButton,
1515
} from "~/components";
1616

17-
{/* prettier-ignore */}
1817
<GlossaryTooltip term="JSON web token (JWT)">JSON web tokens (JWT)</GlossaryTooltip> are often used as part of an authentication component on many web applications today. Since JWTs are crucial to identifying users and their access, ensuring the token’s integrity is important.
1918

2019
API Shield’s JWT validation stops JWT replay attacks and JWT tampering by cryptographically verifying incoming JWTs before they are passed to your API origin. JWT validation will also stop requests with expired tokens or tokens that are not yet valid.
@@ -126,7 +125,7 @@ API Shield will verify JSON Web Tokens regardless of whether or not they have th
126125

127126
### Ignore `OPTIONS` pre-flight CORS requests
128127

129-
Due to cross-origin resource sharing (CORS) security, web browsers will send "pre-flight" requests using the `OPTIONS` verb to API endpoints before sending a `GET` (or other verb) request. By definition, `OPTIONS` requests do not include headers or cookies and are anonymous.
128+
Due to cross-origin resource sharing (CORS) security, web browsers will send "pre-flight" requests using the `OPTIONS` verb to API endpoints before sending a `GET` (or other verb) request. By definition, `OPTIONS` requests do not include headers or cookies and are anonymous.
130129

131130
If you expect web browsers to be valid clients of your API, and to prevent blocking `OPTIONS` requests from those browsers, Cloudflare recommends adding `or http.request.method eq "OPTIONS"` to your JWT validation rules.
132131

@@ -137,6 +136,7 @@ If you expect web browsers to be valid clients of your API, and to prevent block
137136
JWT validation is available for all API Shield customers. Enterprise customers who have not purchased API Shield can preview [API Shield as a non-contract service](https://dash.cloudflare.com/?to=/:account/:zone/security/api-shield) in the Cloudflare dashboard or by contacting your account team.
138137

139138
---
139+
140140
## Limitations
141141

142142
Currently, the following known limitations exist:

src/content/docs/api-shield/security/schema-validation/index.mdx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ You can migrate to Schema validation 2.0 manually by uploading your schemas to t
2929

3030
## Process
3131

32-
{/* prettier-ignore */}
3332
<GlossaryTooltip term="API endpoint">Endpoints</GlossaryTooltip> must be added to [Endpoint Management](/api-shield/management-and-monitoring/endpoint-management/) for Schema validation to protect them. Uploading a schema via the Cloudflare dashboard will automatically add endpoints, or you can manually add them from [API Discovery](/api-shield/security/api-discovery/).
3433

3534
If you are uploading a schema via the API or Terraform, you must parse the schema and add your endpoints manually.
@@ -434,12 +433,12 @@ Schema validation inspects request bodies up to a maximum size that depends on y
434433

435434
The default body size limits are:
436435

437-
| Plan | Default body size limit |
438-
| --- | --- |
439-
| Free | 1 KB |
440-
| Pro | 8 KB |
441-
| Business | 8 KB |
442-
| Enterprise | 128 KB |
436+
| Plan | Default body size limit |
437+
| ---------- | ----------------------- |
438+
| Free | 1 KB |
439+
| Pro | 8 KB |
440+
| Business | 8 KB |
441+
| Enterprise | 128 KB |
443442

444443
:::note
445444
This limit is separate from the [WAF maximum body inspection size](/waf/managed-rules/#maximum-body-size), which controls how much of the request payload the WAF scans. Increasing one does not affect the other.

src/content/docs/durable-objects/api/base.mdx

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,7 @@ class MyDurableObject(DurableObject):
5656
5757
### `fetch`
5858
59-
- <code>
60-
fetch(request <Type text="Request" />)
61-
</code>
59+
- <code>fetch(request <Type text="Request" />)</code>
6260
: <Type text="Response" /> | <Type text="Promise<Response>" />- Takes an HTTP
6361
[Request](https://developers.cloudflare.com/workers/runtime-apis/request/) and
6462
returns an HTTP
@@ -93,16 +91,11 @@ export class MyDurableObject extends DurableObject<Env> {
9391

9492
### `alarm`
9593

96-
- <code>
97-
alarm(alarmInfo? <Type text="AlarmInvocationInfo" />)
98-
</code>
94+
- <code>alarm(alarmInfo? <Type text="AlarmInvocationInfo" />)</code>
9995
: <Type text="void" /> | <Type text="Promise<void>" />
10096
- Called by the system when a scheduled alarm time is reached.
101-
10297
- The `alarm()` handler has guaranteed at-least-once execution and will be retried upon failure using exponential backoff, starting at two second delays for up to six retries. Retries will be performed if the method fails with an uncaught exception.
103-
10498
- This method can be `async`.
105-
10699
- Refer to [Alarms](/durable-objects/api/alarms/) for more information.
107100

108101
#### Parameters
@@ -132,10 +125,7 @@ export class MyDurableObject extends DurableObject<Env> {
132125

133126
### `webSocketMessage`
134127

135-
- <code>
136-
webSocketMessage(ws <Type text="WebSocket" />, message{" "}
137-
<Type text="string | ArrayBuffer" />)
138-
</code>
128+
- <code>webSocketMessage(ws <Type text="WebSocket" />, message <Type text="string | ArrayBuffer" />)</code>
139129
: <Type text="void" /> | <Type text="Promise<void>" />- Called by the system
140130
when an accepted WebSocket receives a message. - This method is not called for
141131
WebSocket control frames. The system will respond to an incoming [WebSocket
@@ -170,10 +160,7 @@ export class MyDurableObject extends DurableObject<Env> {
170160

171161
### `webSocketClose`
172162

173-
- <code>
174-
webSocketClose(ws <Type text="WebSocket" />, code <Type text="number" />,
175-
reason <Type text="string" />, wasClean <Type text="boolean" />)
176-
</code>
163+
- <code>webSocketClose(ws <Type text="WebSocket" />, code <Type text="number" />, reason <Type text="string" />, wasClean <Type text="boolean" />)</code>
177164
: <Type text="void" /> | <Type text="Promise<void>" />- Called by the system
178165
when a WebSocket connection is closed.
179166
- With the [`web_socket_auto_reply_to_close`](/workers/configuration/compatibility-flags/#websocket-auto-reply-to-close) compatibility flag (enabled by default on compatibility dates on or after `2026-04-07`), the runtime automatically sends a reciprocal Close frame and transitions `readyState` to `CLOSED` before this handler is called. You do not need to call `ws.close()` — but doing so is safe (the call is silently ignored).
@@ -209,9 +196,7 @@ export class MyDurableObject extends DurableObject<Env> {
209196

210197
### `webSocketError`
211198

212-
- <code>
213-
webSocketError(ws <Type text="WebSocket" />, error <Type text="unknown" />)
214-
</code>
199+
- <code>webSocketError(ws <Type text="WebSocket" />, error <Type text="unknown" />)</code>
215200
: <Type text="void" /> | <Type text="Promise<void>" />- Called by the system
216201
when a non-disconnection error occurs on a WebSocket connection. - This method
217202
can be `async`.

0 commit comments

Comments
 (0)