jamdesk 1.1.196 → 1.1.197
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/frontmatter-utils.d.ts.map +1 -1
- package/dist/lib/frontmatter-utils.js +56 -2
- package/dist/lib/frontmatter-utils.js.map +1 -1
- package/package.json +1 -1
- package/vendored/lib/frontmatter-utils.ts +52 -2
- package/vendored/lib/mdx.ts +2 -1
- package/vendored/lib/remark-style-string-to-object.ts +482 -0
- package/vendored/lib/render-doc-page.tsx +2 -0
- package/vendored/lib/snippet-loader-isr.ts +13 -1
- package/vendored/scripts/build-search-index.cjs +38 -2
- package/vendored/workspace-package-lock.json +200 -200
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* remark-style-string-to-object
|
|
3
|
+
*
|
|
4
|
+
* MDX AST plugin that rewrites a string `style="…"` attribute on intrinsic JSX
|
|
5
|
+
* elements into a React style OBJECT.
|
|
6
|
+
*
|
|
7
|
+
* WHY: MDX has no raw HTML — everything angle-bracketed is parsed as JSX. So an
|
|
8
|
+
* embed pasted from a third-party site keeps its HTML `style` attribute and MDX
|
|
9
|
+
* hands React a plain string, which React refuses:
|
|
10
|
+
*
|
|
11
|
+
* The `style` prop expects a mapping from style properties to values, not a
|
|
12
|
+
* string. (minified: React error #62)
|
|
13
|
+
*
|
|
14
|
+
* That throw happens at RENDER, not at compile, so nothing that guards the
|
|
15
|
+
* compile step sees it: `render-doc-page.tsx` catches compile failures only,
|
|
16
|
+
* and the editor's `compileMdx` try/catch likewise. In the editor the throw
|
|
17
|
+
* escaped all the way to the dashboard's root error boundary and unmounted the
|
|
18
|
+
* whole app ("Something went wrong"); in production it breaks the page render.
|
|
19
|
+
* Reported by a customer whose iframe carried a scribehow.com embed's
|
|
20
|
+
* `style="aspect-ratio: 1 / 1; border: 0; min-height: 480px"`.
|
|
21
|
+
*
|
|
22
|
+
* Markdown-generated HTML does NOT have this problem — hast elements go through
|
|
23
|
+
* `hast-util-to-jsx-runtime`, which already converts `style` via `style-to-js`.
|
|
24
|
+
* The gap is specific to MDX JSX nodes, which bypass hast entirely, so the fix
|
|
25
|
+
* belongs at the mdast/JSX layer — here.
|
|
26
|
+
*
|
|
27
|
+
* Two AST shapes carry a `style` attribute, mirroring remark-svg-namespace-attrs
|
|
28
|
+
* (the same problem class: HTML-shaped attributes pasted into MDX):
|
|
29
|
+
* 1. mdast JSX nodes (`mdxJsxFlowElement`, `mdxJsxTextElement`) — the pasted
|
|
30
|
+
* `<iframe style="…">` in the doc body. The attribute value is a string.
|
|
31
|
+
* 2. estree JSX nodes inside expression containers
|
|
32
|
+
* (`mdxJsxAttributeValueExpression` for `<Card icon={<svg style="…"/>}>`,
|
|
33
|
+
* `mdxFlowExpression` / `mdxTextExpression` for `{<svg …/>}` blocks,
|
|
34
|
+
* `mdxjsEsm` for `export const Icon = () => <svg style="…"/>`). There the
|
|
35
|
+
* value is a `JSXAttribute` with a string `Literal`.
|
|
36
|
+
*
|
|
37
|
+
* SCOPE — intrinsic elements only. React throws for DOM elements; a custom
|
|
38
|
+
* component may legitimately declare `style?: string`, and rewriting its prop
|
|
39
|
+
* would break it. Element names are therefore converted only when they look
|
|
40
|
+
* intrinsic (lowercase first letter, no dot). Same rule React itself uses to
|
|
41
|
+
* decide between a DOM tag and a component.
|
|
42
|
+
*
|
|
43
|
+
* VALUES stay verbatim strings. React appends `px` to a bare NUMBER on most
|
|
44
|
+
* properties, so coercing `line-height: 2` to the number 2 would silently
|
|
45
|
+
* produce `line-height: 2px`. Keeping the source string also preserves
|
|
46
|
+
* `!important` (which React's SSR serializer emits as written).
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
import type { Root } from 'mdast';
|
|
50
|
+
import type { MdxJsxFlowElement, MdxJsxTextElement } from 'mdast-util-mdx-jsx';
|
|
51
|
+
import type { MdxFlowExpression, MdxTextExpression } from 'mdast-util-mdx-expression';
|
|
52
|
+
import type { MdxjsEsm } from 'mdast-util-mdxjs-esm';
|
|
53
|
+
import { visit } from 'unist-util-visit';
|
|
54
|
+
import { visit as visitEstree } from 'estree-util-visit';
|
|
55
|
+
import type {
|
|
56
|
+
Program,
|
|
57
|
+
JSXOpeningElement,
|
|
58
|
+
JSXIdentifier,
|
|
59
|
+
JSXMemberExpression,
|
|
60
|
+
JSXNamespacedName,
|
|
61
|
+
ObjectExpression,
|
|
62
|
+
Property,
|
|
63
|
+
} from 'estree-jsx';
|
|
64
|
+
|
|
65
|
+
/** One parsed CSS declaration: the React property key and its verbatim value. */
|
|
66
|
+
interface Declaration {
|
|
67
|
+
key: string;
|
|
68
|
+
value: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Does the quote opened at `open` ever close?
|
|
73
|
+
*
|
|
74
|
+
* An UNMATCHED quote must not put a scanner into string mode. If it does, every
|
|
75
|
+
* later `;` and `:` looks quoted, so the rest of the attribute folds into the
|
|
76
|
+
* current value and those declarations vanish with no error:
|
|
77
|
+
*
|
|
78
|
+
* font-family: Kim's Handwriting; color: red; border: 0
|
|
79
|
+
*
|
|
80
|
+
* — one apostrophe in a font name and the author silently loses `color` and
|
|
81
|
+
* `border`. A stray quote is far likelier than a deliberate unterminated string,
|
|
82
|
+
* so it is treated as an ordinary character and the remaining declarations
|
|
83
|
+
* survive.
|
|
84
|
+
*/
|
|
85
|
+
function quoteCloses(s: string, open: number, quote: string): boolean {
|
|
86
|
+
for (let i = open + 1; i < s.length; i += 1) {
|
|
87
|
+
if (s[i] === '\\') {
|
|
88
|
+
i += 1;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (s[i] === quote) return true;
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** True when a quote at `i` should open a string rather than be literal text. */
|
|
97
|
+
function opensString(s: string, i: number): boolean {
|
|
98
|
+
const ch = s[i];
|
|
99
|
+
return (ch === '"' || ch === "'") && quoteCloses(s, i, ch);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Index just past the `)` closing a `url(` whose body starts at `from`.
|
|
104
|
+
* Quotes are honoured so `url("a)b.png")` is not cut at the inner paren.
|
|
105
|
+
* An unclosed `url(` runs to the end of the value.
|
|
106
|
+
*/
|
|
107
|
+
function endOfUrlToken(s: string, from: number): number {
|
|
108
|
+
let quote: string | null = null;
|
|
109
|
+
for (let i = from; i < s.length; i += 1) {
|
|
110
|
+
const ch = s[i];
|
|
111
|
+
if (quote) {
|
|
112
|
+
if (ch === '\\') i += 1;
|
|
113
|
+
else if (ch === quote) quote = null;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (opensString(s, i)) quote = ch;
|
|
117
|
+
else if (ch === ')') return i + 1;
|
|
118
|
+
}
|
|
119
|
+
return s.length;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Split a `style` attribute into declarations.
|
|
124
|
+
*
|
|
125
|
+
* Hand-rolled rather than pulling in `style-to-object`: that package is present
|
|
126
|
+
* only TRANSITIVELY (via @mdx-js/mdx → hast-util-to-jsx-runtime → style-to-js),
|
|
127
|
+
* and this file is vendored verbatim into both `builder/cli/vendored/` and
|
|
128
|
+
* `dashboard/hosting/vendored/builder/`, where a direct dependency would have to
|
|
129
|
+
* be added to three package trees and the CLI's generated workspace lockfile.
|
|
130
|
+
*
|
|
131
|
+
* The grammar that actually matters here is small, but the two structures below
|
|
132
|
+
* are NOT optional — both appear in real pasted embeds:
|
|
133
|
+
* - parentheses: `url(data:image/png;base64,…)` contains BOTH `;` and `:`
|
|
134
|
+
* - quotes: `content: "a;b"`, `font-family: 'A;B'`
|
|
135
|
+
* so the scanner tracks quote state and paren depth and only breaks on a
|
|
136
|
+
* top-level, unquoted delimiter. Escapes inside quotes (`"\""`) are honoured so
|
|
137
|
+
* a backslash-escaped quote does not end the string early.
|
|
138
|
+
*/
|
|
139
|
+
function splitDeclarations(style: string): Declaration[] {
|
|
140
|
+
const out: Declaration[] = [];
|
|
141
|
+
style = stripCssComments(style);
|
|
142
|
+
let depth = 0;
|
|
143
|
+
let quote: string | null = null;
|
|
144
|
+
let start = 0;
|
|
145
|
+
|
|
146
|
+
const flush = (end: number): void => {
|
|
147
|
+
const decl = style.slice(start, end);
|
|
148
|
+
start = end + 1;
|
|
149
|
+
const parsed = parseDeclaration(decl);
|
|
150
|
+
if (parsed) out.push(parsed);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
for (let i = 0; i < style.length; i += 1) {
|
|
154
|
+
const ch = style[i];
|
|
155
|
+
if (quote) {
|
|
156
|
+
// A backslash escapes the next character inside a quoted string, so `\"`
|
|
157
|
+
// does not terminate it. Skip the escaped character outright.
|
|
158
|
+
if (ch === '\\') i += 1;
|
|
159
|
+
else if (ch === quote) quote = null;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (opensString(style, i)) quote = ch;
|
|
163
|
+
else if (ch === '(') depth += 1;
|
|
164
|
+
else if (ch === ')') depth = Math.max(0, depth - 1);
|
|
165
|
+
else if (ch === ';' && depth === 0) flush(i);
|
|
166
|
+
}
|
|
167
|
+
flush(style.length);
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Remove `/* … *\/` comments, which are legal anywhere in a style attribute and
|
|
173
|
+
* are NOT legal in a React style value.
|
|
174
|
+
*
|
|
175
|
+
* This is a silent-corruption guard, not tidiness. `color: red /* note *\/`
|
|
176
|
+
* would otherwise become the value `"red /* note *\/"`, and React assigns that
|
|
177
|
+
* straight to `style.color`, where it is invalid and the browser DROPS the whole
|
|
178
|
+
* declaration — the author's colour vanishes with no error anywhere. A leading
|
|
179
|
+
* comment is worse: `/* note *\/ color: red` parses the comment into the
|
|
180
|
+
* PROPERTY NAME and emits a garbage key.
|
|
181
|
+
*
|
|
182
|
+
* Quote-aware, because `content: "/*"` is a string, not a comment. An
|
|
183
|
+
* unterminated `/*` runs to the end of the value, matching the CSS spec.
|
|
184
|
+
*
|
|
185
|
+
* Also url-aware. An UNQUOTED `url(…)` is a single URL token and CSS does not
|
|
186
|
+
* recognise comments inside it, so a `/*` in a path — `url(https://cdn/a/*b.png)`
|
|
187
|
+
* — would otherwise open a comment that never closes and take the rest of the
|
|
188
|
+
* style with it, truncating the URL mid-string and deleting every later
|
|
189
|
+
* declaration. The token is copied through verbatim instead.
|
|
190
|
+
*/
|
|
191
|
+
function stripCssComments(style: string): string {
|
|
192
|
+
if (!style.includes('/*')) return style;
|
|
193
|
+
let out = '';
|
|
194
|
+
let quote: string | null = null;
|
|
195
|
+
for (let i = 0; i < style.length; i += 1) {
|
|
196
|
+
const ch = style[i];
|
|
197
|
+
if (quote) {
|
|
198
|
+
if (ch === '\\') {
|
|
199
|
+
out += ch + (style[i + 1] ?? '');
|
|
200
|
+
i += 1;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (ch === quote) quote = null;
|
|
204
|
+
out += ch;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (opensString(style, i)) {
|
|
208
|
+
quote = ch;
|
|
209
|
+
out += ch;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if ((ch === 'u' || ch === 'U') && /^url\(/i.test(style.slice(i, i + 4))) {
|
|
213
|
+
const end = endOfUrlToken(style, i + 4);
|
|
214
|
+
out += style.slice(i, end);
|
|
215
|
+
i = end - 1;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (ch === '/' && style[i + 1] === '*') {
|
|
219
|
+
const end = style.indexOf('*/', i + 2);
|
|
220
|
+
// Unterminated comment consumes the rest, per CSS.
|
|
221
|
+
if (end === -1) return out;
|
|
222
|
+
i = end + 1;
|
|
223
|
+
// A comment is a token separator, so leave whitespace behind rather than
|
|
224
|
+
// gluing `border/*x*\/0` into `border0`.
|
|
225
|
+
out += ' ';
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
out += ch;
|
|
229
|
+
}
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Split one declaration on its FIRST top-level colon. `background: url(a:b)`
|
|
235
|
+
* and `content: "a:b"` both carry later colons that are part of the value.
|
|
236
|
+
*/
|
|
237
|
+
function parseDeclaration(decl: string): Declaration | null {
|
|
238
|
+
let depth = 0;
|
|
239
|
+
let quote: string | null = null;
|
|
240
|
+
for (let i = 0; i < decl.length; i += 1) {
|
|
241
|
+
const ch = decl[i];
|
|
242
|
+
if (quote) {
|
|
243
|
+
if (ch === '\\') i += 1;
|
|
244
|
+
else if (ch === quote) quote = null;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (opensString(decl, i)) quote = ch;
|
|
248
|
+
else if (ch === '(') depth += 1;
|
|
249
|
+
else if (ch === ')') depth = Math.max(0, depth - 1);
|
|
250
|
+
else if (ch === ':' && depth === 0) {
|
|
251
|
+
const name = decl.slice(0, i).trim();
|
|
252
|
+
const value = decl.slice(i + 1).trim();
|
|
253
|
+
// A declaration needs both halves. `color:` (no value) and `: red` (no
|
|
254
|
+
// property) are dropped rather than emitted as an empty-keyed property,
|
|
255
|
+
// which would render as a stray `:value` in the style attribute.
|
|
256
|
+
if (!name || !value) return null;
|
|
257
|
+
return { key: toReactProperty(name), value };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// No colon at all — `novalue`. Not a declaration.
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Convert a CSS property name to the key React expects.
|
|
266
|
+
*
|
|
267
|
+
* aspect-ratio → aspectRatio
|
|
268
|
+
* -webkit-line-clamp → WebkitLineClamp (React capitalises the vendor prefix)
|
|
269
|
+
* -moz-appearance → MozAppearance
|
|
270
|
+
* -ms-flex → msFlex (the documented -ms- exception:
|
|
271
|
+
* lowercase `ms`, unlike every other
|
|
272
|
+
* vendor prefix — matches React's own
|
|
273
|
+
* hyphenation table)
|
|
274
|
+
* --brand-color → --brand-color (custom property, passed through:
|
|
275
|
+
* React sets these with
|
|
276
|
+
* style.setProperty and camelCasing
|
|
277
|
+
* would break the lookup)
|
|
278
|
+
*
|
|
279
|
+
* A kebab key is not merely untidy — React renders it but logs "Unsupported
|
|
280
|
+
* style property aspect-ratio. Did you mean aspectRatio?" on every render, which
|
|
281
|
+
* is the exact noise remark-svg-namespace-attrs was written to eliminate.
|
|
282
|
+
*/
|
|
283
|
+
function toReactProperty(name: string): string {
|
|
284
|
+
// CSS custom property. Two leading dashes, case-sensitive, never camelCased.
|
|
285
|
+
if (name.startsWith('--')) return name;
|
|
286
|
+
|
|
287
|
+
const lower = name.toLowerCase();
|
|
288
|
+
// `-ms-` is the one prefix React lowercases; every other `-x-` prefix gets an
|
|
289
|
+
// upper-cased first letter. Strip the leading dash first so the generic
|
|
290
|
+
// kebab→camel pass below sees a normal name.
|
|
291
|
+
if (lower.startsWith('-ms-')) return camelCase(lower.slice(1));
|
|
292
|
+
if (lower.startsWith('-')) {
|
|
293
|
+
const camel = camelCase(lower.slice(1));
|
|
294
|
+
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
|
295
|
+
}
|
|
296
|
+
return camelCase(lower);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function camelCase(name: string): string {
|
|
300
|
+
return name.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Build the estree ObjectExpression React will receive as the `style` prop. */
|
|
304
|
+
function buildObjectExpression(declarations: Declaration[]): ObjectExpression {
|
|
305
|
+
const properties: Property[] = declarations.map((d) => ({
|
|
306
|
+
type: 'Property',
|
|
307
|
+
method: false,
|
|
308
|
+
shorthand: false,
|
|
309
|
+
computed: false,
|
|
310
|
+
// Always a string-literal key: custom properties (`--brand`) and vendor
|
|
311
|
+
// forms are not valid bare identifiers, and a quoted key is valid for the
|
|
312
|
+
// camelCase ones too, so one shape covers every case.
|
|
313
|
+
key: { type: 'Literal', value: d.key },
|
|
314
|
+
value: { type: 'Literal', value: d.value },
|
|
315
|
+
kind: 'init',
|
|
316
|
+
}));
|
|
317
|
+
return { type: 'ObjectExpression', properties };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Serialize declarations back to a JS object literal source string.
|
|
322
|
+
*
|
|
323
|
+
* MDX carries both an estree (`data.estree`) and the raw `value` source for an
|
|
324
|
+
* attribute expression, and downstream tooling reads either one, so both are
|
|
325
|
+
* emitted and must agree. JSON.stringify gives correctly escaped keys and
|
|
326
|
+
* values for the Literal nodes above.
|
|
327
|
+
*/
|
|
328
|
+
function toSource(declarations: Declaration[]): string {
|
|
329
|
+
const body = declarations
|
|
330
|
+
.map((d) => `${JSON.stringify(d.key)}: ${JSON.stringify(d.value)}`)
|
|
331
|
+
.join(', ');
|
|
332
|
+
return `{${body}}`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* React treats a lowercase-initial, dot-free tag as a DOM element and anything
|
|
337
|
+
* else as a component. Only DOM elements throw on a string `style`, and only
|
|
338
|
+
* they are safe to rewrite.
|
|
339
|
+
*/
|
|
340
|
+
function isIntrinsic(name: string | null | undefined): boolean {
|
|
341
|
+
if (!name) return false;
|
|
342
|
+
if (name.includes('.')) return false;
|
|
343
|
+
const first = name.charAt(0);
|
|
344
|
+
return first === first.toLowerCase() && first !== first.toUpperCase();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Rewrite `style="…"` on an mdast JSX element.
|
|
349
|
+
*
|
|
350
|
+
* An unparseable style (`style=""`, `style=";;"`, `style="novalue"`) has its
|
|
351
|
+
* attribute REMOVED, not blanked. A JSX attribute carrying no value is
|
|
352
|
+
* `style={true}`, and React's guard is `styles != null && typeof styles !==
|
|
353
|
+
* 'object'` — `true` fails it and throws error #62 exactly like the string did.
|
|
354
|
+
* `rewriteJsxOpening` drops it for the same reason.
|
|
355
|
+
*/
|
|
356
|
+
function rewriteMdastAttrs(node: MdxJsxFlowElement | MdxJsxTextElement): void {
|
|
357
|
+
if (!isIntrinsic(node.name)) return;
|
|
358
|
+
const kept: typeof node.attributes = [];
|
|
359
|
+
for (const attr of node.attributes) {
|
|
360
|
+
// Only a literal string `style`. An existing `style={{…}}` arrives as an
|
|
361
|
+
// mdxJsxAttributeValueExpression and is already correct.
|
|
362
|
+
if (
|
|
363
|
+
attr.type !== 'mdxJsxAttribute' ||
|
|
364
|
+
attr.name !== 'style' ||
|
|
365
|
+
typeof attr.value !== 'string'
|
|
366
|
+
) {
|
|
367
|
+
kept.push(attr);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
const declarations = splitDeclarations(attr.value);
|
|
371
|
+
if (!declarations.length) continue; // drop, per the note above
|
|
372
|
+
attr.value = {
|
|
373
|
+
type: 'mdxJsxAttributeValueExpression',
|
|
374
|
+
value: toSource(declarations),
|
|
375
|
+
data: {
|
|
376
|
+
estree: {
|
|
377
|
+
type: 'Program',
|
|
378
|
+
sourceType: 'module',
|
|
379
|
+
comments: [],
|
|
380
|
+
body: [
|
|
381
|
+
{
|
|
382
|
+
type: 'ExpressionStatement',
|
|
383
|
+
expression: buildObjectExpression(declarations),
|
|
384
|
+
},
|
|
385
|
+
],
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
};
|
|
389
|
+
kept.push(attr);
|
|
390
|
+
}
|
|
391
|
+
node.attributes = kept;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function jsxName(
|
|
395
|
+
nameNode: JSXIdentifier | JSXMemberExpression | JSXNamespacedName,
|
|
396
|
+
): string {
|
|
397
|
+
if (nameNode.type === 'JSXIdentifier') return nameNode.name;
|
|
398
|
+
if (nameNode.type === 'JSXNamespacedName') {
|
|
399
|
+
return `${nameNode.namespace.name}:${nameNode.name.name}`;
|
|
400
|
+
}
|
|
401
|
+
return jsxName(nameNode.object) + '.' + jsxName(nameNode.property);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** Rewrite `style="…"` on a single estree JSX opening element. */
|
|
405
|
+
function rewriteJsxOpening(opening: JSXOpeningElement): void {
|
|
406
|
+
if (!isIntrinsic(jsxName(opening.name))) return;
|
|
407
|
+
const kept: typeof opening.attributes = [];
|
|
408
|
+
for (const attr of opening.attributes) {
|
|
409
|
+
// Only a bare string literal on a `style` attribute — `style={{…}}` is a
|
|
410
|
+
// JSXExpressionContainer and is already correct.
|
|
411
|
+
if (
|
|
412
|
+
attr.type !== 'JSXAttribute' ||
|
|
413
|
+
attr.name.type !== 'JSXIdentifier' ||
|
|
414
|
+
attr.name.name !== 'style' ||
|
|
415
|
+
!attr.value ||
|
|
416
|
+
attr.value.type !== 'Literal' ||
|
|
417
|
+
typeof attr.value.value !== 'string'
|
|
418
|
+
) {
|
|
419
|
+
kept.push(attr);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
const declarations = splitDeclarations(attr.value.value);
|
|
423
|
+
if (!declarations.length) continue; // drop, per the note on rewriteMdastAttrs
|
|
424
|
+
attr.value = {
|
|
425
|
+
type: 'JSXExpressionContainer',
|
|
426
|
+
expression: buildObjectExpression(declarations),
|
|
427
|
+
};
|
|
428
|
+
kept.push(attr);
|
|
429
|
+
}
|
|
430
|
+
opening.attributes = kept;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function rewriteEstreeProgram(estree: Program | null | undefined): void {
|
|
434
|
+
if (!estree) return;
|
|
435
|
+
visitEstree(estree, (node) => {
|
|
436
|
+
if (node.type === 'JSXElement') {
|
|
437
|
+
rewriteJsxOpening(node.openingElement);
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export function remarkStyleStringToObject() {
|
|
443
|
+
return (tree: Root) => {
|
|
444
|
+
visit(
|
|
445
|
+
tree,
|
|
446
|
+
[
|
|
447
|
+
'mdxJsxFlowElement',
|
|
448
|
+
'mdxJsxTextElement',
|
|
449
|
+
'mdxFlowExpression',
|
|
450
|
+
'mdxTextExpression',
|
|
451
|
+
'mdxjsEsm',
|
|
452
|
+
],
|
|
453
|
+
(node) => {
|
|
454
|
+
if (node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') {
|
|
455
|
+
const jsxNode = node as MdxJsxFlowElement | MdxJsxTextElement;
|
|
456
|
+
rewriteMdastAttrs(jsxNode);
|
|
457
|
+
// Also walk estree subtrees inside attribute value expressions —
|
|
458
|
+
// `<Card icon={<svg style="…"/>}>`. Runs regardless of whether the
|
|
459
|
+
// OUTER element was intrinsic: the inner element is judged on its own.
|
|
460
|
+
for (const attr of jsxNode.attributes) {
|
|
461
|
+
if (attr.type !== 'mdxJsxAttribute') continue;
|
|
462
|
+
const value = attr.value;
|
|
463
|
+
if (
|
|
464
|
+
value &&
|
|
465
|
+
typeof value === 'object' &&
|
|
466
|
+
value.type === 'mdxJsxAttributeValueExpression'
|
|
467
|
+
) {
|
|
468
|
+
rewriteEstreeProgram(value.data?.estree);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
} else {
|
|
472
|
+
// mdxFlowExpression / mdxTextExpression — body-level `{…}` blocks.
|
|
473
|
+
// mdxjsEsm — `export const Icon = () => <svg style="…"/>`, whose JSX
|
|
474
|
+
// exists only in estree.
|
|
475
|
+
rewriteEstreeProgram(
|
|
476
|
+
(node as MdxFlowExpression | MdxTextExpression | MdxjsEsm).data?.estree,
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
);
|
|
481
|
+
};
|
|
482
|
+
}
|
|
@@ -40,6 +40,7 @@ import { MARKDOWN_BLOCK_STRUCTURE } from '@/components/mdx/markdown-classes';
|
|
|
40
40
|
import { rehypeCodeMeta, rehypeRestoreDataTitle } from '@/lib/rehype-code-meta';
|
|
41
41
|
import { rehypeClassToClassName } from '@/lib/rehype-class-to-classname';
|
|
42
42
|
import { remarkSvgNamespaceAttrs } from '@/lib/remark-svg-namespace-attrs';
|
|
43
|
+
import { remarkStyleStringToObject } from '@/lib/remark-style-string-to-object';
|
|
43
44
|
import { rehypeNoZoomToData } from '@/lib/rehype-nozoom-to-data';
|
|
44
45
|
import { rehypeUnwrapNestedAnchors } from './rehype-unwrap-nested-anchors';
|
|
45
46
|
import { loadSnippetsAndInlineComponents } from './render-doc-page-parallel-helpers';
|
|
@@ -139,6 +140,7 @@ function getCommonMdxOptions(
|
|
|
139
140
|
remarkGfm,
|
|
140
141
|
[remarkVisibility, { audience: 'humans' }],
|
|
141
142
|
remarkSvgNamespaceAttrs,
|
|
143
|
+
remarkStyleStringToObject,
|
|
142
144
|
...getTypographyRemarkPlugins(config),
|
|
143
145
|
...getLatexRemarkPlugins(config),
|
|
144
146
|
],
|
|
@@ -17,6 +17,8 @@ import { extractSnippetImports, normalizeSnippetPath } from './preprocess-mdx';
|
|
|
17
17
|
import { filterVisibility } from './visibility-filter';
|
|
18
18
|
import { injectPromptSources } from './inject-prompt-source';
|
|
19
19
|
import { mdxSecurityOptions } from './mdx-security-options';
|
|
20
|
+
import { remarkSvgNamespaceAttrs } from './remark-svg-namespace-attrs';
|
|
21
|
+
import { remarkStyleStringToObject } from './remark-style-string-to-object';
|
|
20
22
|
import {
|
|
21
23
|
mdxEvalGuardPlugin,
|
|
22
24
|
guardPropertyKey,
|
|
@@ -271,7 +273,17 @@ async function compilePlainMdxSnippet(
|
|
|
271
273
|
const { content } = await compileMDX({
|
|
272
274
|
source,
|
|
273
275
|
components: builtInComponents,
|
|
274
|
-
options: {
|
|
276
|
+
options: {
|
|
277
|
+
...mdxSecurityOptions,
|
|
278
|
+
parseFrontmatter: true,
|
|
279
|
+
// Both HTML-shaped-attribute repairs, matching every other compile site
|
|
280
|
+
// (pinned by remark-svg-namespace-attrs-coverage.test.ts). A pasted
|
|
281
|
+
// `<iframe style="…">` throws React error #62 at render here just as it
|
|
282
|
+
// does on a page, and a snippet crash takes down every page that includes
|
|
283
|
+
// it. The REHYPE pipeline is still deliberately omitted — see the note
|
|
284
|
+
// above about D2 fences.
|
|
285
|
+
mdxOptions: { remarkPlugins: [remarkSvgNamespaceAttrs, remarkStyleStringToObject] },
|
|
286
|
+
},
|
|
275
287
|
});
|
|
276
288
|
const PlainMdxSnippet: AnyComponent = () => content as React.ReactElement;
|
|
277
289
|
PlainMdxSnippet.displayName = 'PlainMdxSnippet';
|
|
@@ -207,17 +207,21 @@ function preprocessFrontmatter(content) {
|
|
|
207
207
|
const body = content.slice(frontmatterEnd);
|
|
208
208
|
|
|
209
209
|
const lines = frontmatter.split('\n');
|
|
210
|
+
// Lines belonging to a previous key's multi-line value are VALUE TEXT, not
|
|
211
|
+
// key/value pairs. Mirror of lib/frontmatter-utils.ts.
|
|
212
|
+
const isContinuation = markContinuationLines(lines);
|
|
210
213
|
const fixedLines = lines.map((line, i) => {
|
|
211
214
|
if (!line.trim() || line.trim().startsWith('#')) return line;
|
|
215
|
+
if (isContinuation[i]) return line;
|
|
212
216
|
|
|
213
217
|
// Key may contain colon-segments (og:title, twitter:card) — split on the
|
|
214
218
|
// first colon followed by whitespace/EOL. Mirror of lib/frontmatter-utils.ts.
|
|
215
|
-
const match = line.match(
|
|
219
|
+
const match = line.match(KEY_LINE);
|
|
216
220
|
if (!match) return line;
|
|
217
221
|
|
|
218
222
|
const [, indent, key, value] = match;
|
|
219
223
|
|
|
220
|
-
if (!value || value.startsWith('"') || value.startsWith("'") ||
|
|
224
|
+
if (!value || value.startsWith('"') || value.startsWith("'") || isBlockScalar(value)) {
|
|
221
225
|
return line;
|
|
222
226
|
}
|
|
223
227
|
|
|
@@ -240,6 +244,38 @@ function preprocessFrontmatter(content) {
|
|
|
240
244
|
return '---\n' + fixedLines.join('\n') + endMatch[0] + body;
|
|
241
245
|
}
|
|
242
246
|
|
|
247
|
+
/** Mirror of KEY_LINE in lib/frontmatter-utils.ts. */
|
|
248
|
+
const KEY_LINE =
|
|
249
|
+
/^(\s*)([a-zA-Z_][a-zA-Z0-9_.-]*(?::[a-zA-Z0-9_.-]+)*):\s*(.*)$/;
|
|
250
|
+
|
|
251
|
+
/** Mirror of isBlockScalar in lib/frontmatter-utils.ts. */
|
|
252
|
+
function isBlockScalar(value) {
|
|
253
|
+
return /^[|>][-+]?\d*$/.test(value.trim());
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Mirror of markContinuationLines in lib/frontmatter-utils.ts. Without it the
|
|
258
|
+
* search index rebuilt a wrapped value's continuation as `key: value` — the
|
|
259
|
+
* added space landed in a customer's indexed description, and where the line
|
|
260
|
+
* was a plain (non-block) wrap it made js-yaml reject the page outright.
|
|
261
|
+
*/
|
|
262
|
+
function markContinuationLines(lines) {
|
|
263
|
+
const out = new Array(lines.length).fill(false);
|
|
264
|
+
for (let i = 0; i < lines.length; i++) {
|
|
265
|
+
if (out[i]) continue;
|
|
266
|
+
const match = lines[i].match(KEY_LINE);
|
|
267
|
+
if (!match) continue;
|
|
268
|
+
const [, indent, , value] = match;
|
|
269
|
+
if (!isBlockScalar(value) && !hasFoldedContinuation(lines, i)) continue;
|
|
270
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
271
|
+
if (!lines[j].trim()) { out[j] = true; continue; }
|
|
272
|
+
if (lines[j].search(/\S/) <= indent.length) break;
|
|
273
|
+
out[j] = true;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return out;
|
|
277
|
+
}
|
|
278
|
+
|
|
243
279
|
/** Mirror of hasFoldedContinuation in lib/frontmatter-utils.ts. */
|
|
244
280
|
function hasFoldedContinuation(lines, i) {
|
|
245
281
|
for (let j = i + 1; j < lines.length; j++) {
|