@mintlify/common 1.0.1122 → 1.0.1124
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/mdx/index.d.ts +1 -0
- package/dist/mdx/index.js +1 -0
- package/dist/mdx/plugins/remark/remarkMdxExpandExpressions.d.ts +8 -5
- package/dist/mdx/plugins/remark/remarkMdxExpandExpressions.js +30 -5
- package/dist/mdx/preprocessCustomHeadingIds.d.ts +5 -0
- package/dist/mdx/preprocessCustomHeadingIds.js +2 -2
- package/dist/mdx/preprocessMdxFragmentCode.d.ts +36 -0
- package/dist/mdx/preprocessMdxFragmentCode.js +161 -0
- package/dist/mdx/preprocessMdxFragmentCode.test.d.ts +1 -0
- package/dist/mdx/preprocessMdxFragmentCode.test.js +81 -0
- package/dist/mdx/remark.js +2 -1
- package/dist/mdx/server-only/getMdx.js +3 -2
- package/dist/mdx/utils.js +2 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +2 -2
package/dist/mdx/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export * from './utils.js';
|
|
|
4
4
|
export * from './plugins/index.js';
|
|
5
5
|
export * from './lib/index.js';
|
|
6
6
|
export * from './preprocessCustomHeadingIds.js';
|
|
7
|
+
export * from './preprocessMdxFragmentCode.js';
|
|
7
8
|
export * from './getMDXOptions.js';
|
|
8
9
|
export * from './astUtils.js';
|
|
9
10
|
export * from './getS3ImageUri.js';
|
package/dist/mdx/index.js
CHANGED
|
@@ -4,6 +4,7 @@ export * from './utils.js';
|
|
|
4
4
|
export * from './plugins/index.js';
|
|
5
5
|
export * from './lib/index.js';
|
|
6
6
|
export * from './preprocessCustomHeadingIds.js';
|
|
7
|
+
export * from './preprocessMdxFragmentCode.js';
|
|
7
8
|
export * from './getMDXOptions.js';
|
|
8
9
|
export * from './astUtils.js';
|
|
9
10
|
export * from './getS3ImageUri.js';
|
|
@@ -70,11 +70,14 @@ export declare const MAX_MDX_FRAGMENTS_PER_PAGE = 500;
|
|
|
70
70
|
* cannot live inside an expression and are dropped. In an inline (`mdxTextExpression`) a lone
|
|
71
71
|
* `<p>` wrapper is unwrapped so it does not nest in the surrounding paragraph.
|
|
72
72
|
*
|
|
73
|
-
* Escaping: the fragment source is JSX text, so a literal brace must be written the JSX
|
|
74
|
-
* e.g. `{'{'}` / `{'}'}`; that expression is then evaluated by MDX and renders the brace. A
|
|
75
|
-
* heading anchor (`## Title {#id}`) is fine: `preprocessCustomHeadingIds` rewrites it before
|
|
76
|
-
* expression is parsed, at any indent inside `<MDX>`.
|
|
77
|
-
*
|
|
73
|
+
* Escaping: the fragment source is JSX text, so a literal brace in prose must be written the JSX
|
|
74
|
+
* way, e.g. `{'{'}` / `{'}'}`; that expression is then evaluated by MDX and renders the brace. A
|
|
75
|
+
* custom heading anchor (`## Title {#id}`) is fine: `preprocessCustomHeadingIds` rewrites it before
|
|
76
|
+
* the expression is parsed, at any indent inside `<MDX>`. Code is exempt from all of this:
|
|
77
|
+
* `preprocessMdxFragmentCode` escapes `<`, `>`, `{`, `}` inside fences and inline code before any
|
|
78
|
+
* parse, and `decodeFragmentCode` below decodes them (plus `&`) once the fragment's code sits
|
|
79
|
+
* in `code`/`inlineCode` nodes, so code behaves exactly as it does at the page root. Nothing
|
|
80
|
+
* beyond dedenting is done to the recovered text.
|
|
78
81
|
*
|
|
79
82
|
* Components defined on the page (`export const Local = () => …`, and named snippet imports,
|
|
80
83
|
* which prebuild rewrites into page exports) are passed into the fragment pipeline as known
|
|
@@ -16,6 +16,7 @@ import remarkRehype from 'remark-rehype';
|
|
|
16
16
|
import { unified } from 'unified';
|
|
17
17
|
import { visit } from 'unist-util-visit';
|
|
18
18
|
import { visitParents } from 'unist-util-visit-parents';
|
|
19
|
+
import { decodeMdxFragmentCode } from '../../preprocessMdxFragmentCode.js';
|
|
19
20
|
import { getAST } from '../../remark.js';
|
|
20
21
|
import { getExportedFunctionNames } from './remarkMdxClientComponentBoundaries.js';
|
|
21
22
|
const REMARK_REHYPE_OPTIONS = {
|
|
@@ -60,11 +61,14 @@ export const MAX_MDX_FRAGMENTS_PER_PAGE = 500;
|
|
|
60
61
|
* cannot live inside an expression and are dropped. In an inline (`mdxTextExpression`) a lone
|
|
61
62
|
* `<p>` wrapper is unwrapped so it does not nest in the surrounding paragraph.
|
|
62
63
|
*
|
|
63
|
-
* Escaping: the fragment source is JSX text, so a literal brace must be written the JSX
|
|
64
|
-
* e.g. `{'{'}` / `{'}'}`; that expression is then evaluated by MDX and renders the brace. A
|
|
65
|
-
* heading anchor (`## Title {#id}`) is fine: `preprocessCustomHeadingIds` rewrites it before
|
|
66
|
-
* expression is parsed, at any indent inside `<MDX>`.
|
|
67
|
-
*
|
|
64
|
+
* Escaping: the fragment source is JSX text, so a literal brace in prose must be written the JSX
|
|
65
|
+
* way, e.g. `{'{'}` / `{'}'}`; that expression is then evaluated by MDX and renders the brace. A
|
|
66
|
+
* custom heading anchor (`## Title {#id}`) is fine: `preprocessCustomHeadingIds` rewrites it before
|
|
67
|
+
* the expression is parsed, at any indent inside `<MDX>`. Code is exempt from all of this:
|
|
68
|
+
* `preprocessMdxFragmentCode` escapes `<`, `>`, `{`, `}` inside fences and inline code before any
|
|
69
|
+
* parse, and `decodeFragmentCode` below decodes them (plus `&`) once the fragment's code sits
|
|
70
|
+
* in `code`/`inlineCode` nodes, so code behaves exactly as it does at the page root. Nothing
|
|
71
|
+
* beyond dedenting is done to the recovered text.
|
|
68
72
|
*
|
|
69
73
|
* Components defined on the page (`export const Local = () => …`, and named snippet imports,
|
|
70
74
|
* which prebuild rewrites into page exports) are passed into the fragment pipeline as known
|
|
@@ -86,6 +90,7 @@ export const MAX_MDX_FRAGMENTS_PER_PAGE = 500;
|
|
|
86
90
|
export const remarkMdxExpandExpressions = (options) => (tree) => __awaiter(void 0, void 0, void 0, function* () {
|
|
87
91
|
var _a, _b, _c;
|
|
88
92
|
const depth = (_a = options.depth) !== null && _a !== void 0 ? _a : 0;
|
|
93
|
+
decodeFragmentCode(tree, depth > 0);
|
|
89
94
|
const expressions = [];
|
|
90
95
|
visitParents(tree, (node, ancestors) => {
|
|
91
96
|
if (node.type !== 'mdxFlowExpression' && node.type !== 'mdxTextExpression')
|
|
@@ -194,6 +199,26 @@ const expandExpression = (node_1, options_1, _a) => __awaiter(void 0, [node_1, o
|
|
|
194
199
|
node.data.tableOfContents = headings;
|
|
195
200
|
}
|
|
196
201
|
});
|
|
202
|
+
/**
|
|
203
|
+
* Undoes `preprocessMdxFragmentCode`'s escaping now that the code content sits safely inside
|
|
204
|
+
* `code`/`inlineCode` nodes. On the page tree (`insideFragment` false) only code inside a
|
|
205
|
+
* block-level `<MDX>` element was escaped; on a fragment tree everything came from inside the
|
|
206
|
+
* `<MDX>` tags, so every code node is decoded. Each tree passes through this exactly once —
|
|
207
|
+
* a nested block-level `<MDX>` is part of the enclosing tree, and a nested expression `<MDX>`
|
|
208
|
+
* is parsed into a fresh tree the fragment pipeline's own instance of this plugin decodes.
|
|
209
|
+
*/
|
|
210
|
+
const decodeFragmentCode = (tree, insideFragment) => {
|
|
211
|
+
visitParents(tree, (node, ancestors) => {
|
|
212
|
+
if (node.type !== 'code' && node.type !== 'inlineCode')
|
|
213
|
+
return;
|
|
214
|
+
const insideMdxElement = ancestors.some((ancestor) => (ancestor.type === 'mdxJsxFlowElement' || ancestor.type === 'mdxJsxTextElement') &&
|
|
215
|
+
'name' in ancestor &&
|
|
216
|
+
ancestor.name === 'MDX');
|
|
217
|
+
if (insideFragment || insideMdxElement) {
|
|
218
|
+
node.value = decodeMdxFragmentCode(node.value);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
};
|
|
197
222
|
const collectMdxElements = (expression) => {
|
|
198
223
|
const elements = [];
|
|
199
224
|
walk(expression, {
|
|
@@ -23,6 +23,11 @@ export interface PreprocessCustomHeadingIdsOptions {
|
|
|
23
23
|
* Without this the `{#id}` would reach the expression parser as a JSX expression and fail.
|
|
24
24
|
*/
|
|
25
25
|
export declare function preprocessCustomHeadingIds(content: string, options?: PreprocessCustomHeadingIdsOptions): string;
|
|
26
|
+
export declare function parseOpeningFence(stripped: string): {
|
|
27
|
+
char: string;
|
|
28
|
+
count: number;
|
|
29
|
+
} | undefined;
|
|
30
|
+
export declare function isClosingFence(stripped: string, fenceChar: string, fenceCount: number): boolean;
|
|
26
31
|
export interface JsxHeadingElement extends MdxJsxFlowElement {
|
|
27
32
|
readonly __jsxHeading: true;
|
|
28
33
|
}
|
|
@@ -75,7 +75,7 @@ export function preprocessCustomHeadingIds(content, options) {
|
|
|
75
75
|
}
|
|
76
76
|
return result.join('\n');
|
|
77
77
|
}
|
|
78
|
-
function parseOpeningFence(stripped) {
|
|
78
|
+
export function parseOpeningFence(stripped) {
|
|
79
79
|
const char = stripped[0];
|
|
80
80
|
if (char !== '`' && char !== '~')
|
|
81
81
|
return undefined;
|
|
@@ -86,7 +86,7 @@ function parseOpeningFence(stripped) {
|
|
|
86
86
|
return undefined;
|
|
87
87
|
return { char, count };
|
|
88
88
|
}
|
|
89
|
-
function isClosingFence(stripped, fenceChar, fenceCount) {
|
|
89
|
+
export function isClosingFence(stripped, fenceChar, fenceCount) {
|
|
90
90
|
let i = 0;
|
|
91
91
|
while (i < stripped.length && stripped[i] === fenceChar)
|
|
92
92
|
i++;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escapes `<`, `>`, `{` and `}` inside code fences and inline code within an `<MDX>` body, so code
|
|
3
|
+
* there needs no hand-escaping and parses exactly like code at the page root.
|
|
4
|
+
*
|
|
5
|
+
* The body of an `<MDX>` inside a `{…}` expression is JSX text to the MDX parser: a raw `<` starts
|
|
6
|
+
* a tag and a raw `{` starts an expression, so ```mint <cmd>``` or a fence containing
|
|
7
|
+
* `<project-root>` fails the whole page with "Could not parse expression with acorn" — a code
|
|
8
|
+
* fence gives no protection because it only becomes code later, when `remarkMdxExpandExpressions`
|
|
9
|
+
* re-parses the body as markdown. This pass rewrites those characters to character references
|
|
10
|
+
* (valid JSX text) before any MDX parse; `remarkMdxExpandExpressions` decodes them
|
|
11
|
+
* (`decodeMdxFragmentCode`) once the body has been parsed and the code content is safely inside
|
|
12
|
+
* `code`/`inlineCode` nodes.
|
|
13
|
+
*
|
|
14
|
+
* Runs as a string pass alongside `preprocessCustomHeadingIds`, with the same line scan and fence
|
|
15
|
+
* tracking: content inside a page-level fence is never touched (an `<MDX>` there is example code),
|
|
16
|
+
* and prose/JSX inside an `<MDX>` body is left alone — only fence content and inline code spans
|
|
17
|
+
* are rewritten, including spans on the same line as the `<MDX>` tags. Block-level `<MDX>` bodies
|
|
18
|
+
* get the same treatment so both forms behave identically; their code nodes are decoded by the
|
|
19
|
+
* same plugin. Constructs the line scan cannot see — an inline code span or an opening tag
|
|
20
|
+
* spanning lines, a span whose content contains backticks — are left alone and fail open: the
|
|
21
|
+
* page parses (or fails) exactly as it did before this pass existed, and nothing is corrupted.
|
|
22
|
+
*
|
|
23
|
+
* The pass is idempotent (nothing it emits is rewritten again), so stacked parse entries — e.g.
|
|
24
|
+
* prebuild normalizing a page that `getMdx` later compiles, or a fragment re-parse hitting
|
|
25
|
+
* `getAST` — are safe by construction, and a fragment sliced out of its `<MDX>` tags no longer
|
|
26
|
+
* matches the scanner at all.
|
|
27
|
+
*/
|
|
28
|
+
export declare function preprocessMdxFragmentCode(content: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Inverse of the escape above, applied by `remarkMdxExpandExpressions` to `code`/`inlineCode`
|
|
31
|
+
* node values inside `<MDX>`. Decodes exactly the references the escape emits plus `&`, which
|
|
32
|
+
* makes hand-written references work the way JSX text says they should (`<` is `<`) and keeps
|
|
33
|
+
* every literal spellable (`&lt;` is a literal `<`). Single-pass, so decoded output is
|
|
34
|
+
* never re-decoded.
|
|
35
|
+
*/
|
|
36
|
+
export declare const decodeMdxFragmentCode: (value: string) => string;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { isClosingFence, parseOpeningFence } from './preprocessCustomHeadingIds.js';
|
|
2
|
+
// `<MDX>` or `<MDX className="x">`; not `<MDX />` (no body) and not `<MDXProvider>`
|
|
3
|
+
const MDX_OPEN_RE = /<MDX(?:\s[^>]*[^/>\s])?\s*>/g;
|
|
4
|
+
const MDX_CLOSE_RE = /<\/MDX\s*>/g;
|
|
5
|
+
/**
|
|
6
|
+
* Characters that end or break the JSX parse of an `<MDX>` body, mapped to the character
|
|
7
|
+
* references acorn treats as plain JSX text. `&` is deliberately absent: leaving it alone makes
|
|
8
|
+
* the escape idempotent, so the preprocessor can run at every parse entry (getMdx, getAST,
|
|
9
|
+
* prebuild, editor validation) without the passes having to know about each other.
|
|
10
|
+
*/
|
|
11
|
+
const CODE_ESCAPES = {
|
|
12
|
+
'<': '<',
|
|
13
|
+
'>': '>',
|
|
14
|
+
'{': '{',
|
|
15
|
+
'}': '}',
|
|
16
|
+
};
|
|
17
|
+
const CODE_ESCAPE_RE = /[<>{}]/g;
|
|
18
|
+
const CODE_UNESCAPES = {
|
|
19
|
+
'&': '&',
|
|
20
|
+
'<': '<',
|
|
21
|
+
'>': '>',
|
|
22
|
+
'{': '{',
|
|
23
|
+
'}': '}',
|
|
24
|
+
};
|
|
25
|
+
const CODE_UNESCAPE_RE = /&(?:amp|lt|gt|#123|#125);/g;
|
|
26
|
+
// backtick-delimited code span on a single line; spans whose content itself contains backticks
|
|
27
|
+
// (``a`b``) or crosses lines are left alone and behave as before (the page fails to parse the
|
|
28
|
+
// way it always did — fail open, never corrupt)
|
|
29
|
+
const INLINE_CODE_RE = /(`+)([^`]+)\1(?!`)/g;
|
|
30
|
+
// single-line quoted run: a string literal in an expression ({t("<MDX>")}) or an attribute value
|
|
31
|
+
// (title="<MDX>"). An <MDX> inside one is a mention, not a tag.
|
|
32
|
+
const QUOTED_RE = /"[^"\n]*"|'[^'\n]*'/g;
|
|
33
|
+
const escapeCode = (text) => text.replace(CODE_ESCAPE_RE, (char) => { var _a; return (_a = CODE_ESCAPES[char]) !== null && _a !== void 0 ? _a : char; });
|
|
34
|
+
/**
|
|
35
|
+
* Escapes the inline code spans of one non-fence line that sit inside an `<MDX>` body, and
|
|
36
|
+
* returns the `<MDX>` nesting depth after the line. Position-aware so a line may enter or leave
|
|
37
|
+
* `<MDX>` mid-way: only spans between the tags are escaped, an `<MDX>` mention inside an inline
|
|
38
|
+
* code span or a quoted string never counts as a tag, and tags inside root-level inline code or
|
|
39
|
+
* string literals cannot poison the depth for the rest of the document.
|
|
40
|
+
*/
|
|
41
|
+
const processProseLine = (line, depth) => {
|
|
42
|
+
const spans = [...line.matchAll(INLINE_CODE_RE)].map((match) => ({
|
|
43
|
+
start: match.index,
|
|
44
|
+
end: match.index + match[0].length,
|
|
45
|
+
ticks: match[1],
|
|
46
|
+
code: match[2],
|
|
47
|
+
}));
|
|
48
|
+
const inSpan = (index) => spans.some((span) => index >= span.start && index < span.end);
|
|
49
|
+
// Quoted runs are found with inline-code content masked out, so a quote inside a code span
|
|
50
|
+
// (`don't`) cannot open one.
|
|
51
|
+
const masked = spans.reduce((acc, span) => acc.slice(0, span.start) + ' '.repeat(span.end - span.start) + acc.slice(span.end), line);
|
|
52
|
+
const quoted = [...masked.matchAll(QUOTED_RE)].map((match) => ({
|
|
53
|
+
start: match.index,
|
|
54
|
+
end: match.index + match[0].length,
|
|
55
|
+
}));
|
|
56
|
+
const inQuoted = (index) => quoted.some((run) => index > run.start && index < run.end - 1);
|
|
57
|
+
const tags = [
|
|
58
|
+
...[...line.matchAll(MDX_OPEN_RE)].map((match) => ({ index: match.index, open: true })),
|
|
59
|
+
...[...line.matchAll(MDX_CLOSE_RE)].map((match) => ({ index: match.index, open: false })),
|
|
60
|
+
]
|
|
61
|
+
.filter((tag) => !inSpan(tag.index) && !inQuoted(tag.index))
|
|
62
|
+
.sort((a, b) => a.index - b.index);
|
|
63
|
+
const depthAt = (position) => {
|
|
64
|
+
let current = depth;
|
|
65
|
+
for (const tag of tags) {
|
|
66
|
+
if (tag.index >= position)
|
|
67
|
+
break;
|
|
68
|
+
current = Math.max(0, current + (tag.open ? 1 : -1));
|
|
69
|
+
}
|
|
70
|
+
return current;
|
|
71
|
+
};
|
|
72
|
+
let out = '';
|
|
73
|
+
let cursor = 0;
|
|
74
|
+
for (const span of spans) {
|
|
75
|
+
out += line.slice(cursor, span.start);
|
|
76
|
+
out +=
|
|
77
|
+
depthAt(span.start) > 0
|
|
78
|
+
? `${span.ticks}${escapeCode(span.code)}${span.ticks}`
|
|
79
|
+
: line.slice(span.start, span.end);
|
|
80
|
+
cursor = span.end;
|
|
81
|
+
}
|
|
82
|
+
out += line.slice(cursor);
|
|
83
|
+
return { line: out, depth: depthAt(line.length) };
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Escapes `<`, `>`, `{` and `}` inside code fences and inline code within an `<MDX>` body, so code
|
|
87
|
+
* there needs no hand-escaping and parses exactly like code at the page root.
|
|
88
|
+
*
|
|
89
|
+
* The body of an `<MDX>` inside a `{…}` expression is JSX text to the MDX parser: a raw `<` starts
|
|
90
|
+
* a tag and a raw `{` starts an expression, so ```mint <cmd>``` or a fence containing
|
|
91
|
+
* `<project-root>` fails the whole page with "Could not parse expression with acorn" — a code
|
|
92
|
+
* fence gives no protection because it only becomes code later, when `remarkMdxExpandExpressions`
|
|
93
|
+
* re-parses the body as markdown. This pass rewrites those characters to character references
|
|
94
|
+
* (valid JSX text) before any MDX parse; `remarkMdxExpandExpressions` decodes them
|
|
95
|
+
* (`decodeMdxFragmentCode`) once the body has been parsed and the code content is safely inside
|
|
96
|
+
* `code`/`inlineCode` nodes.
|
|
97
|
+
*
|
|
98
|
+
* Runs as a string pass alongside `preprocessCustomHeadingIds`, with the same line scan and fence
|
|
99
|
+
* tracking: content inside a page-level fence is never touched (an `<MDX>` there is example code),
|
|
100
|
+
* and prose/JSX inside an `<MDX>` body is left alone — only fence content and inline code spans
|
|
101
|
+
* are rewritten, including spans on the same line as the `<MDX>` tags. Block-level `<MDX>` bodies
|
|
102
|
+
* get the same treatment so both forms behave identically; their code nodes are decoded by the
|
|
103
|
+
* same plugin. Constructs the line scan cannot see — an inline code span or an opening tag
|
|
104
|
+
* spanning lines, a span whose content contains backticks — are left alone and fail open: the
|
|
105
|
+
* page parses (or fails) exactly as it did before this pass existed, and nothing is corrupted.
|
|
106
|
+
*
|
|
107
|
+
* The pass is idempotent (nothing it emits is rewritten again), so stacked parse entries — e.g.
|
|
108
|
+
* prebuild normalizing a page that `getMdx` later compiles, or a fragment re-parse hitting
|
|
109
|
+
* `getAST` — are safe by construction, and a fragment sliced out of its `<MDX>` tags no longer
|
|
110
|
+
* matches the scanner at all.
|
|
111
|
+
*/
|
|
112
|
+
export function preprocessMdxFragmentCode(content) {
|
|
113
|
+
if (!content.includes('<MDX'))
|
|
114
|
+
return content;
|
|
115
|
+
const lines = content.split('\n');
|
|
116
|
+
const result = [];
|
|
117
|
+
let fenceChar;
|
|
118
|
+
let fenceCount = 0;
|
|
119
|
+
let mdxDepth = 0;
|
|
120
|
+
let changed = false;
|
|
121
|
+
for (const line of lines) {
|
|
122
|
+
const stripped = line.trimStart();
|
|
123
|
+
if (fenceChar !== undefined) {
|
|
124
|
+
if (isClosingFence(stripped, fenceChar, fenceCount)) {
|
|
125
|
+
fenceChar = undefined;
|
|
126
|
+
result.push(line);
|
|
127
|
+
}
|
|
128
|
+
else if (mdxDepth > 0) {
|
|
129
|
+
const escaped = escapeCode(line);
|
|
130
|
+
if (escaped !== line)
|
|
131
|
+
changed = true;
|
|
132
|
+
result.push(escaped);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
result.push(line);
|
|
136
|
+
}
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const fence = parseOpeningFence(stripped);
|
|
140
|
+
if (fence) {
|
|
141
|
+
fenceChar = fence.char;
|
|
142
|
+
fenceCount = fence.count;
|
|
143
|
+
result.push(line);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const processed = processProseLine(line, mdxDepth);
|
|
147
|
+
if (processed.line !== line)
|
|
148
|
+
changed = true;
|
|
149
|
+
mdxDepth = processed.depth;
|
|
150
|
+
result.push(processed.line);
|
|
151
|
+
}
|
|
152
|
+
return changed ? result.join('\n') : content;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Inverse of the escape above, applied by `remarkMdxExpandExpressions` to `code`/`inlineCode`
|
|
156
|
+
* node values inside `<MDX>`. Decodes exactly the references the escape emits plus `&`, which
|
|
157
|
+
* makes hand-written references work the way JSX text says they should (`<` is `<`) and keeps
|
|
158
|
+
* every literal spellable (`&lt;` is a literal `<`). Single-pass, so decoded output is
|
|
159
|
+
* never re-decoded.
|
|
160
|
+
*/
|
|
161
|
+
export const decodeMdxFragmentCode = (value) => value.replace(CODE_UNESCAPE_RE, (entity) => { var _a; return (_a = CODE_UNESCAPES[entity]) !== null && _a !== void 0 ? _a : entity; });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { preprocessMdxFragmentCode } from './preprocessMdxFragmentCode.js';
|
|
3
|
+
const wrap = (body) => `{true ? <MDX>\n${body}\n</MDX> : null}`;
|
|
4
|
+
describe('preprocessMdxFragmentCode', () => {
|
|
5
|
+
it('escapes < > { } inside a fence inside <MDX>', () => {
|
|
6
|
+
const out = preprocessMdxFragmentCode(wrap('```bash\n<root>/{arch}/app.vpkg\n```'));
|
|
7
|
+
expect(out).toBe(wrap('```bash\n<root>/{arch}/app.vpkg\n```'));
|
|
8
|
+
});
|
|
9
|
+
it('escapes inline code inside <MDX>', () => {
|
|
10
|
+
const out = preprocessMdxFragmentCode(wrap('run `mint <cmd>` and `a{b}` now'));
|
|
11
|
+
expect(out).toBe(wrap('run `mint <cmd>` and `a{b}` now'));
|
|
12
|
+
});
|
|
13
|
+
it('leaves prose and JSX inside <MDX> alone', () => {
|
|
14
|
+
const src = wrap('a <b>bold</b> word and a {expr} stay');
|
|
15
|
+
expect(preprocessMdxFragmentCode(src)).toBe(src);
|
|
16
|
+
});
|
|
17
|
+
it('leaves fences and inline code outside <MDX> alone', () => {
|
|
18
|
+
const src = 'x\n\n```bash\n<root>/{arch}\n```\n\nrun `a <b>` now';
|
|
19
|
+
expect(preprocessMdxFragmentCode(src)).toBe(src);
|
|
20
|
+
});
|
|
21
|
+
it('does not treat <MDX> inside a page-level fence as an opener', () => {
|
|
22
|
+
const src = '```mdx\n<MDX>\n\n# example\n</MDX>\n```\n\n`<x>` after';
|
|
23
|
+
expect(preprocessMdxFragmentCode(src)).toBe(src);
|
|
24
|
+
});
|
|
25
|
+
it('is idempotent and never escapes &', () => {
|
|
26
|
+
const src = wrap('```bash\n<a> and <b> and &\n```');
|
|
27
|
+
const once = preprocessMdxFragmentCode(src);
|
|
28
|
+
expect(once).toBe(wrap('```bash\n<a> and <b> and &\n```'));
|
|
29
|
+
expect(preprocessMdxFragmentCode(once)).toBe(once);
|
|
30
|
+
});
|
|
31
|
+
it('a </MDX> inside a fence is escaped and does not close the block', () => {
|
|
32
|
+
const out = preprocessMdxFragmentCode(wrap('```\n</MDX>\n```\n\n`<after>`'));
|
|
33
|
+
expect(out).toBe(wrap('```\n</MDX>\n```\n\n`<after>`'));
|
|
34
|
+
});
|
|
35
|
+
it('handles block-level <MDX> and nested <MDX> bodies', () => {
|
|
36
|
+
const src = '<MDX>\n\n```txt\n<x>\n```\n\n<MDX>\n\n`{y}`\n</MDX>\n</MDX>';
|
|
37
|
+
expect(preprocessMdxFragmentCode(src)).toBe('<MDX>\n\n```txt\n<x>\n```\n\n<MDX>\n\n`{y}`\n</MDX>\n</MDX>');
|
|
38
|
+
});
|
|
39
|
+
it('keeps fence markers, info strings, and indentation untouched', () => {
|
|
40
|
+
const src = wrap(' ```bash filename="a.sh"\n <x>\n ```');
|
|
41
|
+
expect(preprocessMdxFragmentCode(src)).toBe(wrap(' ```bash filename="a.sh"\n <x>\n ```'));
|
|
42
|
+
});
|
|
43
|
+
it('an `<MDX>` mention in root inline code does not poison later root code', () => {
|
|
44
|
+
const src = 'Use `<MDX>` to branch.\n\n```bash\n<root>/{arch}\n```\n\nrun `a <b>` now';
|
|
45
|
+
expect(preprocessMdxFragmentCode(src)).toBe(src);
|
|
46
|
+
});
|
|
47
|
+
it('escapes inline code on the same line as the <MDX> tags', () => {
|
|
48
|
+
const out = preprocessMdxFragmentCode('{true ? <MDX>run `mint <cmd>` now</MDX> : null}');
|
|
49
|
+
expect(out).toBe('{true ? <MDX>run `mint <cmd>` now</MDX> : null}');
|
|
50
|
+
});
|
|
51
|
+
it('only spans between the tags are escaped on a shared line', () => {
|
|
52
|
+
const out = preprocessMdxFragmentCode('`a<b` <MDX>`c<d`</MDX> `e<f`');
|
|
53
|
+
expect(out).toBe('`a<b` <MDX>`c<d`</MDX> `e<f`');
|
|
54
|
+
});
|
|
55
|
+
it('a tag inside an inline code span within a fragment is not counted', () => {
|
|
56
|
+
const out = preprocessMdxFragmentCode(wrap('mention `</MDX>` here\n\n`x<y`'));
|
|
57
|
+
expect(out).toBe(wrap('mention `</MDX>` here\n\n`x<y`'));
|
|
58
|
+
});
|
|
59
|
+
it('a complete <MDX> example inside a fragment fence is escaped, not parsed', () => {
|
|
60
|
+
const out = preprocessMdxFragmentCode(wrap('```mdx\n<MDX>\n# hi\n</MDX>\n```\n\n`x<y`'));
|
|
61
|
+
expect(out).toBe(wrap('```mdx\n<MDX>\n# hi\n</MDX>\n```\n\n`x<y`'));
|
|
62
|
+
});
|
|
63
|
+
it('a tag inside a string literal does not poison later root code', () => {
|
|
64
|
+
// A lone opener in a string used to leave the scanner "inside" a fragment for the rest of
|
|
65
|
+
// the document, escaping root-level code that nothing would ever decode.
|
|
66
|
+
const src = '{label("<MDX>")} intro\n\n```bash\n<root>/{arch}\n```\n\nrun `a <b>` now';
|
|
67
|
+
expect(preprocessMdxFragmentCode(src)).toBe(src);
|
|
68
|
+
});
|
|
69
|
+
it('a balanced pair inside a string literal escapes nothing on the line', () => {
|
|
70
|
+
const src = '{t("<MDX> `a<b` </MDX>")} and `x<y` after';
|
|
71
|
+
expect(preprocessMdxFragmentCode(src)).toBe(src);
|
|
72
|
+
});
|
|
73
|
+
it('a tag inside an attribute value is not counted', () => {
|
|
74
|
+
const src = '<Card title="<MDX>">x</Card>\n\n`code <a>` span';
|
|
75
|
+
expect(preprocessMdxFragmentCode(src)).toBe(src);
|
|
76
|
+
});
|
|
77
|
+
it('returns input unchanged when there is no <MDX>', () => {
|
|
78
|
+
const src = '# Title\n\n```bash\n<root>\n```';
|
|
79
|
+
expect(preprocessMdxFragmentCode(src)).toBe(src);
|
|
80
|
+
});
|
|
81
|
+
});
|
package/dist/mdx/remark.js
CHANGED
|
@@ -6,6 +6,7 @@ import remarkMdx from 'remark-mdx';
|
|
|
6
6
|
import remarkStringify from 'remark-stringify';
|
|
7
7
|
import { remarkVisibilityForMarkdown } from './plugins/remark/remarkVisibilityForMarkdown.js';
|
|
8
8
|
import { preprocessCustomHeadingIds } from './preprocessCustomHeadingIds.js';
|
|
9
|
+
import { preprocessMdxFragmentCode } from './preprocessMdxFragmentCode.js';
|
|
9
10
|
import { getJsxEsmTree } from './snippets/getJsxEsmTree.js';
|
|
10
11
|
import { isJsxOrTsx } from './snippets/isJsxOrTsx.js';
|
|
11
12
|
export const coreRemarkMdxPlugins = [
|
|
@@ -19,7 +20,7 @@ export const getAST = (str, filePath) => {
|
|
|
19
20
|
if (isJsxOrTsx(filePath)) {
|
|
20
21
|
return getJsxEsmTree(str, filePath);
|
|
21
22
|
}
|
|
22
|
-
return coreRemark().parse(preprocessCustomHeadingIds(str));
|
|
23
|
+
return coreRemark().parse(preprocessCustomHeadingIds(preprocessMdxFragmentCode(str)));
|
|
23
24
|
};
|
|
24
25
|
export const stringifyTree = (tree) => coreRemark().use(remarkStringify).stringify(tree);
|
|
25
26
|
export function stripVisibilityForMarkdown(mdx) {
|
|
@@ -12,14 +12,15 @@ import { getTailwindSelectors } from '../../css/tailwind.js';
|
|
|
12
12
|
import { getMDXOptions, remarkMdxRemoveJs, remarkExpandContent, remarkSplitCodeGroup, remarkSplitTabs, remarkValidateAccordions, remarkValidateSteps, remarkValidateVisibility, remarkValidateTabs, remarkValidateApiExamples, } from '../../index.js';
|
|
13
13
|
import { codeStylingToThemeOrThemes } from '../getCodeStyling.js';
|
|
14
14
|
import { preprocessCustomHeadingIds } from '../preprocessCustomHeadingIds.js';
|
|
15
|
+
import { preprocessMdxFragmentCode } from '../preprocessMdxFragmentCode.js';
|
|
15
16
|
import { replaceVariables } from '../replaceVariables.js';
|
|
16
17
|
import { createSnippetTreeMap } from './getMdx/snippets.js';
|
|
17
18
|
import { getSerializeExtras } from './getSerializeExtras.js';
|
|
18
19
|
export function getMdx(_a) {
|
|
19
20
|
return __awaiter(this, arguments, void 0, function* ({ path, content, metadata, snippets, subdomain, codeStyling, config, tailwindSelectors = undefined, pageType = 'default', trace = undefined, customLanguages = [], variables = undefined, rehypePlugins = [], remarkPlugins = [], allowedComponents = [], }) {
|
|
20
21
|
const traceFn = trace !== null && trace !== void 0 ? trace : ((_name, fn) => fn());
|
|
21
|
-
const processedContent = preprocessCustomHeadingIds(replaceVariables(content, variables));
|
|
22
|
-
const processedSnippets = snippets.map((snippet) => (Object.assign(Object.assign({}, snippet), { content: preprocessCustomHeadingIds(replaceVariables(snippet.content, variables)) })));
|
|
22
|
+
const processedContent = preprocessCustomHeadingIds(preprocessMdxFragmentCode(replaceVariables(content, variables)));
|
|
23
|
+
const processedSnippets = snippets.map((snippet) => (Object.assign(Object.assign({}, snippet), { content: preprocessCustomHeadingIds(preprocessMdxFragmentCode(replaceVariables(snippet.content, variables))) })));
|
|
23
24
|
if (!tailwindSelectors)
|
|
24
25
|
tailwindSelectors = yield traceFn('getMdx.getTailwindSelectors', () => __awaiter(this, void 0, void 0, function* () { return getTailwindSelectors({ content: processedContent }); }));
|
|
25
26
|
const snippetTreeMap = yield traceFn('getMdx.createSnippetTreeMap', () => __awaiter(this, void 0, void 0, function* () { return createSnippetTreeMap(processedSnippets); }));
|