@readme/markdown 14.1.1 → 14.1.3

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
@@ -87611,7 +87611,12 @@ const tocHastToMdx = (toc, components, variables) => {
87611
87611
 
87612
87612
 
87613
87613
  const sanitizeSchema = cjs_default()(defaultSchema, {
87614
- protocols: ['doc', 'ref', 'blog', 'changelog', 'page'],
87614
+ attributes: {
87615
+ a: ['target'],
87616
+ },
87617
+ protocols: {
87618
+ href: ['doc', 'ref', 'blog', 'changelog', 'page'],
87619
+ },
87615
87620
  });
87616
87621
  const compile_compile = (text, { components = {}, missingComponents, copyButtons, useTailwind, ...opts } = {}) => {
87617
87622
  // Destructure at runtime to avoid circular dependency issues
@@ -97202,17 +97207,46 @@ function mdxComponent() {
97202
97207
 
97203
97208
 
97204
97209
 
97210
+
97211
+
97212
+
97213
+
97214
+ const buildInlineMdProcessor = (safeMode) => {
97215
+ const micromarkExts = [mdxComponent(), syntax_gemoji(), legacyVariable(), magicBlock()];
97216
+ const fromMarkdownExts = [
97217
+ mdxComponentFromMarkdown(),
97218
+ gemojiFromMarkdown(),
97219
+ legacyVariableFromMarkdown(),
97220
+ emptyTaskListItemFromMarkdown(),
97221
+ magicBlockFromMarkdown(),
97222
+ ];
97223
+ // Since evaluating expressions can be dangerous, do so only when safeMode is off
97224
+ if (!safeMode) {
97225
+ const mdxExprExt = mdxExpression({ allowEmpty: true });
97226
+ micromarkExts.push({ text: mdxExprExt.text });
97227
+ fromMarkdownExts.push(mdxExpressionFromMarkdown());
97228
+ }
97229
+ return unified()
97230
+ .data('micromarkExtensions', micromarkExts)
97231
+ .data('fromMarkdownExtensions', fromMarkdownExts)
97232
+ .use(remarkParse)
97233
+ .use(remarkGfm);
97234
+ };
97235
+ const processorCache = new Map();
97205
97236
  /**
97206
- * Shared unified processor for re-parsing the body of an MDX-ish component.
97207
- * Used by both the block and inline-block transformers so a nested component
97208
- * (e.g. `<Anchor>text with <b>bold</b></Anchor>`) goes through the same
97209
- * tokenizer chain as the top-level parse.
97237
+ * Unified processor for re-parsing the body of an MDX component
97238
+ * Memoized based on the argument value so we don't pay the construction cost on every parse
97239
+ * Currently the argument is only safeMode, but we could add more arguments in the future,
97240
+ * in which case the key would need to be extend to include the new arguments.
97210
97241
  */
97211
- const inlineMdProcessor = unified()
97212
- .data('micromarkExtensions', [mdxComponent(), legacyVariable(), magicBlock()])
97213
- .data('fromMarkdownExtensions', [mdxComponentFromMarkdown(), legacyVariableFromMarkdown(), emptyTaskListItemFromMarkdown(), magicBlockFromMarkdown()])
97214
- .use(remarkParse)
97215
- .use(remarkGfm);
97242
+ const getInlineMdProcessor = ({ safeMode = false } = {}) => {
97243
+ let processor = processorCache.get(safeMode);
97244
+ if (!processor) {
97245
+ processor = buildInlineMdProcessor(safeMode);
97246
+ processorCache.set(safeMode, processor);
97247
+ }
97248
+ return processor;
97249
+ };
97216
97250
  /**
97217
97251
  * True when a tag name starts with an uppercase letter — ReadMe's marker for
97218
97252
  * a custom MDX component (vs a lowercase HTML tag).
@@ -97253,11 +97287,11 @@ const toMdxJsxTextElement = (name, attributes, children, position) => ({
97253
97287
  * rejects line endings), so the paragraph-flatten path covers every case we
97254
97288
  * actually produce; other shapes fall back to empty children.
97255
97289
  */
97256
- const parsePhrasingChildren = (value) => {
97290
+ const parsePhrasingChildren = (value, safeMode) => {
97257
97291
  const trimmed = value.trim();
97258
97292
  if (!trimmed)
97259
97293
  return [];
97260
- const [first] = inlineMdProcessor.parse(trimmed).children;
97294
+ const [first] = getInlineMdProcessor({ safeMode }).parse(trimmed).children;
97261
97295
  return first?.type === 'paragraph' ? first.children : [];
97262
97296
  };
97263
97297
  /**
@@ -97270,7 +97304,7 @@ const parsePhrasingChildren = (value) => {
97270
97304
  *
97271
97305
  * Returns the original node unchanged when it doesn't qualify.
97272
97306
  */
97273
- const promoteInlineHtml = (node, parseOpts) => {
97307
+ const promoteInlineHtml = (node, parseOpts, safeMode) => {
97274
97308
  const parsed = parseTag((node.value ?? '').trim(), parseOpts);
97275
97309
  if (!parsed)
97276
97310
  return node;
@@ -97295,7 +97329,7 @@ const promoteInlineHtml = (node, parseOpts) => {
97295
97329
  if (closeIdx < 0)
97296
97330
  return node;
97297
97331
  const inner = contentAfterTag.slice(0, closeIdx);
97298
- return toMdxJsxTextElement(tag, attributes, parsePhrasingChildren(inner), node.position);
97332
+ return toMdxJsxTextElement(tag, attributes, parsePhrasingChildren(inner, safeMode), node.position);
97299
97333
  };
97300
97334
  /**
97301
97335
  * Transform inline html nodes with expression attributes
@@ -97314,9 +97348,10 @@ const promoteInlineHtml = (node, parseOpts) => {
97314
97348
  * `<a href="x">` stays as an html node for rehype-raw.
97315
97349
  */
97316
97350
  const mdxishInlineMdxHtmlBlocks = (opts = {}) => tree => {
97317
- const parseOpts = { preserveExpressionsAsText: !!opts.safeMode };
97351
+ const safeMode = !!opts.safeMode;
97352
+ const parseOpts = { preserveExpressionsAsText: safeMode };
97318
97353
  visit(tree, 'paragraph', (paragraph) => {
97319
- paragraph.children = paragraph.children.map(child => child.type === 'html' ? promoteInlineHtml(child, parseOpts) : child);
97354
+ paragraph.children = paragraph.children.map(child => child.type === 'html' ? promoteInlineHtml(child, parseOpts, safeMode) : child);
97320
97355
  });
97321
97356
  };
97322
97357
  /* harmony default export */ const inline_html = (mdxishInlineMdxHtmlBlocks);
@@ -97399,15 +97434,15 @@ function safeDeindent(text) {
97399
97434
  * Dedents the content first to prevent indented component content
97400
97435
  * (from nested components) from being treated as code blocks.
97401
97436
  */
97402
- const parseMdChildren = (value) => {
97403
- const parsed = inlineMdProcessor.parse(safeDeindent(value).trim());
97437
+ const parseMdChildren = (value, safeMode) => {
97438
+ const parsed = getInlineMdProcessor({ safeMode }).parse(safeDeindent(value).trim());
97404
97439
  return parsed.children || [];
97405
97440
  };
97406
97441
  /**
97407
97442
  * Parse substring content of a node and update the parent's children to include the new nodes.
97408
97443
  */
97409
- const parseSibling = (stack, parent, index, sibling) => {
97410
- const siblingNodes = parseMdChildren(sibling);
97444
+ const parseSibling = (stack, parent, index, sibling, safeMode) => {
97445
+ const siblingNodes = parseMdChildren(sibling, safeMode);
97411
97446
  // The new sibling nodes might contain new components to be processed
97412
97447
  if (siblingNodes.length > 0) {
97413
97448
  parent.children.splice(index + 1, 0, ...siblingNodes);
@@ -97459,7 +97494,8 @@ const substituteNodeWithMdxNode = (parent, index, mdxNode) => {
97459
97494
  */
97460
97495
  const mdxishMdxComponentBlocks = (opts = {}) => tree => {
97461
97496
  const stack = [tree];
97462
- const parseOpts = { preserveExpressionsAsText: !!opts.safeMode };
97497
+ const safeMode = !!opts.safeMode;
97498
+ const parseOpts = { preserveExpressionsAsText: safeMode };
97463
97499
  const processChildNode = (parent, index) => {
97464
97500
  const node = parent.children[index];
97465
97501
  if (!node)
@@ -97505,7 +97541,7 @@ const mdxishMdxComponentBlocks = (opts = {}) => tree => {
97505
97541
  // Check and parse if there's relevant content after the current closing tag
97506
97542
  const remainingContent = contentAfterTag.trim();
97507
97543
  if (remainingContent) {
97508
- parseSibling(stack, parent, index, remainingContent);
97544
+ parseSibling(stack, parent, index, remainingContent, safeMode);
97509
97545
  }
97510
97546
  return;
97511
97547
  }
@@ -97518,7 +97554,7 @@ const mdxishMdxComponentBlocks = (opts = {}) => tree => {
97518
97554
  const componentInnerContent = contentAfterTag.substring(0, closingTagIndex);
97519
97555
  const contentAfterClose = contentAfterTag.substring(closingTagIndex + closingTagStr.length).trim();
97520
97556
  let parsedChildren = componentInnerContent.trim()
97521
- ? parseMdChildren(componentInnerContent)
97557
+ ? parseMdChildren(componentInnerContent, safeMode)
97522
97558
  : [];
97523
97559
  // Lowercase HTML tags are usually inline (e.g. <a>, <span>). Remark wraps
97524
97560
  // bare text in a paragraph; unwrap when there's exactly one paragraph so
@@ -97535,7 +97571,7 @@ const mdxishMdxComponentBlocks = (opts = {}) => tree => {
97535
97571
  substituteNodeWithMdxNode(parent, index, componentNode);
97536
97572
  // After the closing tag, there might be more content to be processed
97537
97573
  if (contentAfterClose) {
97538
- parseSibling(stack, parent, index, contentAfterClose);
97574
+ parseSibling(stack, parent, index, contentAfterClose, safeMode);
97539
97575
  }
97540
97576
  else if (componentNode.children.length > 0) {
97541
97577
  // The content inside the component block might contain new components to be processed
@@ -99445,11 +99481,17 @@ const blockTypes = [
99445
99481
  * Check if a node is a block-level node (cannot be inside a paragraph)
99446
99482
  */
99447
99483
  const isBlockNode = (node) => blockTypes.includes(node.type);
99484
+ const isParagraph = (node) => node.type === 'paragraph';
99485
+ /**
99486
+ * True for phrasing content that contributes only whitespace at render time
99487
+ * (a soft `break` node or a text node with no non-whitespace characters).
99488
+ */
99489
+ const isWhitespacePhrasing = (node) => node.type === 'break' || (node.type === 'text' && !node.value.trim());
99448
99490
  /**
99449
99491
  * Unified plugin that transforms magicBlock nodes into final MDAST nodes.
99450
99492
  */
99451
99493
  const magicBlockTransformer = (options = {}) => tree => {
99452
- const replacements = [];
99494
+ const lifts = [];
99453
99495
  visitParents(tree, 'magicBlock', (node, ancestors) => {
99454
99496
  const parent = ancestors[ancestors.length - 1]; // direct parent of the current node
99455
99497
  const index = parent.children.indexOf(node);
@@ -99463,51 +99505,60 @@ const magicBlockTransformer = (options = {}) => tree => {
99463
99505
  parent.children.splice(index, 1);
99464
99506
  return;
99465
99507
  }
99466
- // If parent is a paragraph and we're inserting block nodes (which must not be in paragraphs), lift them out
99467
- if (parent.type === 'paragraph' && children.some(child => isBlockNode(child))) {
99508
+ // If parent is a paragraph and we're inserting block nodes (which must not be in paragraphs)
99509
+ // it means we need to lift them out
99510
+ if (isParagraph(parent) && children.some(isBlockNode)) {
99468
99511
  const blockNodes = [];
99469
- const inlineNodes = [];
99470
99512
  children.forEach(child => {
99471
- (isBlockNode(child) ? blockNodes : inlineNodes).push(child);
99513
+ if (isBlockNode(child)) {
99514
+ blockNodes.push(child);
99515
+ }
99472
99516
  });
99473
- replacements.push({
99474
- container: ancestors[ancestors.length - 2] || tree, // grandparent of the current node
99517
+ lifts.push({
99518
+ childrenBlockNodes: blockNodes,
99519
+ grandparent: ancestors[ancestors.length - 2] || tree, // grandparent of the current node
99520
+ node,
99475
99521
  parent,
99476
- blockNodes,
99477
- inlineNodes,
99478
- before: parent.children.slice(0, index),
99479
- after: parent.children.slice(index + 1),
99480
99522
  });
99481
99523
  }
99482
99524
  else {
99483
99525
  parent.children.splice(index, 1, ...children);
99484
99526
  }
99485
99527
  });
99486
- // Second pass: apply replacements that require lifting block nodes out of paragraphs
99487
- // Process in reverse order to maintain correct indices
99488
- for (let i = replacements.length - 1; i >= 0; i -= 1) {
99489
- const { after, before, blockNodes, container, inlineNodes, parent } = replacements[i];
99490
- const containerChildren = container.children;
99491
- const paraIndex = containerChildren.indexOf(parent);
99492
- if (paraIndex === -1) {
99493
- parent.children.splice(before.length, 1, ...blockNodes, ...inlineNodes);
99528
+ // Second pass: apply lifts that move block nodes, and the content after them, out of paragraphs.
99529
+ // Operate on live state (find each node's current index now) and process in reverse so
99530
+ // that container insertions don't disturb earlier paragraphs' positions.
99531
+ for (let i = lifts.length - 1; i >= 0; i -= 1) {
99532
+ const { childrenBlockNodes: blockNodes, grandparent, node, parent: parentParagraph } = lifts[i];
99533
+ const nodePosition = parentParagraph.children.indexOf(node);
99534
+ const parentPosition = grandparent.children.indexOf(parentParagraph);
99535
+ if (nodePosition === -1 || parentPosition === -1) {
99494
99536
  // eslint-disable-next-line no-continue
99495
99537
  continue;
99496
99538
  }
99497
- if (inlineNodes.length > 0) {
99498
- parent.children = [...before, ...inlineNodes, ...after];
99499
- if (blockNodes.length > 0) {
99500
- containerChildren.splice(paraIndex + 1, 0, ...blockNodes);
99501
- }
99502
- }
99503
- else if (before.length === 0 && after.length === 0) {
99504
- containerChildren.splice(paraIndex, 1, ...blockNodes);
99539
+ // Snapshot live siblings to reconstruct the parent paragraph around the lifted node.
99540
+ const parentSiblingsBefore = parentParagraph.children.slice(0, nodePosition);
99541
+ const parentSiblingsAfter = parentParagraph.children.slice(nodePosition + 1);
99542
+ // Remove split-edge whitespace so lifted blocks do not render with extra blank lines.
99543
+ while (parentSiblingsBefore.length > 0 && isWhitespacePhrasing(parentSiblingsBefore[parentSiblingsBefore.length - 1]))
99544
+ parentSiblingsBefore.pop();
99545
+ while (parentSiblingsAfter.length > 0 && isWhitespacePhrasing(parentSiblingsAfter[0]))
99546
+ parentSiblingsAfter.shift();
99547
+ const splitOffContentFromParent = [...blockNodes];
99548
+ if (parentSiblingsAfter.length > 0) {
99549
+ // Keep trailing inline content grouped under a paragraph sibling as it might be an inline node
99550
+ // Even if it contains a block node, it will be hoisted out during its turn in the loop
99551
+ const trailingParagraph = { type: 'paragraph', children: parentSiblingsAfter };
99552
+ splitOffContentFromParent.push(trailingParagraph);
99553
+ }
99554
+ parentParagraph.children = [...parentSiblingsBefore];
99555
+ // If the parent paragraph is empty, just replace it with the lifted block nodes
99556
+ const shouldReplaceParent = parentParagraph.children.length === 0;
99557
+ if (shouldReplaceParent) {
99558
+ grandparent.children.splice(parentPosition, 1, ...splitOffContentFromParent);
99505
99559
  }
99506
99560
  else {
99507
- parent.children = [...before, ...after];
99508
- if (blockNodes.length > 0) {
99509
- containerChildren.splice(paraIndex + 1, 0, ...blockNodes);
99510
- }
99561
+ grandparent.children.splice(parentPosition + 1, 0, ...splitOffContentFromParent);
99511
99562
  }
99512
99563
  }
99513
99564
  };
@@ -99577,29 +99628,56 @@ function protectHTMLBlockContent(content) {
99577
99628
  function removeJSXComments(content) {
99578
99629
  return content.replace(JSX_COMMENT_REGEX, '');
99579
99630
  }
99631
+ const HTML_ELEM_PLACEHOLDER_PREFIX = '___MDXISH_HTML_ELEM_';
99632
+ const HTML_ELEM_PLACEHOLDER = new RegExp(`${HTML_ELEM_PLACEHOLDER_PREFIX}(\\d+)___`, 'g');
99633
+ // Matches an HTML element that starts at a line boundary and ends at a line boundary.
99634
+ // Allows optional leading indentation and lazily matches until the same closing tag.
99635
+ const BLOCK_HTML_RE = /(?<=^|\n)[ \t]*<([a-z][a-zA-Z0-9]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>[ \t]*(?=\n|$)/g;
99580
99636
  /**
99581
- * Escapes problematic braces in content to prevent MDX expression parsing errors.
99582
- * Handles unbalanced braces and paragraph-spanning expressions. Skips HTML elements
99583
- * so backslashes don't leak into rendered output via rehypeRaw.
99637
+ * Hides line-anchored HTML elements from the brace-escaping pass so we don't leak `\{`
99638
+ * into rendered output (rehypeRaw renders the `\` literally, e.g. `<div>{foo</div>`).
99639
+ *
99640
+ * One carve-out: if an interior line at column 0 has bare text containing `{`, mdxish
99641
+ * parses that line as a paragraph and the mdxExpression step would throw without an
99642
+ * escape — so we leave that case to the brace balancer.
99584
99643
  */
99585
- function escapeProblematicBraces(content) {
99586
- // Skip HTML elements — their content should never be escaped because
99587
- // rehypeRaw parses them into hast elements, making `\` literal text in output
99644
+ function protectHTMLElements(content) {
99588
99645
  const htmlElements = [];
99589
- const safe = content.replace(/<([a-z][a-zA-Z0-9]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>/g, match => {
99590
- const idx = htmlElements.length;
99646
+ const protectedContent = content.replace(BLOCK_HTML_RE, match => {
99647
+ // Look at the lines between the open and close tags. If any of them starts
99648
+ // at column 0 with bare text (not whitespace, not another tag) and contains
99649
+ // `{`, mdxish will parse that line as a paragraph and the brace as an MDX
99650
+ // expression, which would throw an error. So we let the brace balancer escape it.
99651
+ // Otherwise, we need to extract the sequence to protect it from the brace escaping.
99652
+ const interior = match.split('\n').slice(1, -1);
99653
+ const hazard = interior.some(line => line.length > 0 && line[0] !== ' ' && line[0] !== '\t' && line[0] !== '<' && line.includes('{'));
99654
+ if (hazard)
99655
+ return match;
99591
99656
  htmlElements.push(match);
99592
- return `___HTML_ELEM_${idx}___`;
99657
+ return `${HTML_ELEM_PLACEHOLDER_PREFIX}${htmlElements.length - 1}___`;
99593
99658
  });
99594
- const toEscape = new Set();
99595
- // Convert to array of Unicode code points to handle emojis and multi-byte characters correctly
99596
- const chars = Array.from(safe);
99659
+ return { htmlElements, protectedContent };
99660
+ }
99661
+ function restoreHTMLElements(content, htmlElements) {
99662
+ if (htmlElements.length === 0)
99663
+ return content;
99664
+ return content.replace(HTML_ELEM_PLACEHOLDER, (_m, idx) => htmlElements[parseInt(idx, 10)]);
99665
+ }
99666
+ /**
99667
+ * Escapes unbalanced and paragraph-spanning braces so MDX doesn't trip on them.
99668
+ */
99669
+ function escapeProblematicBraces(content) {
99670
+ const { htmlElements, protectedContent } = protectHTMLElements(content);
99597
99671
  let strDelim = null;
99598
99672
  let strEscaped = false;
99599
- // Stack of open braces with their state
99600
- const openStack = [];
99601
99673
  // Track position of last newline (outside strings) to detect blank lines
99602
- let lastNewlinePos = -2; // -2 means no recent newline
99674
+ // -2 means no recent newline
99675
+ let lastNewlinePos = -2;
99676
+ // Character state machine trackers
99677
+ const toEscape = new Set();
99678
+ // Convert to array of Unicode code points so that emojis and multi-byte characters are correctly tracked
99679
+ const chars = Array.from(protectedContent);
99680
+ const openStack = [];
99603
99681
  for (let i = 0; i < chars.length; i += 1) {
99604
99682
  const ch = chars[i];
99605
99683
  // Track string delimiters inside expressions to ignore braces within them
@@ -99619,22 +99697,17 @@ function escapeProblematicBraces(content) {
99619
99697
  // eslint-disable-next-line no-continue
99620
99698
  continue;
99621
99699
  }
99622
- // Track newlines to detect blank lines (paragraph boundaries)
99623
99700
  if (ch === '\n') {
99624
- // Check if this newline creates a blank line (only whitespace since last newline)
99625
99701
  if (lastNewlinePos >= 0) {
99626
99702
  const between = chars.slice(lastNewlinePos + 1, i).join('');
99627
99703
  if (/^[ \t]*$/.test(between)) {
99628
- // This is a blank line - mark all open expressions as paragraph-spanning
99629
- openStack.forEach(entry => {
99630
- entry.hasBlankLine = true;
99631
- });
99704
+ openStack.forEach(entry => { entry.hasBlankLine = true; });
99632
99705
  }
99633
99706
  }
99634
99707
  lastNewlinePos = i;
99635
99708
  }
99636
99709
  }
99637
- // Skip already-escaped braces (count preceding backslashes)
99710
+ // Skip already-escaped braces (odd run of preceding backslashes).
99638
99711
  if (ch === '{' || ch === '}') {
99639
99712
  let bs = 0;
99640
99713
  for (let j = i - 1; j >= 0 && chars[j] === '\\'; j -= 1)
@@ -99645,10 +99718,9 @@ function escapeProblematicBraces(content) {
99645
99718
  }
99646
99719
  }
99647
99720
  if (ch === '{') {
99648
- // If preceded by `=` (ignoring whitespace), this is a JSX attribute
99649
- // expression (e.g. `data={[...]}`). The mdxComponent tokenizer captures
99650
- // the entire component block, so blank lines inside attribute values
99651
- // won't split paragraphs — skip the blank-line check for these.
99721
+ // `=` (after whitespace) before `{` JSX attribute expression. The
99722
+ // mdxComponent tokenizer captures the whole component, so blank lines
99723
+ // inside attribute values are harmless. Nested `{` inherits the flag.
99652
99724
  let isAttrExpr = false;
99653
99725
  for (let j = i - 1; j >= 0; j -= 1) {
99654
99726
  const pc = chars[j];
@@ -99662,27 +99734,17 @@ function escapeProblematicBraces(content) {
99662
99734
  // Nested `{ ... }` inside an attribute value (e.g. `data={[{ ... }]}` or
99663
99735
  // `data={{ a: { b: 1 } }}`) must inherit the same exemption; only the
99664
99736
  // outer `{` is directly after `=`.
99665
- if (!isAttrExpr && openStack.length > 0) {
99666
- const parent = openStack[openStack.length - 1];
99667
- if (parent.isAttrExpr) {
99668
- isAttrExpr = true;
99669
- }
99737
+ if (!isAttrExpr && openStack.length > 0 && openStack[openStack.length - 1].isAttrExpr) {
99738
+ isAttrExpr = true;
99670
99739
  }
99671
99740
  openStack.push({ pos: i, hasBlankLine: false, isAttrExpr });
99672
- lastNewlinePos = -2; // Reset newline tracking for new expression
99741
+ lastNewlinePos = -2;
99673
99742
  }
99674
99743
  else if (ch === '}') {
99675
99744
  if (openStack.length > 0) {
99676
99745
  const entry = openStack.pop();
99677
- // Don't escape pure JSX comments, the `jsxComment` tokenizer downstream
99678
- // already knows how to swallow a whole `{/* ... */}` block in one go,
99679
- // even if the body has blank lines in it. If we escape the braces here
99680
- // the tokenizer never gets a shot at it.
99681
- //
99682
- // "Pure" means the braces open with `{/*` and close with `*/}` right
99683
- // next to each other. Something like `{/* c */ expr\n\nmore}` is just
99684
- // a regular expression that happens to start with a comment, so it
99685
- // still needs the normal blank-line protection.
99746
+ // Pure `{/* ... */}` comments are handled downstream by the jsxComment
99747
+ // tokenizer escaping their braces would prevent it from running.
99686
99748
  const isPureJsxComment = chars[entry.pos + 1] === '/' &&
99687
99749
  chars[entry.pos + 2] === '*' &&
99688
99750
  chars[i - 1] === '/' &&
@@ -99693,21 +99755,15 @@ function escapeProblematicBraces(content) {
99693
99755
  }
99694
99756
  }
99695
99757
  else {
99696
- // Unbalanced closing brace (no matching open)
99697
99758
  toEscape.add(i);
99698
99759
  }
99699
99760
  }
99700
99761
  }
99701
- // Any remaining open braces are unbalanced
99762
+ // Anything still open is unbalanced.
99702
99763
  openStack.forEach(entry => toEscape.add(entry.pos));
99703
- // If there are no problematic braces, return safe content as-is;
99704
- // otherwise, escape each problematic `{` or `}` so MDX doesn't treat them as expressions.
99705
- let result = toEscape.size === 0 ? safe : chars.map((ch, i) => (toEscape.has(i) ? `\\${ch}` : ch)).join('');
99706
- // Restore HTML elements
99707
- if (htmlElements.length > 0) {
99708
- result = result.replace(/___HTML_ELEM_(\d+)___/g, (_m, idx) => htmlElements[parseInt(idx, 10)]);
99709
- }
99710
- return result;
99764
+ // Reconstruct the content with the escaped braces.
99765
+ const escapedContent = toEscape.size === 0 ? protectedContent : chars.map((ch, i) => (toEscape.has(i) ? `\\${ch}` : ch)).join('');
99766
+ return restoreHTMLElements(escapedContent, htmlElements);
99711
99767
  }
99712
99768
  /**
99713
99769
  * Preprocesses JSX-like markdown content before parsing.
@@ -101853,15 +101909,18 @@ function mdxishAstProcessor(mdContent, opts = {}) {
101853
101909
  .use(remarkParse)
101854
101910
  .use(remarkFrontmatter)
101855
101911
  .use(normalize_malformed_md_syntax)
101856
- .use(magic_block_transformer)
101857
- .use(transform_images, { isMdxish: true })
101858
- .use(defaultTransformers)
101859
101912
  .use(self_closing_blocks)
101860
101913
  .use(mdx_blocks, { safeMode })
101861
101914
  .use(inline_html, { safeMode })
101862
101915
  .use(restore_snake_case_component_name, { mapping: snakeCaseMapping })
101863
101916
  .use(mdxish_tables)
101864
101917
  .use(mdxish_html_blocks)
101918
+ // The next few transformers must appear after mdxishMdxComponentBlocks
101919
+ // so nodes produced by the inline re-parse of component bodies
101920
+ // (e.g. code/image/embed inside <Tabs>) get visited too
101921
+ .use(magic_block_transformer)
101922
+ .use(transform_images, { isMdxish: true })
101923
+ .use(defaultTransformers)
101865
101924
  .use(newEditorTypes ? inline_mdx_blocks : undefined) // Merge inline html components (e.g. <Anchor>) into MDAST nodes
101866
101925
  .use(newEditorTypes ? mdxish_jsx_to_mdast : undefined) // Convert block JSX elements to MDAST types
101867
101926
  .use(variables_text) // Parse {user.*} patterns from text nodes