@readme/markdown 14.6.0 → 14.7.1

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/main.js CHANGED
@@ -12273,11 +12273,20 @@ const Image = (Props) => {
12273
12273
  lightboxOverlay));
12274
12274
  }
12275
12275
  if (children || caption) {
12276
- return (external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_.createElement("figure", null,
12277
- closedLightbox(alt || 'Expand image', 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,
12278
- imgElement,
12279
- external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_.createElement("figcaption", null, children || caption))),
12280
- lightboxOverlay));
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 content from template literal
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 = processed.replace(/^\s*\n|\n\s*$/g, '');
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
- // Both match (both bare or both attached) — mdxjs parses this fine.
75685
- if (openerHasExtras === closerHasExtras)
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
- // Asymmetric. Push non-tag content on the offending side to its own line,
75688
- // visually aligning the inserted line with the existing whitespace prefix.
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))
@@ -76313,13 +76355,32 @@ const processTableNode = (node, index, parent, documentPosition) => {
76313
76355
  // remarkMdx wraps inline `<tr>`s in a paragraph; unwrap one level so the
76314
76356
  // hasRow check below sees them.
76315
76357
  const flattenSectionChildren = (nodes) => nodes.flatMap(n => n.type === 'paragraph' && 'children' in n && Array.isArray(n.children) ? n.children : [n]);
76316
- visit(node, isMDXElement, (child) => {
76317
- if (child.name !== 'thead' && child.name !== 'tbody')
76358
+ // Iterate the table's direct children in document order. Rows may live
76359
+ // inside a `<thead>`/`<tbody>` section, or sit bare directly under the table
76360
+ // with no section wrapper — both are collected here so the first row becomes
76361
+ // the markdown table header.
76362
+ const tableChildren = flattenSectionChildren(node.children);
76363
+ tableChildren.forEach(child => {
76364
+ if (!isMDXElement(child))
76365
+ return;
76366
+ const childElement = child;
76367
+ // Path for a `<tr>` directly under the table, without a `<thead>`/`<tbody>` wrapper.
76368
+ if (childElement.name === 'tr') {
76369
+ children.push({
76370
+ type: 'tableRow',
76371
+ children: collectCells(childElement),
76372
+ position: childElement.position,
76373
+ });
76374
+ return;
76375
+ }
76376
+ // Path for when the rows are wrapped in a `<thead>`/`<tbody>`
76377
+ // We visit & collect the entire rows under them directly here
76378
+ if (childElement.name !== 'thead' && childElement.name !== 'tbody')
76318
76379
  return;
76319
- const sectionChildren = flattenSectionChildren(child.children);
76380
+ const sectionChildren = flattenSectionChildren(childElement.children);
76320
76381
  const hasRow = sectionChildren.some(c => isMDXElement(c) && c.name === 'tr');
76321
76382
  if (hasRow) {
76322
- visit(child, isMDXElement, (row) => {
76383
+ visit(childElement, isMDXElement, (row) => {
76323
76384
  if (row.name !== 'tr')
76324
76385
  return;
76325
76386
  children.push({
@@ -76333,7 +76394,7 @@ const processTableNode = (node, index, parent, documentPosition) => {
76333
76394
  // No `<tr>`, chunk bare cells into rows using the prior row's column
76334
76395
  // count (e.g. from `<thead>`), so 4 bare `<td>`s under a 2-col header
76335
76396
  // become 2 rows of 2.
76336
- const cells = collectCells(child);
76397
+ const cells = collectCells(childElement);
76337
76398
  if (cells.length === 0)
76338
76399
  return;
76339
76400
  const cols = children[0]?.children?.length || cells.length;
@@ -76341,11 +76402,12 @@ const processTableNode = (node, index, parent, documentPosition) => {
76341
76402
  children.push({
76342
76403
  type: 'tableRow',
76343
76404
  children: cells.slice(i, i + cols),
76344
- position: child.position,
76405
+ position: childElement.position,
76345
76406
  });
76346
76407
  }
76347
76408
  }
76348
76409
  });
76410
+ // Output the markdown table node
76349
76411
  const firstRow = children[0];
76350
76412
  const columnCount = firstRow?.children?.length || 0;
76351
76413
  const alignArray = align && columnCount > 0
@@ -100871,7 +100933,7 @@ function createTokenize(mode) {
100871
100933
  return tagNameRest;
100872
100934
  }
100873
100935
  // Tag name complete — check exclusions
100874
- if (GENERIC_MDX_COMPONENT_EXCLUDED_TAGS.has(tagName)) {
100936
+ if (TOKENIZER_MDX_COMPONENT_EXCLUDED_TAGS.has(tagName)) {
100875
100937
  return nok(code);
100876
100938
  }
100877
100939
  depth = 1;
@@ -102962,557 +103024,158 @@ const magicBlockTransformer = (options = {}) => tree => {
102962
103024
  };
102963
103025
  /* harmony default export */ const magic_block_transformer = (magicBlockTransformer);
102964
103026
 
102965
- ;// ./lib/micromark/jsx-comment/pattern.ts
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;
103027
+ ;// ./processor/transform/mdxish/mdxish-html-blocks.ts
102976
103028
 
102977
- ;// ./processor/transform/mdxish/preprocess-jsx-expressions.ts
102978
103029
 
102979
103030
 
102980
- // Base64 encode (Node.js + browser compatible)
102981
- function base64Encode(str) {
102982
- if (typeof Buffer !== 'undefined') {
102983
- return Buffer.from(str, 'utf-8').toString('base64');
102984
- }
102985
- return btoa(unescape(encodeURIComponent(str)));
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-->';
103031
+ // `<HTMLBlock …>{`…`}</HTMLBlock>` embedded inside a raw HTML block (e.g. a
103032
+ // single-line `<div>…</div>`). CommonMark slurps the whole div as one `html`
103033
+ // node, so the tokenizer never sees the HTMLBlock — we recover it here.
103034
+ const RAW_HTML_BLOCK_RE = /<HTMLBlock\b([^>]*)>\s*\{\s*`((?:[^`\\]|\\.)*)`\s*\}\s*<\/HTMLBlock>/g;
103035
+ // Opening `<HTMLBlock …>` as its own `html` node — produced inside a paragraph
103036
+ // when an HTMLBlock appears inline alongside text.
103037
+ const HTML_BLOCK_OPEN_RE = /^<HTMLBlock\b([^>]*)>$/;
102997
103038
  /**
102998
- * Base64 encodes HTMLBlock template literal content to prevent markdown parser from consuming <script>/<style> tags.
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
- * ```
103039
+ * Builds the canonical `html-block` MDAST node the renderer expects.
103008
103040
  */
103009
- function protectHTMLBlockContent(content) {
103010
- return content.replace(/(<HTMLBlock[^>]*>)\{\s*`((?:[^`\\]|\\.)*)`\s*\}(<\/HTMLBlock>)/g, (_match, openTag, templateContent, closeTag) => {
103011
- const encoded = base64Encode(templateContent);
103012
- return `${openTag}${HTML_BLOCK_CONTENT_START}${encoded}${HTML_BLOCK_CONTENT_END}${closeTag}`;
103013
- });
103014
- }
103041
+ const createHtmlBlockNode = (html, position, runScripts, safeMode) => ({
103042
+ position,
103043
+ children: [{ type: 'text', value: html }],
103044
+ type: NodeTypes.htmlBlock,
103045
+ data: {
103046
+ hName: 'html-block',
103047
+ hProperties: {
103048
+ html,
103049
+ ...(runScripts !== undefined && { runScripts }),
103050
+ ...(safeMode !== undefined && { safeMode }),
103051
+ },
103052
+ },
103053
+ });
103015
103054
  /**
103016
- * Removes JSX-style comments (e.g., { /* comment *\/ }) from content.
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
- * ```
103055
+ * Reads the cooked string out of a brace expression wrapping a single template
103056
+ * literal (`` `<p>n</p>` `` → `<p>n</p>`).
103025
103057
  */
103026
- function removeJSXComments(content) {
103027
- return content.replace(JSX_COMMENT_REGEX, '');
103028
- }
103029
- const HTML_ELEM_PLACEHOLDER_PREFIX = '___MDXISH_HTML_ELEM_';
103030
- const HTML_ELEM_PLACEHOLDER = new RegExp(`${HTML_ELEM_PLACEHOLDER_PREFIX}(\\d+)___`, 'g');
103031
- // Matches an HTML element that starts at a line boundary and ends at a line boundary.
103032
- // Allows optional leading indentation and lazily matches until the same closing tag.
103033
- const BLOCK_HTML_RE = /(?<=^|\n)[ \t]*<([a-z][a-zA-Z0-9]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>[ \t]*(?=\n|$)/g;
103058
+ const extractTemplateLiteral = (value) => {
103059
+ if (!value)
103060
+ return '';
103061
+ const match = value.trim().match(/^`([\s\S]*)`$/);
103062
+ // Non-template-literal bodies (e.g. `{someVar}`) are malformed mdxish input;
103063
+ // returning '' beats shipping JS identifier source as an HTML payload.
103064
+ return match ? match[1] : '';
103065
+ };
103066
+ const toRunScripts = (raw) => raw === 'true' ? true : raw === 'false' ? false : raw;
103067
+ /** Reads an attribute from a raw `<HTMLBlock …>` attribute string. */
103068
+ const rawAttr = (attrs, name) => {
103069
+ const quoted = attrs.match(new RegExp(`\\b${name}\\s*=\\s*"([^"]*)"`));
103070
+ if (quoted)
103071
+ return quoted[1];
103072
+ const expr = attrs.match(new RegExp(`\\b${name}\\s*=\\s*\\{(true|false)\\}`));
103073
+ if (expr)
103074
+ return expr[1];
103075
+ return new RegExp(`\\b${name}\\b`).test(attrs) ? 'true' : undefined;
103076
+ };
103077
+ /** Reads an attribute from a parsed `<HTMLBlock>` JSX element. */
103078
+ const jsxAttr = (element, name) => {
103079
+ const attr = element.attributes.find(a => a.type === 'mdxJsxAttribute' && a.name === name);
103080
+ if (!attr || attr.type !== 'mdxJsxAttribute')
103081
+ return undefined;
103082
+ if (typeof attr.value === 'string')
103083
+ return attr.value;
103084
+ if (attr.value && typeof attr.value === 'object' && 'value' in attr.value)
103085
+ return attr.value.value;
103086
+ return 'true'; // bare boolean attribute, e.g. <HTMLBlock runScripts />
103087
+ };
103088
+ /** Builds an `html-block` from a raw attribute string and (unparsed) body. */
103089
+ const htmlBlockFromRaw = (attrs, html, position, openingTagIndent = 0) => createHtmlBlockNode(formatHtmlForMdxish(html, openingTagIndent), position, toRunScripts(rawAttr(attrs, 'runScripts')), rawAttr(attrs, 'safeMode'));
103034
103090
  /**
103035
- * Hides line-anchored HTML elements from the brace-escaping pass so we don't leak `\{`
103036
- * into rendered output (rehypeRaw renders the `\` literally, e.g. `<div>{foo</div>`).
103091
+ * Splits a raw `html` node that embeds one or more `<HTMLBlock>`s into
103092
+ * `[html before, html-block, html after, …]`. Returns null when there is none.
103037
103093
  *
103038
- * One carve-out: if an interior line at column 0 has bare text containing `{`, mdxish
103039
- * parses that line as a paragraph and the mdxExpression step would throw without an
103040
- * escape — so we leave that case to the brace balancer.
103041
- */
103042
- function protectHTMLElements(content) {
103043
- const htmlElements = [];
103044
- const protectedContent = content.replace(BLOCK_HTML_RE, match => {
103045
- // Look at the lines between the open and close tags. If any of them starts
103046
- // at column 0 with bare text (not whitespace, not another tag) and contains
103047
- // `{`, mdxish will parse that line as a paragraph and the brace as an MDX
103048
- // expression, which would throw an error. So we let the brace balancer escape it.
103049
- // Otherwise, we need to extract the sequence to protect it from the brace escaping.
103050
- const interior = match.split('\n').slice(1, -1);
103051
- const hazard = interior.some(line => line.length > 0 && line[0] !== ' ' && line[0] !== '\t' && line[0] !== '<' && line.includes('{'));
103052
- if (hazard)
103053
- return match;
103054
- htmlElements.push(match);
103055
- return `${HTML_ELEM_PLACEHOLDER_PREFIX}${htmlElements.length - 1}___`;
103056
- });
103057
- return { htmlElements, protectedContent };
103058
- }
103059
- function restoreHTMLElements(content, htmlElements) {
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.
103094
+ * `String.split` on a regex with capture groups interleaves the captures into
103095
+ * the result, so segments arrive as `[text, attrs, body, text, attrs, body, …]`.
103066
103096
  */
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
- }
103097
+ const splitRawHtmlBlocks = (node) => {
103098
+ const segments = node.value.split(RAW_HTML_BLOCK_RE);
103099
+ if (segments.length === 1)
103100
+ return null; // no <HTMLBlock> present
103101
+ const parts = [];
103102
+ for (let i = 0; i < segments.length; i += 3) {
103103
+ const [text, attrs, body] = segments.slice(i, i + 3);
103104
+ if (text)
103105
+ parts.push({ type: 'html', value: text });
103106
+ if (body !== undefined) {
103107
+ // The opening tag's column equals the length of the line it starts on
103108
+ // (the text run since the previous newline preceding the match).
103109
+ const openingTagIndent = text.slice(text.lastIndexOf('\n') + 1).length;
103110
+ parts.push(htmlBlockFromRaw(attrs, body, node.position, openingTagIndent));
103111
+ }
103112
+ }
103113
+ return parts;
103114
+ };
103166
103115
  /**
103167
- * Preprocesses JSX-like markdown content before parsing.
103116
+ * Converts every `<HTMLBlock>` shape that survives parsing into the canonical
103117
+ * `html-block` MDAST node, reading the body from the tokenizer's template-literal
103118
+ * expression. Three shapes occur:
103168
103119
  *
103169
- * JSX attribute expressions (`href={baseUrl}`) are no longer rewritten here —
103170
- * they flow through the tokenizer as `mdxJsxAttributeValueExpression` nodes
103171
- * and are evaluated at the hast handler step.
103120
+ * 1. JSX element (`mdxJsxFlowElement`/`mdxJsxTextElement`) multiline/block
103121
+ * context and table cells (after their remarkMdx re-parse).
103122
+ * 2. Raw `html` blob (`splitRawHtmlBlocks`) single-line top-level, or nested
103123
+ * in raw HTML like an inline `<div>`.
103124
+ * 3. Inline-in-paragraph — split into `html` + expression + `html` siblings.
103172
103125
  *
103173
- * @param content
103174
- * @returns Preprocessed content ready for markdown parsing
103175
- */
103176
- function preprocessJSXExpressions(content) {
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.
103126
+ * Runs *after* `mdxishTables` so table cells are re-parsed first;
103127
+ * `mdxishTables` recognizes the still-JSX `<HTMLBlock>` element when deciding to
103128
+ * keep a table as a JSX `<Table>`. This replaces the old base64-comment marker
103129
+ * machinery — the #1455 tokenizer hands the body over already parsed.
103340
103130
  */
103341
103131
  const mdxishHtmlBlocks = () => tree => {
103342
- // Handle HTMLBlock split across root children (caused by newlines)
103343
- visit(tree, 'root', (root) => {
103344
- const children = root.children;
103345
- let i = 0;
103346
- while (i < children.length) {
103347
- const child = children[i];
103348
- const { attrs, found: hasOpening } = hasOpeningTagOnly(child);
103349
- if (hasOpening) {
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
- }
103132
+ // Shape 1: tokenized JSX element.
103133
+ visit(tree, node => node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement', (node, index, parent) => {
103134
+ const element = node;
103135
+ if (element.name !== 'HTMLBlock' || !parent || index === undefined)
103136
+ return;
103137
+ const exprChild = element.children.find(child => child.type === 'mdxFlowExpression' || child.type === 'mdxTextExpression');
103138
+ const openingTagIndent = (element.position?.start.column ?? 1) - 1;
103139
+ parent.children[index] = createHtmlBlockNode(formatHtmlForMdxish(extractTemplateLiteral(exprChild?.value), openingTagIndent), element.position, toRunScripts(jsxAttr(element, 'runScripts')), jsxAttr(element, 'safeMode'));
103380
103140
  });
103381
- // Handle HTMLBlock parsed as HTML elements (when template literal contains block-level HTML tags)
103141
+ // Shape 2: raw HTML blob.
103382
103142
  visit(tree, 'html', (node, index, parent) => {
103383
103143
  if (!parent || index === undefined)
103384
103144
  return;
103385
- const value = node.value;
103386
- if (!value)
103387
- return;
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
- }
103145
+ const replacement = splitRawHtmlBlocks(node);
103146
+ if (replacement)
103147
+ parent.children.splice(index, 1, ...replacement);
103443
103148
  });
103444
- // Handle HTMLBlock inside paragraphs (parsed as inline elements)
103445
- visit(tree, 'paragraph', (node, index, parent) => {
103446
- if (!parent || index === undefined)
103447
- return;
103448
- const children = node.children || [];
103449
- let htmlBlockStartIdx = -1;
103450
- let htmlBlockEndIdx = -1;
103451
- let templateLiteralStartIdx = -1;
103452
- let templateLiteralEndIdx = -1;
103149
+ // Shape 3: inline within a paragraph `<HTMLBlock>` open/close arrive as
103150
+ // separate `html` siblings with the template-literal expression between them.
103151
+ visit(tree, 'paragraph', (paragraph) => {
103152
+ // An html-block is block content, so it isn't a valid PhrasingContent child;
103153
+ // widen to RootContent (which HTMLBlock belongs to) for the in-place splice.
103154
+ const children = paragraph.children;
103453
103155
  for (let i = 0; i < children.length; i += 1) {
103454
- const child = children[i];
103455
- if (child.type === 'html' && typeof child.value === 'string') {
103456
- const value = child.value;
103457
- if (value === '<HTMLBlock>' || value.match(/^<HTMLBlock\s[^>]*>$/)) {
103458
- htmlBlockStartIdx = i;
103459
- }
103460
- else if (value === '</HTMLBlock>') {
103461
- htmlBlockEndIdx = i;
103156
+ const open = children[i];
103157
+ const openMatch = open.type === 'html' ? open.value.match(HTML_BLOCK_OPEN_RE) : null;
103158
+ if (!openMatch)
103159
+ continue; // eslint-disable-line no-continue
103160
+ const closeIdx = children.findIndex((child, j) => j > i && child.type === 'html' && child.value === '</HTMLBlock>');
103161
+ if (closeIdx === -1)
103162
+ continue; // eslint-disable-line no-continue
103163
+ const body = children
103164
+ .slice(i + 1, closeIdx)
103165
+ .map(child => {
103166
+ if (child.type === 'mdxTextExpression' || child.type === 'mdxFlowExpression') {
103167
+ return extractTemplateLiteral(child.value);
103462
103168
  }
103463
- }
103464
- // Find opening brace after HTMLBlock start
103465
- if (htmlBlockStartIdx !== -1 && templateLiteralStartIdx === -1 && child.type === 'text') {
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;
103476
- }
103477
- }
103478
- }
103479
- if (htmlBlockStartIdx !== -1 &&
103480
- htmlBlockEndIdx !== -1 &&
103481
- templateLiteralStartIdx !== -1 &&
103482
- templateLiteralEndIdx !== -1 &&
103483
- templateLiteralStartIdx < templateLiteralEndIdx) {
103484
- const openingTag = children[htmlBlockStartIdx];
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
- ];
103169
+ // Preserve raw text from any other phrasing sibling (e.g. stray
103170
+ // whitespace or content the tokenizer didn't claim) so it isn't
103171
+ // silently dropped from the html payload.
103172
+ return 'value' in child && typeof child.value === 'string' ? child.value : '';
103173
+ })
103174
+ .join('');
103175
+ const openingTagIndent = (open.position?.start.column ?? 1) - 1;
103176
+ children.splice(i, closeIdx - i + 1, htmlBlockFromRaw(openMatch[1], body, open.position, openingTagIndent));
103513
103177
  }
103514
103178
  });
103515
- return tree;
103516
103179
  };
103517
103180
  /* harmony default export */ const mdxish_html_blocks = (mdxishHtmlBlocks);
103518
103181
 
@@ -104293,6 +103956,189 @@ const normalizeMdxJsxNodes = () => tree => {
104293
103956
  };
104294
103957
  /* harmony default export */ const normalize_mdx_jsx_nodes = (normalizeMdxJsxNodes);
104295
103958
 
103959
+ ;// ./lib/micromark/jsx-comment/pattern.ts
103960
+ /**
103961
+ * Matches a JSX comment: `{/*`, content, `*\/}` — no whitespace tolerated
103962
+ * between the braces and the comment markers.
103963
+ *
103964
+ * This grammar is mirrored by the flow tokenizer in ./syntax.ts. Any change
103965
+ * here needs a mirror change in the state machine; the parity test in
103966
+ * __tests__/lib/micromark/jsx-comment-pattern-parity.test.ts locks the two
103967
+ * together so they can't silently drift.
103968
+ */
103969
+ const JSX_COMMENT_REGEX = /\{\/\*[^*]*(?:\*(?!\/)[^*]*)*\*\/\}/g;
103970
+
103971
+ ;// ./processor/transform/mdxish/preprocess-jsx-expressions.ts
103972
+
103973
+
103974
+ /**
103975
+ * Removes JSX-style comments (e.g., { /* comment *\/ }) from content.
103976
+ *
103977
+ * @param content
103978
+ * @returns Content with JSX comments removed
103979
+ * @example
103980
+ * ```typescript
103981
+ * removeJSXComments('Text { /* comment *\/ } more text')
103982
+ * // Returns: 'Text more text'
103983
+ * ```
103984
+ */
103985
+ function removeJSXComments(content) {
103986
+ return content.replace(JSX_COMMENT_REGEX, '');
103987
+ }
103988
+ const HTML_ELEM_PLACEHOLDER_PREFIX = '___MDXISH_HTML_ELEM_';
103989
+ const HTML_ELEM_PLACEHOLDER = new RegExp(`${HTML_ELEM_PLACEHOLDER_PREFIX}(\\d+)___`, 'g');
103990
+ // Matches an HTML element that starts at a line boundary and ends at a line boundary.
103991
+ // Allows optional leading indentation and lazily matches until the same closing tag.
103992
+ const BLOCK_HTML_RE = /(?<=^|\n)[ \t]*<([a-z][a-zA-Z0-9]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>[ \t]*(?=\n|$)/g;
103993
+ /**
103994
+ * Hides line-anchored HTML elements from the brace-escaping pass so we don't leak `\{`
103995
+ * into rendered output (rehypeRaw renders the `\` literally, e.g. `<div>{foo</div>`).
103996
+ *
103997
+ * One carve-out: if an interior line at column 0 has bare text containing `{`, mdxish
103998
+ * parses that line as a paragraph and the mdxExpression step would throw without an
103999
+ * escape — so we leave that case to the brace balancer.
104000
+ */
104001
+ function protectHTMLElements(content) {
104002
+ const htmlElements = [];
104003
+ const protectedContent = content.replace(BLOCK_HTML_RE, match => {
104004
+ // Look at the lines between the open and close tags. If any of them starts
104005
+ // at column 0 with bare text (not whitespace, not another tag) and contains
104006
+ // `{`, mdxish will parse that line as a paragraph and the brace as an MDX
104007
+ // expression, which would throw an error. So we let the brace balancer escape it.
104008
+ // Otherwise, we need to extract the sequence to protect it from the brace escaping.
104009
+ const interior = match.split('\n').slice(1, -1);
104010
+ const hazard = interior.some(line => line.length > 0 && line[0] !== ' ' && line[0] !== '\t' && line[0] !== '<' && line.includes('{'));
104011
+ if (hazard)
104012
+ return match;
104013
+ htmlElements.push(match);
104014
+ return `${HTML_ELEM_PLACEHOLDER_PREFIX}${htmlElements.length - 1}___`;
104015
+ });
104016
+ return { htmlElements, protectedContent };
104017
+ }
104018
+ function restoreHTMLElements(content, htmlElements) {
104019
+ if (htmlElements.length === 0)
104020
+ return content;
104021
+ return content.replace(HTML_ELEM_PLACEHOLDER, (_m, idx) => htmlElements[parseInt(idx, 10)]);
104022
+ }
104023
+ /**
104024
+ * Escapes unbalanced and paragraph-spanning braces so MDX doesn't trip on them.
104025
+ */
104026
+ function escapeProblematicBraces(content) {
104027
+ const { htmlElements, protectedContent } = protectHTMLElements(content);
104028
+ let strDelim = null;
104029
+ let strEscaped = false;
104030
+ // Track position of last newline (outside strings) to detect blank lines
104031
+ // -2 means no recent newline
104032
+ let lastNewlinePos = -2;
104033
+ // Character state machine trackers
104034
+ const toEscape = new Set();
104035
+ // Convert to array of Unicode code points so that emojis and multi-byte characters are correctly tracked
104036
+ const chars = Array.from(protectedContent);
104037
+ const openStack = [];
104038
+ for (let i = 0; i < chars.length; i += 1) {
104039
+ const ch = chars[i];
104040
+ // Track string delimiters inside expressions to ignore braces within them
104041
+ if (openStack.length > 0) {
104042
+ if (strDelim) {
104043
+ if (strEscaped)
104044
+ strEscaped = false;
104045
+ else if (ch === '\\')
104046
+ strEscaped = true;
104047
+ else if (ch === strDelim)
104048
+ strDelim = null;
104049
+ // eslint-disable-next-line no-continue
104050
+ continue;
104051
+ }
104052
+ if (ch === '"' || ch === "'" || ch === '`') {
104053
+ strDelim = ch;
104054
+ // eslint-disable-next-line no-continue
104055
+ continue;
104056
+ }
104057
+ if (ch === '\n') {
104058
+ if (lastNewlinePos >= 0) {
104059
+ const between = chars.slice(lastNewlinePos + 1, i).join('');
104060
+ if (/^[ \t]*$/.test(between)) {
104061
+ openStack.forEach(entry => { entry.hasBlankLine = true; });
104062
+ }
104063
+ }
104064
+ lastNewlinePos = i;
104065
+ }
104066
+ }
104067
+ // Skip already-escaped braces (odd run of preceding backslashes).
104068
+ if (ch === '{' || ch === '}') {
104069
+ let bs = 0;
104070
+ for (let j = i - 1; j >= 0 && chars[j] === '\\'; j -= 1)
104071
+ bs += 1;
104072
+ if (bs % 2 === 1) {
104073
+ // eslint-disable-next-line no-continue
104074
+ continue;
104075
+ }
104076
+ }
104077
+ if (ch === '{') {
104078
+ // `=` (after whitespace) before `{` ⇒ JSX attribute expression. The
104079
+ // mdxComponent tokenizer captures the whole component, so blank lines
104080
+ // inside attribute values are harmless. Nested `{` inherits the flag.
104081
+ let isAttrExpr = false;
104082
+ for (let j = i - 1; j >= 0; j -= 1) {
104083
+ const pc = chars[j];
104084
+ if (pc === '=') {
104085
+ isAttrExpr = true;
104086
+ break;
104087
+ }
104088
+ if (pc !== ' ' && pc !== '\t')
104089
+ break;
104090
+ }
104091
+ // Nested `{ ... }` inside an attribute value (e.g. `data={[{ ... }]}` or
104092
+ // `data={{ a: { b: 1 } }}`) must inherit the same exemption; only the
104093
+ // outer `{` is directly after `=`.
104094
+ if (!isAttrExpr && openStack.length > 0 && openStack[openStack.length - 1].isAttrExpr) {
104095
+ isAttrExpr = true;
104096
+ }
104097
+ openStack.push({ pos: i, hasBlankLine: false, isAttrExpr });
104098
+ lastNewlinePos = -2;
104099
+ }
104100
+ else if (ch === '}') {
104101
+ if (openStack.length > 0) {
104102
+ const entry = openStack.pop();
104103
+ // Pure `{/* ... */}` comments are handled downstream by the jsxComment
104104
+ // tokenizer — escaping their braces would prevent it from running.
104105
+ const isPureJsxComment = chars[entry.pos + 1] === '/' &&
104106
+ chars[entry.pos + 2] === '*' &&
104107
+ chars[i - 1] === '/' &&
104108
+ chars[i - 2] === '*';
104109
+ if (entry.hasBlankLine && !isPureJsxComment && !entry.isAttrExpr) {
104110
+ toEscape.add(entry.pos);
104111
+ toEscape.add(i);
104112
+ }
104113
+ }
104114
+ else {
104115
+ toEscape.add(i);
104116
+ }
104117
+ }
104118
+ }
104119
+ // Anything still open is unbalanced.
104120
+ openStack.forEach(entry => toEscape.add(entry.pos));
104121
+ // Reconstruct the content with the escaped braces.
104122
+ const escapedContent = toEscape.size === 0 ? protectedContent : chars.map((ch, i) => (toEscape.has(i) ? `\\${ch}` : ch)).join('');
104123
+ return restoreHTMLElements(escapedContent, htmlElements);
104124
+ }
104125
+ /**
104126
+ * Preprocesses JSX-like markdown content before parsing.
104127
+ *
104128
+ * JSX attribute expressions (`href={baseUrl}`) are no longer rewritten here —
104129
+ * they flow through the tokenizer as `mdxJsxAttributeValueExpression` nodes
104130
+ * and are evaluated at the hast handler step.
104131
+ *
104132
+ * @param content
104133
+ * @returns Preprocessed content ready for markdown parsing
104134
+ */
104135
+ function preprocessJSXExpressions(content) {
104136
+ const { protectedCode, protectedContent } = protectCodeBlocks(content);
104137
+ let processed = escapeProblematicBraces(protectedContent);
104138
+ processed = restoreCodeBlocks(processed, protectedCode);
104139
+ return processed;
104140
+ }
104141
+
104296
104142
  ;// ./processor/transform/mdxish/restore-snake-case-component-name.ts
104297
104143
 
104298
104144
 
@@ -105409,7 +105255,7 @@ function mdxishAstProcessor(mdContent, opts = {}) {
105409
105255
  .use(inline_html, { safeMode })
105410
105256
  .use(restore_snake_case_component_name, { mapping: snakeCaseMapping })
105411
105257
  .use(mdxish_tables)
105412
- .use(mdxish_html_blocks)
105258
+ .use(mdxish_html_blocks) // Convert every <HTMLBlock> shape → html-block
105413
105259
  // The next few transformers must appear after mdxishMdxComponentBlocks
105414
105260
  // so nodes produced by the inline re-parse of component bodies
105415
105261
  // (e.g. code/image/embed inside <Tabs>) get visited too