@readme/markdown 14.6.0 → 14.7.0
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/components/Image/index.tsx +14 -8
- package/components/Image/style.scss +56 -0
- package/dist/lib/constants.d.ts +5 -0
- package/dist/main.css +1 -1
- package/dist/main.css.map +1 -1
- package/dist/main.js +373 -547
- package/dist/main.node.js +371 -545
- package/dist/main.node.js.map +1 -1
- package/dist/processor/transform/mdxish/mdxish-html-blocks.d.ts +14 -1
- package/dist/processor/transform/mdxish/preprocess-jsx-expressions.d.ts +0 -3
- package/dist/processor/utils.d.ts +4 -2
- package/package.json +2 -2
package/dist/main.js
CHANGED
|
@@ -12273,11 +12273,20 @@ const Image = (Props) => {
|
|
|
12273
12273
|
lightboxOverlay));
|
|
12274
12274
|
}
|
|
12275
12275
|
if (children || caption) {
|
|
12276
|
-
|
|
12277
|
-
|
|
12278
|
-
|
|
12279
|
-
|
|
12280
|
-
|
|
12276
|
+
// Mirrors the framed pattern: left/right captioned figures float and shrink
|
|
12277
|
+
// to fit so a long caption doesn't widen the float past the image.
|
|
12278
|
+
const isFloating = (align === 'left' || align === 'right') && !noWrap;
|
|
12279
|
+
const figureClass = [
|
|
12280
|
+
(align === 'left' || align === 'right') && `img-figure-${align}`,
|
|
12281
|
+
noWrap && 'img-no-wrap',
|
|
12282
|
+
]
|
|
12283
|
+
.filter(Boolean)
|
|
12284
|
+
.join(' ');
|
|
12285
|
+
const figureStyle = isFloating && typeof width === 'string' && width.endsWith('%') ? { width } : undefined;
|
|
12286
|
+
return (external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_.createElement("figure", { className: figureClass || undefined, style: figureStyle },
|
|
12287
|
+
closedLightbox(alt || 'Expand image', imgElement),
|
|
12288
|
+
lightboxOverlay,
|
|
12289
|
+
external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_.createElement("figcaption", null, children || caption)));
|
|
12281
12290
|
}
|
|
12282
12291
|
return (external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_.createElement(external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_.Fragment, null,
|
|
12283
12292
|
closedLightbox('Expand image', imgElement),
|
|
@@ -13109,6 +13118,7 @@ const INLINE_COMPONENT_TAGS = new Set(['Anchor', 'Glossary']);
|
|
|
13109
13118
|
/**
|
|
13110
13119
|
* PascalCase tags that have their own dedicated tokenizer / transformer
|
|
13111
13120
|
* and must not be claimed by the generic `mdxComponent` construct.
|
|
13121
|
+
* Subject to change as we add more dedicated tokenizers.
|
|
13112
13122
|
*/
|
|
13113
13123
|
const DEDICATED_COMPONENT_TAGS = ['HTMLBlock', 'Table'];
|
|
13114
13124
|
/**
|
|
@@ -13119,6 +13129,14 @@ const GENERIC_MDX_COMPONENT_EXCLUDED_TAGS = new Set([
|
|
|
13119
13129
|
...DEDICATED_COMPONENT_TAGS,
|
|
13120
13130
|
...INLINE_COMPONENT_TAGS,
|
|
13121
13131
|
]);
|
|
13132
|
+
/**
|
|
13133
|
+
* Tags the micromark `mdxComponent` tokenizer must not claim, which
|
|
13134
|
+
* are inline components and those that have their own dedicated tokenizer
|
|
13135
|
+
*/
|
|
13136
|
+
const TOKENIZER_MDX_COMPONENT_EXCLUDED_TAGS = new Set([
|
|
13137
|
+
'Table',
|
|
13138
|
+
...INLINE_COMPONENT_TAGS,
|
|
13139
|
+
]);
|
|
13122
13140
|
/**
|
|
13123
13141
|
* Lowercased variant of {@link INLINE_COMPONENT_TAGS} for consumers that
|
|
13124
13142
|
* run after rehype (where hast `tagName` is normalized to lowercase).
|
|
@@ -54513,18 +54531,28 @@ const isMDXEsm = (node) => {
|
|
|
54513
54531
|
* Takes an HTML string and formats it for display in the editor. Removes leading/trailing newlines
|
|
54514
54532
|
* and unindents the HTML.
|
|
54515
54533
|
*
|
|
54516
|
-
* @param {string} html - HTML
|
|
54534
|
+
* @param {string} html - cooked HTML payload (callers strip any template-literal backticks first)
|
|
54535
|
+
* @param {number} [openingTagIndent=0] - column the `<HTMLBlock>` opening tag sits at, used to
|
|
54536
|
+
* dedent each content line so its indentation reads relative to the tag, not the line start
|
|
54517
54537
|
* @returns {string} processed HTML
|
|
54518
54538
|
*/
|
|
54519
|
-
function formatHtmlForMdxish(html) {
|
|
54520
|
-
// Remove leading/trailing backticks if present, since they're used to keep the HTML
|
|
54521
|
-
// from being parsed prematurely
|
|
54522
|
-
let processed = html;
|
|
54523
|
-
if (processed.startsWith('`') && processed.endsWith('`')) {
|
|
54524
|
-
processed = processed.slice(1, -1);
|
|
54525
|
-
}
|
|
54539
|
+
function formatHtmlForMdxish(html, openingTagIndent = 0) {
|
|
54526
54540
|
// Removes the leading/trailing newlines
|
|
54527
|
-
let cleaned =
|
|
54541
|
+
let cleaned = html.replace(/^\s*\n|\n\s*$/g, '');
|
|
54542
|
+
// Strip / deindent the lines in the HTML string so that the indents are relative
|
|
54543
|
+
// to the opening HTMLBlock tag, not the literal line start
|
|
54544
|
+
// Keep any deeper indent
|
|
54545
|
+
if (openingTagIndent > 0) {
|
|
54546
|
+
cleaned = cleaned
|
|
54547
|
+
.split('\n')
|
|
54548
|
+
.map(line => {
|
|
54549
|
+
let i = 0;
|
|
54550
|
+
while (i < openingTagIndent && (line[i] === ' ' || line[i] === '\t'))
|
|
54551
|
+
i += 1;
|
|
54552
|
+
return line.slice(i);
|
|
54553
|
+
})
|
|
54554
|
+
.join('\n');
|
|
54555
|
+
}
|
|
54528
54556
|
// Convert literal \n sequences to actual newlines only inside <pre> and <code>.
|
|
54529
54557
|
// Because <pre> needs to respect the newline visual and
|
|
54530
54558
|
// escape characters should be processed in the <code> tag.
|
|
@@ -75681,11 +75709,15 @@ const symmetrizePair = (html, { openStart, openEnd, closeStart, closeEnd }) => {
|
|
|
75681
75709
|
const postCloser = html.slice(closeEnd, closeLine.end);
|
|
75682
75710
|
const openerHasExtras = preOpener.trim().length > 0 || postOpener.trim().length > 0;
|
|
75683
75711
|
const closerHasExtras = preCloser.trim().length > 0 || postCloser.trim().length > 0;
|
|
75684
|
-
//
|
|
75685
|
-
|
|
75712
|
+
// A blank line splits opener/closer into separate paragraphs.
|
|
75713
|
+
// If either tag is attached to surrounding text, mdxjs can fail to match.
|
|
75714
|
+
const spansBlankLine = /\n[^\S\n]*\n/.test(html.slice(openEnd, closeStart));
|
|
75715
|
+
// If both sides are already symmetric, keep as-is.
|
|
75716
|
+
// Exception: attached + blank-line-split pairs still need normalization.
|
|
75717
|
+
if (openerHasExtras === closerHasExtras && !(openerHasExtras && spansBlankLine))
|
|
75686
75718
|
return [];
|
|
75687
|
-
//
|
|
75688
|
-
//
|
|
75719
|
+
// For asymmetric (or attached-but-split) pairs, move side content to its own
|
|
75720
|
+
// line so opener/closer become line-symmetric.
|
|
75689
75721
|
const indentFor = (linePrefix) => linePrefix.match(/^\s*/)?.[0] ?? '';
|
|
75690
75722
|
const inserts = [];
|
|
75691
75723
|
if (openerHasExtras) {
|
|
@@ -76131,6 +76163,7 @@ const repairUnclosedTags = (html) => {
|
|
|
76131
76163
|
|
|
76132
76164
|
|
|
76133
76165
|
|
|
76166
|
+
|
|
76134
76167
|
|
|
76135
76168
|
|
|
76136
76169
|
const isTableCell = (node) => isMDXElement(node) && ['th', 'td'].includes(node.name);
|
|
@@ -76231,6 +76264,15 @@ const processTableNode = (node, index, parent, documentPosition) => {
|
|
|
76231
76264
|
const { align: alignAttr } = getAttrs(node);
|
|
76232
76265
|
const align = Array.isArray(alignAttr) ? alignAttr : null;
|
|
76233
76266
|
let tableHasFlowContent = false;
|
|
76267
|
+
// An `<HTMLBlock>` (still a JSX element here; converted to `html-block` by
|
|
76268
|
+
// `mdxishHtmlBlocks` after this transformer) is block-level content that a
|
|
76269
|
+
// markdown table cell can't represent, so keep the table as a JSX `<Table>`.
|
|
76270
|
+
visit(node, candidate => candidate.type === NodeTypes.htmlBlock ||
|
|
76271
|
+
((candidate.type === 'mdxJsxFlowElement' || candidate.type === 'mdxJsxTextElement') &&
|
|
76272
|
+
candidate.name === 'HTMLBlock'), () => {
|
|
76273
|
+
tableHasFlowContent = true;
|
|
76274
|
+
return EXIT;
|
|
76275
|
+
});
|
|
76234
76276
|
// Re-parse text-only cells through markdown and detect flow content
|
|
76235
76277
|
visit(node, isTableCell, (cell) => {
|
|
76236
76278
|
if (!isTextOnly(cell.children))
|
|
@@ -100871,7 +100913,7 @@ function createTokenize(mode) {
|
|
|
100871
100913
|
return tagNameRest;
|
|
100872
100914
|
}
|
|
100873
100915
|
// Tag name complete — check exclusions
|
|
100874
|
-
if (
|
|
100916
|
+
if (TOKENIZER_MDX_COMPONENT_EXCLUDED_TAGS.has(tagName)) {
|
|
100875
100917
|
return nok(code);
|
|
100876
100918
|
}
|
|
100877
100919
|
depth = 1;
|
|
@@ -102962,557 +103004,158 @@ const magicBlockTransformer = (options = {}) => tree => {
|
|
|
102962
103004
|
};
|
|
102963
103005
|
/* harmony default export */ const magic_block_transformer = (magicBlockTransformer);
|
|
102964
103006
|
|
|
102965
|
-
;// ./
|
|
102966
|
-
/**
|
|
102967
|
-
* Matches a JSX comment: `{/*`, content, `*\/}` — no whitespace tolerated
|
|
102968
|
-
* between the braces and the comment markers.
|
|
102969
|
-
*
|
|
102970
|
-
* This grammar is mirrored by the flow tokenizer in ./syntax.ts. Any change
|
|
102971
|
-
* here needs a mirror change in the state machine; the parity test in
|
|
102972
|
-
* __tests__/lib/micromark/jsx-comment-pattern-parity.test.ts locks the two
|
|
102973
|
-
* together so they can't silently drift.
|
|
102974
|
-
*/
|
|
102975
|
-
const JSX_COMMENT_REGEX = /\{\/\*[^*]*(?:\*(?!\/)[^*]*)*\*\/\}/g;
|
|
103007
|
+
;// ./processor/transform/mdxish/mdxish-html-blocks.ts
|
|
102976
103008
|
|
|
102977
|
-
;// ./processor/transform/mdxish/preprocess-jsx-expressions.ts
|
|
102978
103009
|
|
|
102979
103010
|
|
|
102980
|
-
//
|
|
102981
|
-
|
|
102982
|
-
|
|
102983
|
-
|
|
102984
|
-
|
|
102985
|
-
|
|
102986
|
-
|
|
102987
|
-
// Base64 decode (Node.js + browser compatible)
|
|
102988
|
-
function base64Decode(str) {
|
|
102989
|
-
if (typeof Buffer !== 'undefined') {
|
|
102990
|
-
return Buffer.from(str, 'base64').toString('utf-8');
|
|
102991
|
-
}
|
|
102992
|
-
return decodeURIComponent(escape(atob(str)));
|
|
102993
|
-
}
|
|
102994
|
-
// Markers for protected HTMLBlock content (HTML comments avoid markdown parsing issues)
|
|
102995
|
-
const HTML_BLOCK_CONTENT_START = '<!--RDMX_HTMLBLOCK:';
|
|
102996
|
-
const HTML_BLOCK_CONTENT_END = ':RDMX_HTMLBLOCK-->';
|
|
103011
|
+
// `<HTMLBlock …>{`…`}</HTMLBlock>` embedded inside a raw HTML block (e.g. a
|
|
103012
|
+
// single-line `<div>…</div>`). CommonMark slurps the whole div as one `html`
|
|
103013
|
+
// node, so the tokenizer never sees the HTMLBlock — we recover it here.
|
|
103014
|
+
const RAW_HTML_BLOCK_RE = /<HTMLBlock\b([^>]*)>\s*\{\s*`((?:[^`\\]|\\.)*)`\s*\}\s*<\/HTMLBlock>/g;
|
|
103015
|
+
// Opening `<HTMLBlock …>` as its own `html` node — produced inside a paragraph
|
|
103016
|
+
// when an HTMLBlock appears inline alongside text.
|
|
103017
|
+
const HTML_BLOCK_OPEN_RE = /^<HTMLBlock\b([^>]*)>$/;
|
|
102997
103018
|
/**
|
|
102998
|
-
*
|
|
102999
|
-
*
|
|
103000
|
-
* @param content
|
|
103001
|
-
* @returns Content with HTMLBlock template literals base64 encoded in HTML comments
|
|
103002
|
-
* @example
|
|
103003
|
-
* ```typescript
|
|
103004
|
-
* const input = '<HTMLBlock>{`<script>alert("xss")</script>`}</HTMLBlock>';
|
|
103005
|
-
* protectHTMLBlockContent(input)
|
|
103006
|
-
* // Returns: '<HTMLBlock><!--RDMX_HTMLBLOCK:PHNjcmlwdD5hbGVydCgieHNzIik8L3NjcmlwdD4=:RDMX_HTMLBLOCK--></HTMLBlock>'
|
|
103007
|
-
* ```
|
|
103019
|
+
* Builds the canonical `html-block` MDAST node the renderer expects.
|
|
103008
103020
|
*/
|
|
103009
|
-
|
|
103010
|
-
|
|
103011
|
-
|
|
103012
|
-
|
|
103013
|
-
|
|
103014
|
-
|
|
103021
|
+
const createHtmlBlockNode = (html, position, runScripts, safeMode) => ({
|
|
103022
|
+
position,
|
|
103023
|
+
children: [{ type: 'text', value: html }],
|
|
103024
|
+
type: NodeTypes.htmlBlock,
|
|
103025
|
+
data: {
|
|
103026
|
+
hName: 'html-block',
|
|
103027
|
+
hProperties: {
|
|
103028
|
+
html,
|
|
103029
|
+
...(runScripts !== undefined && { runScripts }),
|
|
103030
|
+
...(safeMode !== undefined && { safeMode }),
|
|
103031
|
+
},
|
|
103032
|
+
},
|
|
103033
|
+
});
|
|
103015
103034
|
/**
|
|
103016
|
-
*
|
|
103017
|
-
*
|
|
103018
|
-
* @param content
|
|
103019
|
-
* @returns Content with JSX comments removed
|
|
103020
|
-
* @example
|
|
103021
|
-
* ```typescript
|
|
103022
|
-
* removeJSXComments('Text { /* comment *\/ } more text')
|
|
103023
|
-
* // Returns: 'Text more text'
|
|
103024
|
-
* ```
|
|
103035
|
+
* Reads the cooked string out of a brace expression wrapping a single template
|
|
103036
|
+
* literal (`` `<p>n</p>` `` → `<p>n</p>`).
|
|
103025
103037
|
*/
|
|
103026
|
-
|
|
103027
|
-
|
|
103028
|
-
|
|
103029
|
-
const
|
|
103030
|
-
|
|
103031
|
-
//
|
|
103032
|
-
|
|
103033
|
-
|
|
103038
|
+
const extractTemplateLiteral = (value) => {
|
|
103039
|
+
if (!value)
|
|
103040
|
+
return '';
|
|
103041
|
+
const match = value.trim().match(/^`([\s\S]*)`$/);
|
|
103042
|
+
// Non-template-literal bodies (e.g. `{someVar}`) are malformed mdxish input;
|
|
103043
|
+
// returning '' beats shipping JS identifier source as an HTML payload.
|
|
103044
|
+
return match ? match[1] : '';
|
|
103045
|
+
};
|
|
103046
|
+
const toRunScripts = (raw) => raw === 'true' ? true : raw === 'false' ? false : raw;
|
|
103047
|
+
/** Reads an attribute from a raw `<HTMLBlock …>` attribute string. */
|
|
103048
|
+
const rawAttr = (attrs, name) => {
|
|
103049
|
+
const quoted = attrs.match(new RegExp(`\\b${name}\\s*=\\s*"([^"]*)"`));
|
|
103050
|
+
if (quoted)
|
|
103051
|
+
return quoted[1];
|
|
103052
|
+
const expr = attrs.match(new RegExp(`\\b${name}\\s*=\\s*\\{(true|false)\\}`));
|
|
103053
|
+
if (expr)
|
|
103054
|
+
return expr[1];
|
|
103055
|
+
return new RegExp(`\\b${name}\\b`).test(attrs) ? 'true' : undefined;
|
|
103056
|
+
};
|
|
103057
|
+
/** Reads an attribute from a parsed `<HTMLBlock>` JSX element. */
|
|
103058
|
+
const jsxAttr = (element, name) => {
|
|
103059
|
+
const attr = element.attributes.find(a => a.type === 'mdxJsxAttribute' && a.name === name);
|
|
103060
|
+
if (!attr || attr.type !== 'mdxJsxAttribute')
|
|
103061
|
+
return undefined;
|
|
103062
|
+
if (typeof attr.value === 'string')
|
|
103063
|
+
return attr.value;
|
|
103064
|
+
if (attr.value && typeof attr.value === 'object' && 'value' in attr.value)
|
|
103065
|
+
return attr.value.value;
|
|
103066
|
+
return 'true'; // bare boolean attribute, e.g. <HTMLBlock runScripts />
|
|
103067
|
+
};
|
|
103068
|
+
/** Builds an `html-block` from a raw attribute string and (unparsed) body. */
|
|
103069
|
+
const htmlBlockFromRaw = (attrs, html, position, openingTagIndent = 0) => createHtmlBlockNode(formatHtmlForMdxish(html, openingTagIndent), position, toRunScripts(rawAttr(attrs, 'runScripts')), rawAttr(attrs, 'safeMode'));
|
|
103034
103070
|
/**
|
|
103035
|
-
*
|
|
103036
|
-
*
|
|
103071
|
+
* Splits a raw `html` node that embeds one or more `<HTMLBlock>`s into
|
|
103072
|
+
* `[html before, html-block, html after, …]`. Returns null when there is none.
|
|
103037
103073
|
*
|
|
103038
|
-
*
|
|
103039
|
-
*
|
|
103040
|
-
* escape — so we leave that case to the brace balancer.
|
|
103074
|
+
* `String.split` on a regex with capture groups interleaves the captures into
|
|
103075
|
+
* the result, so segments arrive as `[text, attrs, body, text, attrs, body, …]`.
|
|
103041
103076
|
*/
|
|
103042
|
-
|
|
103043
|
-
const
|
|
103044
|
-
|
|
103045
|
-
|
|
103046
|
-
|
|
103047
|
-
|
|
103048
|
-
|
|
103049
|
-
|
|
103050
|
-
|
|
103051
|
-
|
|
103052
|
-
|
|
103053
|
-
|
|
103054
|
-
|
|
103055
|
-
|
|
103056
|
-
|
|
103057
|
-
|
|
103058
|
-
|
|
103059
|
-
|
|
103060
|
-
if (htmlElements.length === 0)
|
|
103061
|
-
return content;
|
|
103062
|
-
return content.replace(HTML_ELEM_PLACEHOLDER, (_m, idx) => htmlElements[parseInt(idx, 10)]);
|
|
103063
|
-
}
|
|
103064
|
-
/**
|
|
103065
|
-
* Escapes unbalanced and paragraph-spanning braces so MDX doesn't trip on them.
|
|
103066
|
-
*/
|
|
103067
|
-
function escapeProblematicBraces(content) {
|
|
103068
|
-
const { htmlElements, protectedContent } = protectHTMLElements(content);
|
|
103069
|
-
let strDelim = null;
|
|
103070
|
-
let strEscaped = false;
|
|
103071
|
-
// Track position of last newline (outside strings) to detect blank lines
|
|
103072
|
-
// -2 means no recent newline
|
|
103073
|
-
let lastNewlinePos = -2;
|
|
103074
|
-
// Character state machine trackers
|
|
103075
|
-
const toEscape = new Set();
|
|
103076
|
-
// Convert to array of Unicode code points so that emojis and multi-byte characters are correctly tracked
|
|
103077
|
-
const chars = Array.from(protectedContent);
|
|
103078
|
-
const openStack = [];
|
|
103079
|
-
for (let i = 0; i < chars.length; i += 1) {
|
|
103080
|
-
const ch = chars[i];
|
|
103081
|
-
// Track string delimiters inside expressions to ignore braces within them
|
|
103082
|
-
if (openStack.length > 0) {
|
|
103083
|
-
if (strDelim) {
|
|
103084
|
-
if (strEscaped)
|
|
103085
|
-
strEscaped = false;
|
|
103086
|
-
else if (ch === '\\')
|
|
103087
|
-
strEscaped = true;
|
|
103088
|
-
else if (ch === strDelim)
|
|
103089
|
-
strDelim = null;
|
|
103090
|
-
// eslint-disable-next-line no-continue
|
|
103091
|
-
continue;
|
|
103092
|
-
}
|
|
103093
|
-
if (ch === '"' || ch === "'" || ch === '`') {
|
|
103094
|
-
strDelim = ch;
|
|
103095
|
-
// eslint-disable-next-line no-continue
|
|
103096
|
-
continue;
|
|
103097
|
-
}
|
|
103098
|
-
if (ch === '\n') {
|
|
103099
|
-
if (lastNewlinePos >= 0) {
|
|
103100
|
-
const between = chars.slice(lastNewlinePos + 1, i).join('');
|
|
103101
|
-
if (/^[ \t]*$/.test(between)) {
|
|
103102
|
-
openStack.forEach(entry => { entry.hasBlankLine = true; });
|
|
103103
|
-
}
|
|
103104
|
-
}
|
|
103105
|
-
lastNewlinePos = i;
|
|
103106
|
-
}
|
|
103107
|
-
}
|
|
103108
|
-
// Skip already-escaped braces (odd run of preceding backslashes).
|
|
103109
|
-
if (ch === '{' || ch === '}') {
|
|
103110
|
-
let bs = 0;
|
|
103111
|
-
for (let j = i - 1; j >= 0 && chars[j] === '\\'; j -= 1)
|
|
103112
|
-
bs += 1;
|
|
103113
|
-
if (bs % 2 === 1) {
|
|
103114
|
-
// eslint-disable-next-line no-continue
|
|
103115
|
-
continue;
|
|
103116
|
-
}
|
|
103117
|
-
}
|
|
103118
|
-
if (ch === '{') {
|
|
103119
|
-
// `=` (after whitespace) before `{` ⇒ JSX attribute expression. The
|
|
103120
|
-
// mdxComponent tokenizer captures the whole component, so blank lines
|
|
103121
|
-
// inside attribute values are harmless. Nested `{` inherits the flag.
|
|
103122
|
-
let isAttrExpr = false;
|
|
103123
|
-
for (let j = i - 1; j >= 0; j -= 1) {
|
|
103124
|
-
const pc = chars[j];
|
|
103125
|
-
if (pc === '=') {
|
|
103126
|
-
isAttrExpr = true;
|
|
103127
|
-
break;
|
|
103128
|
-
}
|
|
103129
|
-
if (pc !== ' ' && pc !== '\t')
|
|
103130
|
-
break;
|
|
103131
|
-
}
|
|
103132
|
-
// Nested `{ ... }` inside an attribute value (e.g. `data={[{ ... }]}` or
|
|
103133
|
-
// `data={{ a: { b: 1 } }}`) must inherit the same exemption; only the
|
|
103134
|
-
// outer `{` is directly after `=`.
|
|
103135
|
-
if (!isAttrExpr && openStack.length > 0 && openStack[openStack.length - 1].isAttrExpr) {
|
|
103136
|
-
isAttrExpr = true;
|
|
103137
|
-
}
|
|
103138
|
-
openStack.push({ pos: i, hasBlankLine: false, isAttrExpr });
|
|
103139
|
-
lastNewlinePos = -2;
|
|
103140
|
-
}
|
|
103141
|
-
else if (ch === '}') {
|
|
103142
|
-
if (openStack.length > 0) {
|
|
103143
|
-
const entry = openStack.pop();
|
|
103144
|
-
// Pure `{/* ... */}` comments are handled downstream by the jsxComment
|
|
103145
|
-
// tokenizer — escaping their braces would prevent it from running.
|
|
103146
|
-
const isPureJsxComment = chars[entry.pos + 1] === '/' &&
|
|
103147
|
-
chars[entry.pos + 2] === '*' &&
|
|
103148
|
-
chars[i - 1] === '/' &&
|
|
103149
|
-
chars[i - 2] === '*';
|
|
103150
|
-
if (entry.hasBlankLine && !isPureJsxComment && !entry.isAttrExpr) {
|
|
103151
|
-
toEscape.add(entry.pos);
|
|
103152
|
-
toEscape.add(i);
|
|
103153
|
-
}
|
|
103154
|
-
}
|
|
103155
|
-
else {
|
|
103156
|
-
toEscape.add(i);
|
|
103157
|
-
}
|
|
103158
|
-
}
|
|
103159
|
-
}
|
|
103160
|
-
// Anything still open is unbalanced.
|
|
103161
|
-
openStack.forEach(entry => toEscape.add(entry.pos));
|
|
103162
|
-
// Reconstruct the content with the escaped braces.
|
|
103163
|
-
const escapedContent = toEscape.size === 0 ? protectedContent : chars.map((ch, i) => (toEscape.has(i) ? `\\${ch}` : ch)).join('');
|
|
103164
|
-
return restoreHTMLElements(escapedContent, htmlElements);
|
|
103165
|
-
}
|
|
103077
|
+
const splitRawHtmlBlocks = (node) => {
|
|
103078
|
+
const segments = node.value.split(RAW_HTML_BLOCK_RE);
|
|
103079
|
+
if (segments.length === 1)
|
|
103080
|
+
return null; // no <HTMLBlock> present
|
|
103081
|
+
const parts = [];
|
|
103082
|
+
for (let i = 0; i < segments.length; i += 3) {
|
|
103083
|
+
const [text, attrs, body] = segments.slice(i, i + 3);
|
|
103084
|
+
if (text)
|
|
103085
|
+
parts.push({ type: 'html', value: text });
|
|
103086
|
+
if (body !== undefined) {
|
|
103087
|
+
// The opening tag's column equals the length of the line it starts on
|
|
103088
|
+
// (the text run since the previous newline preceding the match).
|
|
103089
|
+
const openingTagIndent = text.slice(text.lastIndexOf('\n') + 1).length;
|
|
103090
|
+
parts.push(htmlBlockFromRaw(attrs, body, node.position, openingTagIndent));
|
|
103091
|
+
}
|
|
103092
|
+
}
|
|
103093
|
+
return parts;
|
|
103094
|
+
};
|
|
103166
103095
|
/**
|
|
103167
|
-
*
|
|
103096
|
+
* Converts every `<HTMLBlock>` shape that survives parsing into the canonical
|
|
103097
|
+
* `html-block` MDAST node, reading the body from the tokenizer's template-literal
|
|
103098
|
+
* expression. Three shapes occur:
|
|
103168
103099
|
*
|
|
103169
|
-
* JSX
|
|
103170
|
-
*
|
|
103171
|
-
*
|
|
103100
|
+
* 1. JSX element (`mdxJsxFlowElement`/`mdxJsxTextElement`) — multiline/block
|
|
103101
|
+
* context and table cells (after their remarkMdx re-parse).
|
|
103102
|
+
* 2. Raw `html` blob (`splitRawHtmlBlocks`) — single-line top-level, or nested
|
|
103103
|
+
* in raw HTML like an inline `<div>`.
|
|
103104
|
+
* 3. Inline-in-paragraph — split into `html` + expression + `html` siblings.
|
|
103172
103105
|
*
|
|
103173
|
-
*
|
|
103174
|
-
*
|
|
103175
|
-
|
|
103176
|
-
|
|
103177
|
-
let processed = protectHTMLBlockContent(content);
|
|
103178
|
-
const { protectedCode, protectedContent } = protectCodeBlocks(processed);
|
|
103179
|
-
processed = escapeProblematicBraces(protectedContent);
|
|
103180
|
-
processed = restoreCodeBlocks(processed, protectedCode);
|
|
103181
|
-
return processed;
|
|
103182
|
-
}
|
|
103183
|
-
|
|
103184
|
-
;// ./processor/transform/mdxish/mdxish-html-blocks.ts
|
|
103185
|
-
|
|
103186
|
-
|
|
103187
|
-
|
|
103188
|
-
|
|
103189
|
-
/**
|
|
103190
|
-
* Decodes HTMLBlock content that was protected during preprocessing.
|
|
103191
|
-
* Content is wrapped in <!--RDMX_HTMLBLOCK:base64:RDMX_HTMLBLOCK-->
|
|
103192
|
-
*/
|
|
103193
|
-
function decodeProtectedContent(content) {
|
|
103194
|
-
// Escape special regex characters in the markers
|
|
103195
|
-
const startEscaped = HTML_BLOCK_CONTENT_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
103196
|
-
const endEscaped = HTML_BLOCK_CONTENT_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
103197
|
-
const markerRegex = new RegExp(`${startEscaped}([A-Za-z0-9+/=]+)${endEscaped}`, 'g');
|
|
103198
|
-
return content.replace(markerRegex, (_match, encoded) => {
|
|
103199
|
-
try {
|
|
103200
|
-
return base64Decode(encoded);
|
|
103201
|
-
}
|
|
103202
|
-
catch {
|
|
103203
|
-
return encoded;
|
|
103204
|
-
}
|
|
103205
|
-
});
|
|
103206
|
-
}
|
|
103207
|
-
/**
|
|
103208
|
-
* Collects text content from a node and its children recursively
|
|
103209
|
-
*/
|
|
103210
|
-
function collectTextContent(node) {
|
|
103211
|
-
const parts = [];
|
|
103212
|
-
if (node.type === 'text' && node.value) {
|
|
103213
|
-
parts.push(node.value);
|
|
103214
|
-
}
|
|
103215
|
-
else if (node.type === 'html' && node.value) {
|
|
103216
|
-
parts.push(node.value);
|
|
103217
|
-
}
|
|
103218
|
-
else if (node.type === 'inlineCode' && node.value) {
|
|
103219
|
-
parts.push(node.value);
|
|
103220
|
-
}
|
|
103221
|
-
else if (node.type === 'code' && node.value) {
|
|
103222
|
-
// Reconstruct code fence syntax (markdown parser consumes opening ```)
|
|
103223
|
-
const lang = node.lang || '';
|
|
103224
|
-
const fence = `\`\`\`${lang ? `${lang}\n` : ''}`;
|
|
103225
|
-
parts.push(fence);
|
|
103226
|
-
parts.push(node.value);
|
|
103227
|
-
// Add newline before closing fence if missing
|
|
103228
|
-
const closingFence = node.value.endsWith('\n') ? '```' : '\n```';
|
|
103229
|
-
parts.push(closingFence);
|
|
103230
|
-
}
|
|
103231
|
-
else if (node.children && Array.isArray(node.children)) {
|
|
103232
|
-
node.children.forEach(child => {
|
|
103233
|
-
if (typeof child === 'object' && child !== null) {
|
|
103234
|
-
parts.push(collectTextContent(child));
|
|
103235
|
-
}
|
|
103236
|
-
});
|
|
103237
|
-
}
|
|
103238
|
-
return parts.join('');
|
|
103239
|
-
}
|
|
103240
|
-
/**
|
|
103241
|
-
* Extracts boolean attribute from HTML tag. Handles JSX (safeMode={true}) and string (safeMode="true") syntax.
|
|
103242
|
-
* Returns "true"/"false" string to survive rehypeRaw serialization.
|
|
103243
|
-
*/
|
|
103244
|
-
function extractBooleanAttr(attrs, name) {
|
|
103245
|
-
// Try JSX syntax: name={true|false}
|
|
103246
|
-
const jsxMatch = attrs.match(new RegExp(`${name}=\\{(true|false)\\}`));
|
|
103247
|
-
if (jsxMatch) {
|
|
103248
|
-
return jsxMatch[1];
|
|
103249
|
-
}
|
|
103250
|
-
// Try string syntax: name="true"|true
|
|
103251
|
-
const stringMatch = attrs.match(new RegExp(`${name}="?(true|false)"?`));
|
|
103252
|
-
if (stringMatch) {
|
|
103253
|
-
return stringMatch[1];
|
|
103254
|
-
}
|
|
103255
|
-
return undefined;
|
|
103256
|
-
}
|
|
103257
|
-
/**
|
|
103258
|
-
* Extracts runScripts attribute from HTML tag. Returns boolean for "true"/"false", string for other values, or undefined if not found.
|
|
103259
|
-
*/
|
|
103260
|
-
function extractRunScriptsAttr(attrs) {
|
|
103261
|
-
const runScriptsMatch = attrs.match(/runScripts="?([^">\s]+)"?/);
|
|
103262
|
-
if (!runScriptsMatch) {
|
|
103263
|
-
return undefined;
|
|
103264
|
-
}
|
|
103265
|
-
const value = runScriptsMatch[1];
|
|
103266
|
-
if (value === 'true') {
|
|
103267
|
-
return true;
|
|
103268
|
-
}
|
|
103269
|
-
if (value === 'false') {
|
|
103270
|
-
return false;
|
|
103271
|
-
}
|
|
103272
|
-
return value;
|
|
103273
|
-
}
|
|
103274
|
-
/**
|
|
103275
|
-
* Creates an HTMLBlock node from HTML string and optional attributes
|
|
103276
|
-
*/
|
|
103277
|
-
function createHTMLBlockNode(htmlString, position, runScripts, safeMode) {
|
|
103278
|
-
return {
|
|
103279
|
-
position,
|
|
103280
|
-
children: [{ type: 'text', value: htmlString }],
|
|
103281
|
-
type: NodeTypes.htmlBlock,
|
|
103282
|
-
data: {
|
|
103283
|
-
hName: 'html-block',
|
|
103284
|
-
hProperties: {
|
|
103285
|
-
html: htmlString,
|
|
103286
|
-
...(runScripts !== undefined && { runScripts }),
|
|
103287
|
-
...(safeMode !== undefined && { safeMode }),
|
|
103288
|
-
},
|
|
103289
|
-
},
|
|
103290
|
-
};
|
|
103291
|
-
}
|
|
103292
|
-
/**
|
|
103293
|
-
* Checks for opening tag only (for split detection)
|
|
103294
|
-
*/
|
|
103295
|
-
function hasOpeningTagOnly(node) {
|
|
103296
|
-
let hasOpening = false;
|
|
103297
|
-
let hasClosed = false;
|
|
103298
|
-
let attrs = '';
|
|
103299
|
-
const check = (n) => {
|
|
103300
|
-
if (n.type === 'html' && n.value) {
|
|
103301
|
-
if (n.value === '<HTMLBlock>') {
|
|
103302
|
-
hasOpening = true;
|
|
103303
|
-
}
|
|
103304
|
-
else {
|
|
103305
|
-
const match = n.value.match(/^<HTMLBlock(\s[^>]*)?>$/);
|
|
103306
|
-
if (match) {
|
|
103307
|
-
hasOpening = true;
|
|
103308
|
-
attrs = match[1] || '';
|
|
103309
|
-
}
|
|
103310
|
-
}
|
|
103311
|
-
if (n.value === '</HTMLBlock>' || n.value.includes('</HTMLBlock>')) {
|
|
103312
|
-
hasClosed = true;
|
|
103313
|
-
}
|
|
103314
|
-
}
|
|
103315
|
-
if (n.children && Array.isArray(n.children)) {
|
|
103316
|
-
n.children.forEach(child => {
|
|
103317
|
-
check(child);
|
|
103318
|
-
});
|
|
103319
|
-
}
|
|
103320
|
-
};
|
|
103321
|
-
check(node);
|
|
103322
|
-
// Return true only if opening without closing (split case)
|
|
103323
|
-
return { attrs, found: hasOpening && !hasClosed };
|
|
103324
|
-
}
|
|
103325
|
-
/**
|
|
103326
|
-
* Checks if a node contains an HTMLBlock closing tag
|
|
103327
|
-
*/
|
|
103328
|
-
function hasClosingTag(node) {
|
|
103329
|
-
if (node.type === 'html' && node.value) {
|
|
103330
|
-
if (node.value === '</HTMLBlock>' || node.value.includes('</HTMLBlock>'))
|
|
103331
|
-
return true;
|
|
103332
|
-
}
|
|
103333
|
-
if (node.children && Array.isArray(node.children)) {
|
|
103334
|
-
return node.children.some(child => hasClosingTag(child));
|
|
103335
|
-
}
|
|
103336
|
-
return false;
|
|
103337
|
-
}
|
|
103338
|
-
/**
|
|
103339
|
-
* Transforms HTMLBlock MDX JSX to html-block nodes. Handles <HTMLBlock>{`...`}</HTMLBlock> syntax.
|
|
103106
|
+
* Runs *after* `mdxishTables` so table cells are re-parsed first;
|
|
103107
|
+
* `mdxishTables` recognizes the still-JSX `<HTMLBlock>` element when deciding to
|
|
103108
|
+
* keep a table as a JSX `<Table>`. This replaces the old base64-comment marker
|
|
103109
|
+
* machinery — the #1455 tokenizer hands the body over already parsed.
|
|
103340
103110
|
*/
|
|
103341
103111
|
const mdxishHtmlBlocks = () => tree => {
|
|
103342
|
-
//
|
|
103343
|
-
visit(tree, '
|
|
103344
|
-
const
|
|
103345
|
-
|
|
103346
|
-
|
|
103347
|
-
|
|
103348
|
-
|
|
103349
|
-
|
|
103350
|
-
// Find closing tag in subsequent siblings
|
|
103351
|
-
let closingIdx = -1;
|
|
103352
|
-
for (let j = i + 1; j < children.length; j += 1) {
|
|
103353
|
-
if (hasClosingTag(children[j])) {
|
|
103354
|
-
closingIdx = j;
|
|
103355
|
-
break;
|
|
103356
|
-
}
|
|
103357
|
-
}
|
|
103358
|
-
if (closingIdx !== -1) {
|
|
103359
|
-
// Collect inner content between tags
|
|
103360
|
-
const contentParts = [];
|
|
103361
|
-
for (let j = i; j <= closingIdx; j += 1) {
|
|
103362
|
-
const node = children[j];
|
|
103363
|
-
contentParts.push(collectTextContent(node));
|
|
103364
|
-
}
|
|
103365
|
-
// Remove the opening/closing tags and template literal syntax from content
|
|
103366
|
-
let content = contentParts.join('');
|
|
103367
|
-
content = content.replace(/^<HTMLBlock[^>]*>\s*\{?\s*`?/, '').replace(/`?\s*\}?\s*<\/HTMLBlock>$/, '');
|
|
103368
|
-
// Decode protected content that was base64 encoded during preprocessing
|
|
103369
|
-
content = decodeProtectedContent(content);
|
|
103370
|
-
const htmlString = formatHtmlForMdxish(content);
|
|
103371
|
-
const runScripts = extractRunScriptsAttr(attrs);
|
|
103372
|
-
const safeMode = extractBooleanAttr(attrs, 'safeMode');
|
|
103373
|
-
// Replace range with single HTMLBlock node
|
|
103374
|
-
const mdNode = createHTMLBlockNode(htmlString, children[i].position, runScripts, safeMode);
|
|
103375
|
-
root.children.splice(i, closingIdx - i + 1, mdNode);
|
|
103376
|
-
}
|
|
103377
|
-
}
|
|
103378
|
-
i += 1;
|
|
103379
|
-
}
|
|
103112
|
+
// Shape 1: tokenized JSX element.
|
|
103113
|
+
visit(tree, node => node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement', (node, index, parent) => {
|
|
103114
|
+
const element = node;
|
|
103115
|
+
if (element.name !== 'HTMLBlock' || !parent || index === undefined)
|
|
103116
|
+
return;
|
|
103117
|
+
const exprChild = element.children.find(child => child.type === 'mdxFlowExpression' || child.type === 'mdxTextExpression');
|
|
103118
|
+
const openingTagIndent = (element.position?.start.column ?? 1) - 1;
|
|
103119
|
+
parent.children[index] = createHtmlBlockNode(formatHtmlForMdxish(extractTemplateLiteral(exprChild?.value), openingTagIndent), element.position, toRunScripts(jsxAttr(element, 'runScripts')), jsxAttr(element, 'safeMode'));
|
|
103380
103120
|
});
|
|
103381
|
-
//
|
|
103121
|
+
// Shape 2: raw HTML blob.
|
|
103382
103122
|
visit(tree, 'html', (node, index, parent) => {
|
|
103383
103123
|
if (!parent || index === undefined)
|
|
103384
103124
|
return;
|
|
103385
|
-
const
|
|
103386
|
-
if (
|
|
103387
|
-
|
|
103388
|
-
// Case 1: Full HTMLBlock in single node
|
|
103389
|
-
const fullMatch = value.match(/^<HTMLBlock(\s[^>]*)?>([\s\S]*)<\/HTMLBlock>$/);
|
|
103390
|
-
if (fullMatch) {
|
|
103391
|
-
const attrs = fullMatch[1] || '';
|
|
103392
|
-
let content = fullMatch[2] || '';
|
|
103393
|
-
// Remove template literal syntax if present: {`...`}
|
|
103394
|
-
content = content.replace(/^\s*\{\s*`/, '').replace(/`\s*\}\s*$/, '');
|
|
103395
|
-
// Decode protected content that was base64 encoded during preprocessing
|
|
103396
|
-
content = decodeProtectedContent(content);
|
|
103397
|
-
const htmlString = formatHtmlForMdxish(content);
|
|
103398
|
-
const runScripts = extractRunScriptsAttr(attrs);
|
|
103399
|
-
const safeMode = extractBooleanAttr(attrs, 'safeMode');
|
|
103400
|
-
parent.children[index] = createHTMLBlockNode(htmlString, node.position, runScripts, safeMode);
|
|
103401
|
-
return;
|
|
103402
|
-
}
|
|
103403
|
-
// Case 2: Opening tag only (split by blank lines)
|
|
103404
|
-
if (value === '<HTMLBlock>' || value.match(/^<HTMLBlock\s[^>]*>$/)) {
|
|
103405
|
-
const siblings = parent.children;
|
|
103406
|
-
let closingIdx = -1;
|
|
103407
|
-
// Find closing tag in siblings
|
|
103408
|
-
for (let i = index + 1; i < siblings.length; i += 1) {
|
|
103409
|
-
const sibling = siblings[i];
|
|
103410
|
-
if (sibling.type === 'html') {
|
|
103411
|
-
const sibVal = sibling.value;
|
|
103412
|
-
if (sibVal === '</HTMLBlock>' || sibVal?.includes('</HTMLBlock>')) {
|
|
103413
|
-
closingIdx = i;
|
|
103414
|
-
break;
|
|
103415
|
-
}
|
|
103416
|
-
}
|
|
103417
|
-
}
|
|
103418
|
-
if (closingIdx === -1)
|
|
103419
|
-
return;
|
|
103420
|
-
// Collect content between tags, skipping template literal delimiters
|
|
103421
|
-
const contentParts = [];
|
|
103422
|
-
for (let i = index + 1; i < closingIdx; i += 1) {
|
|
103423
|
-
const sibling = siblings[i];
|
|
103424
|
-
// Skip template literal delimiters
|
|
103425
|
-
if (sibling.type === 'text') {
|
|
103426
|
-
const textVal = sibling.value;
|
|
103427
|
-
if (textVal === '{' || textVal === '}' || textVal === '{`' || textVal === '`}') {
|
|
103428
|
-
// eslint-disable-next-line no-continue
|
|
103429
|
-
continue;
|
|
103430
|
-
}
|
|
103431
|
-
}
|
|
103432
|
-
contentParts.push(collectTextContent(sibling));
|
|
103433
|
-
}
|
|
103434
|
-
// Decode protected content that was base64 encoded during preprocessing
|
|
103435
|
-
const decodedContent = decodeProtectedContent(contentParts.join(''));
|
|
103436
|
-
const htmlString = formatHtmlForMdxish(decodedContent);
|
|
103437
|
-
const runScripts = extractRunScriptsAttr(value);
|
|
103438
|
-
const safeMode = extractBooleanAttr(value, 'safeMode');
|
|
103439
|
-
// Replace opening tag with HTMLBlock node, remove consumed siblings
|
|
103440
|
-
parent.children[index] = createHTMLBlockNode(htmlString, node.position, runScripts, safeMode);
|
|
103441
|
-
parent.children.splice(index + 1, closingIdx - index);
|
|
103442
|
-
}
|
|
103125
|
+
const replacement = splitRawHtmlBlocks(node);
|
|
103126
|
+
if (replacement)
|
|
103127
|
+
parent.children.splice(index, 1, ...replacement);
|
|
103443
103128
|
});
|
|
103444
|
-
//
|
|
103445
|
-
|
|
103446
|
-
|
|
103447
|
-
|
|
103448
|
-
|
|
103449
|
-
|
|
103450
|
-
let htmlBlockEndIdx = -1;
|
|
103451
|
-
let templateLiteralStartIdx = -1;
|
|
103452
|
-
let templateLiteralEndIdx = -1;
|
|
103129
|
+
// Shape 3: inline within a paragraph — `<HTMLBlock>` open/close arrive as
|
|
103130
|
+
// separate `html` siblings with the template-literal expression between them.
|
|
103131
|
+
visit(tree, 'paragraph', (paragraph) => {
|
|
103132
|
+
// An html-block is block content, so it isn't a valid PhrasingContent child;
|
|
103133
|
+
// widen to RootContent (which HTMLBlock belongs to) for the in-place splice.
|
|
103134
|
+
const children = paragraph.children;
|
|
103453
103135
|
for (let i = 0; i < children.length; i += 1) {
|
|
103454
|
-
const
|
|
103455
|
-
|
|
103456
|
-
|
|
103457
|
-
|
|
103458
|
-
|
|
103459
|
-
|
|
103460
|
-
|
|
103461
|
-
|
|
103462
|
-
|
|
103463
|
-
|
|
103464
|
-
|
|
103465
|
-
|
|
103466
|
-
const value = child.value;
|
|
103467
|
-
if (value === '{') {
|
|
103468
|
-
templateLiteralStartIdx = i;
|
|
103469
|
-
}
|
|
103470
|
-
}
|
|
103471
|
-
// Find closing brace before HTMLBlock end
|
|
103472
|
-
if (htmlBlockStartIdx !== -1 && htmlBlockEndIdx === -1 && child.type === 'text') {
|
|
103473
|
-
const value = child.value;
|
|
103474
|
-
if (value === '}') {
|
|
103475
|
-
templateLiteralEndIdx = i;
|
|
103136
|
+
const open = children[i];
|
|
103137
|
+
const openMatch = open.type === 'html' ? open.value.match(HTML_BLOCK_OPEN_RE) : null;
|
|
103138
|
+
if (!openMatch)
|
|
103139
|
+
continue; // eslint-disable-line no-continue
|
|
103140
|
+
const closeIdx = children.findIndex((child, j) => j > i && child.type === 'html' && child.value === '</HTMLBlock>');
|
|
103141
|
+
if (closeIdx === -1)
|
|
103142
|
+
continue; // eslint-disable-line no-continue
|
|
103143
|
+
const body = children
|
|
103144
|
+
.slice(i + 1, closeIdx)
|
|
103145
|
+
.map(child => {
|
|
103146
|
+
if (child.type === 'mdxTextExpression' || child.type === 'mdxFlowExpression') {
|
|
103147
|
+
return extractTemplateLiteral(child.value);
|
|
103476
103148
|
}
|
|
103477
|
-
|
|
103478
|
-
|
|
103479
|
-
|
|
103480
|
-
|
|
103481
|
-
|
|
103482
|
-
|
|
103483
|
-
|
|
103484
|
-
|
|
103485
|
-
// Collect content between braces (handles code blocks)
|
|
103486
|
-
const templateContent = [];
|
|
103487
|
-
for (let i = templateLiteralStartIdx + 1; i < templateLiteralEndIdx; i += 1) {
|
|
103488
|
-
const child = children[i];
|
|
103489
|
-
templateContent.push(collectTextContent(child));
|
|
103490
|
-
}
|
|
103491
|
-
// Decode protected content that was base64 encoded during preprocessing
|
|
103492
|
-
const decodedContent = decodeProtectedContent(templateContent.join(''));
|
|
103493
|
-
const htmlString = formatHtmlForMdxish(decodedContent);
|
|
103494
|
-
const runScripts = openingTag.value ? extractRunScriptsAttr(openingTag.value) : undefined;
|
|
103495
|
-
const safeMode = openingTag.value ? extractBooleanAttr(openingTag.value, 'safeMode') : undefined;
|
|
103496
|
-
const mdNode = createHTMLBlockNode(htmlString, node.position, runScripts, safeMode);
|
|
103497
|
-
parent.children[index] = mdNode;
|
|
103498
|
-
}
|
|
103499
|
-
});
|
|
103500
|
-
// Ensure html-block nodes have HTML in children as text node
|
|
103501
|
-
visit(tree, 'html-block', (node) => {
|
|
103502
|
-
const html = node.data?.hProperties?.html;
|
|
103503
|
-
if (html &&
|
|
103504
|
-
(!node.children ||
|
|
103505
|
-
node.children.length === 0 ||
|
|
103506
|
-
(node.children.length === 1 && node.children[0].type === 'text' && node.children[0].value !== html))) {
|
|
103507
|
-
node.children = [
|
|
103508
|
-
{
|
|
103509
|
-
type: 'text',
|
|
103510
|
-
value: html,
|
|
103511
|
-
},
|
|
103512
|
-
];
|
|
103149
|
+
// Preserve raw text from any other phrasing sibling (e.g. stray
|
|
103150
|
+
// whitespace or content the tokenizer didn't claim) so it isn't
|
|
103151
|
+
// silently dropped from the html payload.
|
|
103152
|
+
return 'value' in child && typeof child.value === 'string' ? child.value : '';
|
|
103153
|
+
})
|
|
103154
|
+
.join('');
|
|
103155
|
+
const openingTagIndent = (open.position?.start.column ?? 1) - 1;
|
|
103156
|
+
children.splice(i, closeIdx - i + 1, htmlBlockFromRaw(openMatch[1], body, open.position, openingTagIndent));
|
|
103513
103157
|
}
|
|
103514
103158
|
});
|
|
103515
|
-
return tree;
|
|
103516
103159
|
};
|
|
103517
103160
|
/* harmony default export */ const mdxish_html_blocks = (mdxishHtmlBlocks);
|
|
103518
103161
|
|
|
@@ -104293,6 +103936,189 @@ const normalizeMdxJsxNodes = () => tree => {
|
|
|
104293
103936
|
};
|
|
104294
103937
|
/* harmony default export */ const normalize_mdx_jsx_nodes = (normalizeMdxJsxNodes);
|
|
104295
103938
|
|
|
103939
|
+
;// ./lib/micromark/jsx-comment/pattern.ts
|
|
103940
|
+
/**
|
|
103941
|
+
* Matches a JSX comment: `{/*`, content, `*\/}` — no whitespace tolerated
|
|
103942
|
+
* between the braces and the comment markers.
|
|
103943
|
+
*
|
|
103944
|
+
* This grammar is mirrored by the flow tokenizer in ./syntax.ts. Any change
|
|
103945
|
+
* here needs a mirror change in the state machine; the parity test in
|
|
103946
|
+
* __tests__/lib/micromark/jsx-comment-pattern-parity.test.ts locks the two
|
|
103947
|
+
* together so they can't silently drift.
|
|
103948
|
+
*/
|
|
103949
|
+
const JSX_COMMENT_REGEX = /\{\/\*[^*]*(?:\*(?!\/)[^*]*)*\*\/\}/g;
|
|
103950
|
+
|
|
103951
|
+
;// ./processor/transform/mdxish/preprocess-jsx-expressions.ts
|
|
103952
|
+
|
|
103953
|
+
|
|
103954
|
+
/**
|
|
103955
|
+
* Removes JSX-style comments (e.g., { /* comment *\/ }) from content.
|
|
103956
|
+
*
|
|
103957
|
+
* @param content
|
|
103958
|
+
* @returns Content with JSX comments removed
|
|
103959
|
+
* @example
|
|
103960
|
+
* ```typescript
|
|
103961
|
+
* removeJSXComments('Text { /* comment *\/ } more text')
|
|
103962
|
+
* // Returns: 'Text more text'
|
|
103963
|
+
* ```
|
|
103964
|
+
*/
|
|
103965
|
+
function removeJSXComments(content) {
|
|
103966
|
+
return content.replace(JSX_COMMENT_REGEX, '');
|
|
103967
|
+
}
|
|
103968
|
+
const HTML_ELEM_PLACEHOLDER_PREFIX = '___MDXISH_HTML_ELEM_';
|
|
103969
|
+
const HTML_ELEM_PLACEHOLDER = new RegExp(`${HTML_ELEM_PLACEHOLDER_PREFIX}(\\d+)___`, 'g');
|
|
103970
|
+
// Matches an HTML element that starts at a line boundary and ends at a line boundary.
|
|
103971
|
+
// Allows optional leading indentation and lazily matches until the same closing tag.
|
|
103972
|
+
const BLOCK_HTML_RE = /(?<=^|\n)[ \t]*<([a-z][a-zA-Z0-9]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>[ \t]*(?=\n|$)/g;
|
|
103973
|
+
/**
|
|
103974
|
+
* Hides line-anchored HTML elements from the brace-escaping pass so we don't leak `\{`
|
|
103975
|
+
* into rendered output (rehypeRaw renders the `\` literally, e.g. `<div>{foo</div>`).
|
|
103976
|
+
*
|
|
103977
|
+
* One carve-out: if an interior line at column 0 has bare text containing `{`, mdxish
|
|
103978
|
+
* parses that line as a paragraph and the mdxExpression step would throw without an
|
|
103979
|
+
* escape — so we leave that case to the brace balancer.
|
|
103980
|
+
*/
|
|
103981
|
+
function protectHTMLElements(content) {
|
|
103982
|
+
const htmlElements = [];
|
|
103983
|
+
const protectedContent = content.replace(BLOCK_HTML_RE, match => {
|
|
103984
|
+
// Look at the lines between the open and close tags. If any of them starts
|
|
103985
|
+
// at column 0 with bare text (not whitespace, not another tag) and contains
|
|
103986
|
+
// `{`, mdxish will parse that line as a paragraph and the brace as an MDX
|
|
103987
|
+
// expression, which would throw an error. So we let the brace balancer escape it.
|
|
103988
|
+
// Otherwise, we need to extract the sequence to protect it from the brace escaping.
|
|
103989
|
+
const interior = match.split('\n').slice(1, -1);
|
|
103990
|
+
const hazard = interior.some(line => line.length > 0 && line[0] !== ' ' && line[0] !== '\t' && line[0] !== '<' && line.includes('{'));
|
|
103991
|
+
if (hazard)
|
|
103992
|
+
return match;
|
|
103993
|
+
htmlElements.push(match);
|
|
103994
|
+
return `${HTML_ELEM_PLACEHOLDER_PREFIX}${htmlElements.length - 1}___`;
|
|
103995
|
+
});
|
|
103996
|
+
return { htmlElements, protectedContent };
|
|
103997
|
+
}
|
|
103998
|
+
function restoreHTMLElements(content, htmlElements) {
|
|
103999
|
+
if (htmlElements.length === 0)
|
|
104000
|
+
return content;
|
|
104001
|
+
return content.replace(HTML_ELEM_PLACEHOLDER, (_m, idx) => htmlElements[parseInt(idx, 10)]);
|
|
104002
|
+
}
|
|
104003
|
+
/**
|
|
104004
|
+
* Escapes unbalanced and paragraph-spanning braces so MDX doesn't trip on them.
|
|
104005
|
+
*/
|
|
104006
|
+
function escapeProblematicBraces(content) {
|
|
104007
|
+
const { htmlElements, protectedContent } = protectHTMLElements(content);
|
|
104008
|
+
let strDelim = null;
|
|
104009
|
+
let strEscaped = false;
|
|
104010
|
+
// Track position of last newline (outside strings) to detect blank lines
|
|
104011
|
+
// -2 means no recent newline
|
|
104012
|
+
let lastNewlinePos = -2;
|
|
104013
|
+
// Character state machine trackers
|
|
104014
|
+
const toEscape = new Set();
|
|
104015
|
+
// Convert to array of Unicode code points so that emojis and multi-byte characters are correctly tracked
|
|
104016
|
+
const chars = Array.from(protectedContent);
|
|
104017
|
+
const openStack = [];
|
|
104018
|
+
for (let i = 0; i < chars.length; i += 1) {
|
|
104019
|
+
const ch = chars[i];
|
|
104020
|
+
// Track string delimiters inside expressions to ignore braces within them
|
|
104021
|
+
if (openStack.length > 0) {
|
|
104022
|
+
if (strDelim) {
|
|
104023
|
+
if (strEscaped)
|
|
104024
|
+
strEscaped = false;
|
|
104025
|
+
else if (ch === '\\')
|
|
104026
|
+
strEscaped = true;
|
|
104027
|
+
else if (ch === strDelim)
|
|
104028
|
+
strDelim = null;
|
|
104029
|
+
// eslint-disable-next-line no-continue
|
|
104030
|
+
continue;
|
|
104031
|
+
}
|
|
104032
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
104033
|
+
strDelim = ch;
|
|
104034
|
+
// eslint-disable-next-line no-continue
|
|
104035
|
+
continue;
|
|
104036
|
+
}
|
|
104037
|
+
if (ch === '\n') {
|
|
104038
|
+
if (lastNewlinePos >= 0) {
|
|
104039
|
+
const between = chars.slice(lastNewlinePos + 1, i).join('');
|
|
104040
|
+
if (/^[ \t]*$/.test(between)) {
|
|
104041
|
+
openStack.forEach(entry => { entry.hasBlankLine = true; });
|
|
104042
|
+
}
|
|
104043
|
+
}
|
|
104044
|
+
lastNewlinePos = i;
|
|
104045
|
+
}
|
|
104046
|
+
}
|
|
104047
|
+
// Skip already-escaped braces (odd run of preceding backslashes).
|
|
104048
|
+
if (ch === '{' || ch === '}') {
|
|
104049
|
+
let bs = 0;
|
|
104050
|
+
for (let j = i - 1; j >= 0 && chars[j] === '\\'; j -= 1)
|
|
104051
|
+
bs += 1;
|
|
104052
|
+
if (bs % 2 === 1) {
|
|
104053
|
+
// eslint-disable-next-line no-continue
|
|
104054
|
+
continue;
|
|
104055
|
+
}
|
|
104056
|
+
}
|
|
104057
|
+
if (ch === '{') {
|
|
104058
|
+
// `=` (after whitespace) before `{` ⇒ JSX attribute expression. The
|
|
104059
|
+
// mdxComponent tokenizer captures the whole component, so blank lines
|
|
104060
|
+
// inside attribute values are harmless. Nested `{` inherits the flag.
|
|
104061
|
+
let isAttrExpr = false;
|
|
104062
|
+
for (let j = i - 1; j >= 0; j -= 1) {
|
|
104063
|
+
const pc = chars[j];
|
|
104064
|
+
if (pc === '=') {
|
|
104065
|
+
isAttrExpr = true;
|
|
104066
|
+
break;
|
|
104067
|
+
}
|
|
104068
|
+
if (pc !== ' ' && pc !== '\t')
|
|
104069
|
+
break;
|
|
104070
|
+
}
|
|
104071
|
+
// Nested `{ ... }` inside an attribute value (e.g. `data={[{ ... }]}` or
|
|
104072
|
+
// `data={{ a: { b: 1 } }}`) must inherit the same exemption; only the
|
|
104073
|
+
// outer `{` is directly after `=`.
|
|
104074
|
+
if (!isAttrExpr && openStack.length > 0 && openStack[openStack.length - 1].isAttrExpr) {
|
|
104075
|
+
isAttrExpr = true;
|
|
104076
|
+
}
|
|
104077
|
+
openStack.push({ pos: i, hasBlankLine: false, isAttrExpr });
|
|
104078
|
+
lastNewlinePos = -2;
|
|
104079
|
+
}
|
|
104080
|
+
else if (ch === '}') {
|
|
104081
|
+
if (openStack.length > 0) {
|
|
104082
|
+
const entry = openStack.pop();
|
|
104083
|
+
// Pure `{/* ... */}` comments are handled downstream by the jsxComment
|
|
104084
|
+
// tokenizer — escaping their braces would prevent it from running.
|
|
104085
|
+
const isPureJsxComment = chars[entry.pos + 1] === '/' &&
|
|
104086
|
+
chars[entry.pos + 2] === '*' &&
|
|
104087
|
+
chars[i - 1] === '/' &&
|
|
104088
|
+
chars[i - 2] === '*';
|
|
104089
|
+
if (entry.hasBlankLine && !isPureJsxComment && !entry.isAttrExpr) {
|
|
104090
|
+
toEscape.add(entry.pos);
|
|
104091
|
+
toEscape.add(i);
|
|
104092
|
+
}
|
|
104093
|
+
}
|
|
104094
|
+
else {
|
|
104095
|
+
toEscape.add(i);
|
|
104096
|
+
}
|
|
104097
|
+
}
|
|
104098
|
+
}
|
|
104099
|
+
// Anything still open is unbalanced.
|
|
104100
|
+
openStack.forEach(entry => toEscape.add(entry.pos));
|
|
104101
|
+
// Reconstruct the content with the escaped braces.
|
|
104102
|
+
const escapedContent = toEscape.size === 0 ? protectedContent : chars.map((ch, i) => (toEscape.has(i) ? `\\${ch}` : ch)).join('');
|
|
104103
|
+
return restoreHTMLElements(escapedContent, htmlElements);
|
|
104104
|
+
}
|
|
104105
|
+
/**
|
|
104106
|
+
* Preprocesses JSX-like markdown content before parsing.
|
|
104107
|
+
*
|
|
104108
|
+
* JSX attribute expressions (`href={baseUrl}`) are no longer rewritten here —
|
|
104109
|
+
* they flow through the tokenizer as `mdxJsxAttributeValueExpression` nodes
|
|
104110
|
+
* and are evaluated at the hast handler step.
|
|
104111
|
+
*
|
|
104112
|
+
* @param content
|
|
104113
|
+
* @returns Preprocessed content ready for markdown parsing
|
|
104114
|
+
*/
|
|
104115
|
+
function preprocessJSXExpressions(content) {
|
|
104116
|
+
const { protectedCode, protectedContent } = protectCodeBlocks(content);
|
|
104117
|
+
let processed = escapeProblematicBraces(protectedContent);
|
|
104118
|
+
processed = restoreCodeBlocks(processed, protectedCode);
|
|
104119
|
+
return processed;
|
|
104120
|
+
}
|
|
104121
|
+
|
|
104296
104122
|
;// ./processor/transform/mdxish/restore-snake-case-component-name.ts
|
|
104297
104123
|
|
|
104298
104124
|
|
|
@@ -105409,7 +105235,7 @@ function mdxishAstProcessor(mdContent, opts = {}) {
|
|
|
105409
105235
|
.use(inline_html, { safeMode })
|
|
105410
105236
|
.use(restore_snake_case_component_name, { mapping: snakeCaseMapping })
|
|
105411
105237
|
.use(mdxish_tables)
|
|
105412
|
-
.use(mdxish_html_blocks)
|
|
105238
|
+
.use(mdxish_html_blocks) // Convert every <HTMLBlock> shape → html-block
|
|
105413
105239
|
// The next few transformers must appear after mdxishMdxComponentBlocks
|
|
105414
105240
|
// so nodes produced by the inline re-parse of component bodies
|
|
105415
105241
|
// (e.g. code/image/embed inside <Tabs>) get visited too
|