@readme/markdown 14.1.2 → 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
@@ -99481,11 +99481,17 @@ const blockTypes = [
99481
99481
  * Check if a node is a block-level node (cannot be inside a paragraph)
99482
99482
  */
99483
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());
99484
99490
  /**
99485
99491
  * Unified plugin that transforms magicBlock nodes into final MDAST nodes.
99486
99492
  */
99487
99493
  const magicBlockTransformer = (options = {}) => tree => {
99488
- const replacements = [];
99494
+ const lifts = [];
99489
99495
  visitParents(tree, 'magicBlock', (node, ancestors) => {
99490
99496
  const parent = ancestors[ancestors.length - 1]; // direct parent of the current node
99491
99497
  const index = parent.children.indexOf(node);
@@ -99499,51 +99505,60 @@ const magicBlockTransformer = (options = {}) => tree => {
99499
99505
  parent.children.splice(index, 1);
99500
99506
  return;
99501
99507
  }
99502
- // If parent is a paragraph and we're inserting block nodes (which must not be in paragraphs), lift them out
99503
- 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)) {
99504
99511
  const blockNodes = [];
99505
- const inlineNodes = [];
99506
99512
  children.forEach(child => {
99507
- (isBlockNode(child) ? blockNodes : inlineNodes).push(child);
99513
+ if (isBlockNode(child)) {
99514
+ blockNodes.push(child);
99515
+ }
99508
99516
  });
99509
- replacements.push({
99510
- 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,
99511
99521
  parent,
99512
- blockNodes,
99513
- inlineNodes,
99514
- before: parent.children.slice(0, index),
99515
- after: parent.children.slice(index + 1),
99516
99522
  });
99517
99523
  }
99518
99524
  else {
99519
99525
  parent.children.splice(index, 1, ...children);
99520
99526
  }
99521
99527
  });
99522
- // Second pass: apply replacements that require lifting block nodes out of paragraphs
99523
- // Process in reverse order to maintain correct indices
99524
- for (let i = replacements.length - 1; i >= 0; i -= 1) {
99525
- const { after, before, blockNodes, container, inlineNodes, parent } = replacements[i];
99526
- const containerChildren = container.children;
99527
- const paraIndex = containerChildren.indexOf(parent);
99528
- if (paraIndex === -1) {
99529
- 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) {
99530
99536
  // eslint-disable-next-line no-continue
99531
99537
  continue;
99532
99538
  }
99533
- if (inlineNodes.length > 0) {
99534
- parent.children = [...before, ...inlineNodes, ...after];
99535
- if (blockNodes.length > 0) {
99536
- containerChildren.splice(paraIndex + 1, 0, ...blockNodes);
99537
- }
99538
- }
99539
- else if (before.length === 0 && after.length === 0) {
99540
- 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);
99541
99559
  }
99542
99560
  else {
99543
- parent.children = [...before, ...after];
99544
- if (blockNodes.length > 0) {
99545
- containerChildren.splice(paraIndex + 1, 0, ...blockNodes);
99546
- }
99561
+ grandparent.children.splice(parentPosition + 1, 0, ...splitOffContentFromParent);
99547
99562
  }
99548
99563
  }
99549
99564
  };
@@ -99613,29 +99628,56 @@ function protectHTMLBlockContent(content) {
99613
99628
  function removeJSXComments(content) {
99614
99629
  return content.replace(JSX_COMMENT_REGEX, '');
99615
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;
99616
99636
  /**
99617
- * Escapes problematic braces in content to prevent MDX expression parsing errors.
99618
- * Handles unbalanced braces and paragraph-spanning expressions. Skips HTML elements
99619
- * 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.
99620
99643
  */
99621
- function escapeProblematicBraces(content) {
99622
- // Skip HTML elements — their content should never be escaped because
99623
- // rehypeRaw parses them into hast elements, making `\` literal text in output
99644
+ function protectHTMLElements(content) {
99624
99645
  const htmlElements = [];
99625
- const safe = content.replace(/<([a-z][a-zA-Z0-9]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>/g, match => {
99626
- 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;
99627
99656
  htmlElements.push(match);
99628
- return `___HTML_ELEM_${idx}___`;
99657
+ return `${HTML_ELEM_PLACEHOLDER_PREFIX}${htmlElements.length - 1}___`;
99629
99658
  });
99630
- const toEscape = new Set();
99631
- // Convert to array of Unicode code points to handle emojis and multi-byte characters correctly
99632
- 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);
99633
99671
  let strDelim = null;
99634
99672
  let strEscaped = false;
99635
- // Stack of open braces with their state
99636
- const openStack = [];
99637
99673
  // Track position of last newline (outside strings) to detect blank lines
99638
- 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 = [];
99639
99681
  for (let i = 0; i < chars.length; i += 1) {
99640
99682
  const ch = chars[i];
99641
99683
  // Track string delimiters inside expressions to ignore braces within them
@@ -99655,22 +99697,17 @@ function escapeProblematicBraces(content) {
99655
99697
  // eslint-disable-next-line no-continue
99656
99698
  continue;
99657
99699
  }
99658
- // Track newlines to detect blank lines (paragraph boundaries)
99659
99700
  if (ch === '\n') {
99660
- // Check if this newline creates a blank line (only whitespace since last newline)
99661
99701
  if (lastNewlinePos >= 0) {
99662
99702
  const between = chars.slice(lastNewlinePos + 1, i).join('');
99663
99703
  if (/^[ \t]*$/.test(between)) {
99664
- // This is a blank line - mark all open expressions as paragraph-spanning
99665
- openStack.forEach(entry => {
99666
- entry.hasBlankLine = true;
99667
- });
99704
+ openStack.forEach(entry => { entry.hasBlankLine = true; });
99668
99705
  }
99669
99706
  }
99670
99707
  lastNewlinePos = i;
99671
99708
  }
99672
99709
  }
99673
- // Skip already-escaped braces (count preceding backslashes)
99710
+ // Skip already-escaped braces (odd run of preceding backslashes).
99674
99711
  if (ch === '{' || ch === '}') {
99675
99712
  let bs = 0;
99676
99713
  for (let j = i - 1; j >= 0 && chars[j] === '\\'; j -= 1)
@@ -99681,10 +99718,9 @@ function escapeProblematicBraces(content) {
99681
99718
  }
99682
99719
  }
99683
99720
  if (ch === '{') {
99684
- // If preceded by `=` (ignoring whitespace), this is a JSX attribute
99685
- // expression (e.g. `data={[...]}`). The mdxComponent tokenizer captures
99686
- // the entire component block, so blank lines inside attribute values
99687
- // 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.
99688
99724
  let isAttrExpr = false;
99689
99725
  for (let j = i - 1; j >= 0; j -= 1) {
99690
99726
  const pc = chars[j];
@@ -99698,27 +99734,17 @@ function escapeProblematicBraces(content) {
99698
99734
  // Nested `{ ... }` inside an attribute value (e.g. `data={[{ ... }]}` or
99699
99735
  // `data={{ a: { b: 1 } }}`) must inherit the same exemption; only the
99700
99736
  // outer `{` is directly after `=`.
99701
- if (!isAttrExpr && openStack.length > 0) {
99702
- const parent = openStack[openStack.length - 1];
99703
- if (parent.isAttrExpr) {
99704
- isAttrExpr = true;
99705
- }
99737
+ if (!isAttrExpr && openStack.length > 0 && openStack[openStack.length - 1].isAttrExpr) {
99738
+ isAttrExpr = true;
99706
99739
  }
99707
99740
  openStack.push({ pos: i, hasBlankLine: false, isAttrExpr });
99708
- lastNewlinePos = -2; // Reset newline tracking for new expression
99741
+ lastNewlinePos = -2;
99709
99742
  }
99710
99743
  else if (ch === '}') {
99711
99744
  if (openStack.length > 0) {
99712
99745
  const entry = openStack.pop();
99713
- // Don't escape pure JSX comments, the `jsxComment` tokenizer downstream
99714
- // already knows how to swallow a whole `{/* ... */}` block in one go,
99715
- // even if the body has blank lines in it. If we escape the braces here
99716
- // the tokenizer never gets a shot at it.
99717
- //
99718
- // "Pure" means the braces open with `{/*` and close with `*/}` right
99719
- // next to each other. Something like `{/* c */ expr\n\nmore}` is just
99720
- // a regular expression that happens to start with a comment, so it
99721
- // 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.
99722
99748
  const isPureJsxComment = chars[entry.pos + 1] === '/' &&
99723
99749
  chars[entry.pos + 2] === '*' &&
99724
99750
  chars[i - 1] === '/' &&
@@ -99729,21 +99755,15 @@ function escapeProblematicBraces(content) {
99729
99755
  }
99730
99756
  }
99731
99757
  else {
99732
- // Unbalanced closing brace (no matching open)
99733
99758
  toEscape.add(i);
99734
99759
  }
99735
99760
  }
99736
99761
  }
99737
- // Any remaining open braces are unbalanced
99762
+ // Anything still open is unbalanced.
99738
99763
  openStack.forEach(entry => toEscape.add(entry.pos));
99739
- // If there are no problematic braces, return safe content as-is;
99740
- // otherwise, escape each problematic `{` or `}` so MDX doesn't treat them as expressions.
99741
- let result = toEscape.size === 0 ? safe : chars.map((ch, i) => (toEscape.has(i) ? `\\${ch}` : ch)).join('');
99742
- // Restore HTML elements
99743
- if (htmlElements.length > 0) {
99744
- result = result.replace(/___HTML_ELEM_(\d+)___/g, (_m, idx) => htmlElements[parseInt(idx, 10)]);
99745
- }
99746
- 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);
99747
99767
  }
99748
99768
  /**
99749
99769
  * Preprocesses JSX-like markdown content before parsing.
package/dist/main.node.js CHANGED
@@ -119675,11 +119675,17 @@ const blockTypes = [
119675
119675
  * Check if a node is a block-level node (cannot be inside a paragraph)
119676
119676
  */
119677
119677
  const isBlockNode = (node) => blockTypes.includes(node.type);
119678
+ const isParagraph = (node) => node.type === 'paragraph';
119679
+ /**
119680
+ * True for phrasing content that contributes only whitespace at render time
119681
+ * (a soft `break` node or a text node with no non-whitespace characters).
119682
+ */
119683
+ const isWhitespacePhrasing = (node) => node.type === 'break' || (node.type === 'text' && !node.value.trim());
119678
119684
  /**
119679
119685
  * Unified plugin that transforms magicBlock nodes into final MDAST nodes.
119680
119686
  */
119681
119687
  const magicBlockTransformer = (options = {}) => tree => {
119682
- const replacements = [];
119688
+ const lifts = [];
119683
119689
  visitParents(tree, 'magicBlock', (node, ancestors) => {
119684
119690
  const parent = ancestors[ancestors.length - 1]; // direct parent of the current node
119685
119691
  const index = parent.children.indexOf(node);
@@ -119693,51 +119699,60 @@ const magicBlockTransformer = (options = {}) => tree => {
119693
119699
  parent.children.splice(index, 1);
119694
119700
  return;
119695
119701
  }
119696
- // If parent is a paragraph and we're inserting block nodes (which must not be in paragraphs), lift them out
119697
- if (parent.type === 'paragraph' && children.some(child => isBlockNode(child))) {
119702
+ // If parent is a paragraph and we're inserting block nodes (which must not be in paragraphs)
119703
+ // it means we need to lift them out
119704
+ if (isParagraph(parent) && children.some(isBlockNode)) {
119698
119705
  const blockNodes = [];
119699
- const inlineNodes = [];
119700
119706
  children.forEach(child => {
119701
- (isBlockNode(child) ? blockNodes : inlineNodes).push(child);
119707
+ if (isBlockNode(child)) {
119708
+ blockNodes.push(child);
119709
+ }
119702
119710
  });
119703
- replacements.push({
119704
- container: ancestors[ancestors.length - 2] || tree, // grandparent of the current node
119711
+ lifts.push({
119712
+ childrenBlockNodes: blockNodes,
119713
+ grandparent: ancestors[ancestors.length - 2] || tree, // grandparent of the current node
119714
+ node,
119705
119715
  parent,
119706
- blockNodes,
119707
- inlineNodes,
119708
- before: parent.children.slice(0, index),
119709
- after: parent.children.slice(index + 1),
119710
119716
  });
119711
119717
  }
119712
119718
  else {
119713
119719
  parent.children.splice(index, 1, ...children);
119714
119720
  }
119715
119721
  });
119716
- // Second pass: apply replacements that require lifting block nodes out of paragraphs
119717
- // Process in reverse order to maintain correct indices
119718
- for (let i = replacements.length - 1; i >= 0; i -= 1) {
119719
- const { after, before, blockNodes, container, inlineNodes, parent } = replacements[i];
119720
- const containerChildren = container.children;
119721
- const paraIndex = containerChildren.indexOf(parent);
119722
- if (paraIndex === -1) {
119723
- parent.children.splice(before.length, 1, ...blockNodes, ...inlineNodes);
119722
+ // Second pass: apply lifts that move block nodes, and the content after them, out of paragraphs.
119723
+ // Operate on live state (find each node's current index now) and process in reverse so
119724
+ // that container insertions don't disturb earlier paragraphs' positions.
119725
+ for (let i = lifts.length - 1; i >= 0; i -= 1) {
119726
+ const { childrenBlockNodes: blockNodes, grandparent, node, parent: parentParagraph } = lifts[i];
119727
+ const nodePosition = parentParagraph.children.indexOf(node);
119728
+ const parentPosition = grandparent.children.indexOf(parentParagraph);
119729
+ if (nodePosition === -1 || parentPosition === -1) {
119724
119730
  // eslint-disable-next-line no-continue
119725
119731
  continue;
119726
119732
  }
119727
- if (inlineNodes.length > 0) {
119728
- parent.children = [...before, ...inlineNodes, ...after];
119729
- if (blockNodes.length > 0) {
119730
- containerChildren.splice(paraIndex + 1, 0, ...blockNodes);
119731
- }
119732
- }
119733
- else if (before.length === 0 && after.length === 0) {
119734
- containerChildren.splice(paraIndex, 1, ...blockNodes);
119733
+ // Snapshot live siblings to reconstruct the parent paragraph around the lifted node.
119734
+ const parentSiblingsBefore = parentParagraph.children.slice(0, nodePosition);
119735
+ const parentSiblingsAfter = parentParagraph.children.slice(nodePosition + 1);
119736
+ // Remove split-edge whitespace so lifted blocks do not render with extra blank lines.
119737
+ while (parentSiblingsBefore.length > 0 && isWhitespacePhrasing(parentSiblingsBefore[parentSiblingsBefore.length - 1]))
119738
+ parentSiblingsBefore.pop();
119739
+ while (parentSiblingsAfter.length > 0 && isWhitespacePhrasing(parentSiblingsAfter[0]))
119740
+ parentSiblingsAfter.shift();
119741
+ const splitOffContentFromParent = [...blockNodes];
119742
+ if (parentSiblingsAfter.length > 0) {
119743
+ // Keep trailing inline content grouped under a paragraph sibling as it might be an inline node
119744
+ // Even if it contains a block node, it will be hoisted out during its turn in the loop
119745
+ const trailingParagraph = { type: 'paragraph', children: parentSiblingsAfter };
119746
+ splitOffContentFromParent.push(trailingParagraph);
119747
+ }
119748
+ parentParagraph.children = [...parentSiblingsBefore];
119749
+ // If the parent paragraph is empty, just replace it with the lifted block nodes
119750
+ const shouldReplaceParent = parentParagraph.children.length === 0;
119751
+ if (shouldReplaceParent) {
119752
+ grandparent.children.splice(parentPosition, 1, ...splitOffContentFromParent);
119735
119753
  }
119736
119754
  else {
119737
- parent.children = [...before, ...after];
119738
- if (blockNodes.length > 0) {
119739
- containerChildren.splice(paraIndex + 1, 0, ...blockNodes);
119740
- }
119755
+ grandparent.children.splice(parentPosition + 1, 0, ...splitOffContentFromParent);
119741
119756
  }
119742
119757
  }
119743
119758
  };
@@ -119807,29 +119822,56 @@ function protectHTMLBlockContent(content) {
119807
119822
  function removeJSXComments(content) {
119808
119823
  return content.replace(JSX_COMMENT_REGEX, '');
119809
119824
  }
119825
+ const HTML_ELEM_PLACEHOLDER_PREFIX = '___MDXISH_HTML_ELEM_';
119826
+ const HTML_ELEM_PLACEHOLDER = new RegExp(`${HTML_ELEM_PLACEHOLDER_PREFIX}(\\d+)___`, 'g');
119827
+ // Matches an HTML element that starts at a line boundary and ends at a line boundary.
119828
+ // Allows optional leading indentation and lazily matches until the same closing tag.
119829
+ const BLOCK_HTML_RE = /(?<=^|\n)[ \t]*<([a-z][a-zA-Z0-9]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>[ \t]*(?=\n|$)/g;
119810
119830
  /**
119811
- * Escapes problematic braces in content to prevent MDX expression parsing errors.
119812
- * Handles unbalanced braces and paragraph-spanning expressions. Skips HTML elements
119813
- * so backslashes don't leak into rendered output via rehypeRaw.
119831
+ * Hides line-anchored HTML elements from the brace-escaping pass so we don't leak `\{`
119832
+ * into rendered output (rehypeRaw renders the `\` literally, e.g. `<div>{foo</div>`).
119833
+ *
119834
+ * One carve-out: if an interior line at column 0 has bare text containing `{`, mdxish
119835
+ * parses that line as a paragraph and the mdxExpression step would throw without an
119836
+ * escape — so we leave that case to the brace balancer.
119814
119837
  */
119815
- function escapeProblematicBraces(content) {
119816
- // Skip HTML elements — their content should never be escaped because
119817
- // rehypeRaw parses them into hast elements, making `\` literal text in output
119838
+ function protectHTMLElements(content) {
119818
119839
  const htmlElements = [];
119819
- const safe = content.replace(/<([a-z][a-zA-Z0-9]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>/g, match => {
119820
- const idx = htmlElements.length;
119840
+ const protectedContent = content.replace(BLOCK_HTML_RE, match => {
119841
+ // Look at the lines between the open and close tags. If any of them starts
119842
+ // at column 0 with bare text (not whitespace, not another tag) and contains
119843
+ // `{`, mdxish will parse that line as a paragraph and the brace as an MDX
119844
+ // expression, which would throw an error. So we let the brace balancer escape it.
119845
+ // Otherwise, we need to extract the sequence to protect it from the brace escaping.
119846
+ const interior = match.split('\n').slice(1, -1);
119847
+ const hazard = interior.some(line => line.length > 0 && line[0] !== ' ' && line[0] !== '\t' && line[0] !== '<' && line.includes('{'));
119848
+ if (hazard)
119849
+ return match;
119821
119850
  htmlElements.push(match);
119822
- return `___HTML_ELEM_${idx}___`;
119851
+ return `${HTML_ELEM_PLACEHOLDER_PREFIX}${htmlElements.length - 1}___`;
119823
119852
  });
119824
- const toEscape = new Set();
119825
- // Convert to array of Unicode code points to handle emojis and multi-byte characters correctly
119826
- const chars = Array.from(safe);
119853
+ return { htmlElements, protectedContent };
119854
+ }
119855
+ function restoreHTMLElements(content, htmlElements) {
119856
+ if (htmlElements.length === 0)
119857
+ return content;
119858
+ return content.replace(HTML_ELEM_PLACEHOLDER, (_m, idx) => htmlElements[parseInt(idx, 10)]);
119859
+ }
119860
+ /**
119861
+ * Escapes unbalanced and paragraph-spanning braces so MDX doesn't trip on them.
119862
+ */
119863
+ function escapeProblematicBraces(content) {
119864
+ const { htmlElements, protectedContent } = protectHTMLElements(content);
119827
119865
  let strDelim = null;
119828
119866
  let strEscaped = false;
119829
- // Stack of open braces with their state
119830
- const openStack = [];
119831
119867
  // Track position of last newline (outside strings) to detect blank lines
119832
- let lastNewlinePos = -2; // -2 means no recent newline
119868
+ // -2 means no recent newline
119869
+ let lastNewlinePos = -2;
119870
+ // Character state machine trackers
119871
+ const toEscape = new Set();
119872
+ // Convert to array of Unicode code points so that emojis and multi-byte characters are correctly tracked
119873
+ const chars = Array.from(protectedContent);
119874
+ const openStack = [];
119833
119875
  for (let i = 0; i < chars.length; i += 1) {
119834
119876
  const ch = chars[i];
119835
119877
  // Track string delimiters inside expressions to ignore braces within them
@@ -119849,22 +119891,17 @@ function escapeProblematicBraces(content) {
119849
119891
  // eslint-disable-next-line no-continue
119850
119892
  continue;
119851
119893
  }
119852
- // Track newlines to detect blank lines (paragraph boundaries)
119853
119894
  if (ch === '\n') {
119854
- // Check if this newline creates a blank line (only whitespace since last newline)
119855
119895
  if (lastNewlinePos >= 0) {
119856
119896
  const between = chars.slice(lastNewlinePos + 1, i).join('');
119857
119897
  if (/^[ \t]*$/.test(between)) {
119858
- // This is a blank line - mark all open expressions as paragraph-spanning
119859
- openStack.forEach(entry => {
119860
- entry.hasBlankLine = true;
119861
- });
119898
+ openStack.forEach(entry => { entry.hasBlankLine = true; });
119862
119899
  }
119863
119900
  }
119864
119901
  lastNewlinePos = i;
119865
119902
  }
119866
119903
  }
119867
- // Skip already-escaped braces (count preceding backslashes)
119904
+ // Skip already-escaped braces (odd run of preceding backslashes).
119868
119905
  if (ch === '{' || ch === '}') {
119869
119906
  let bs = 0;
119870
119907
  for (let j = i - 1; j >= 0 && chars[j] === '\\'; j -= 1)
@@ -119875,10 +119912,9 @@ function escapeProblematicBraces(content) {
119875
119912
  }
119876
119913
  }
119877
119914
  if (ch === '{') {
119878
- // If preceded by `=` (ignoring whitespace), this is a JSX attribute
119879
- // expression (e.g. `data={[...]}`). The mdxComponent tokenizer captures
119880
- // the entire component block, so blank lines inside attribute values
119881
- // won't split paragraphs — skip the blank-line check for these.
119915
+ // `=` (after whitespace) before `{` JSX attribute expression. The
119916
+ // mdxComponent tokenizer captures the whole component, so blank lines
119917
+ // inside attribute values are harmless. Nested `{` inherits the flag.
119882
119918
  let isAttrExpr = false;
119883
119919
  for (let j = i - 1; j >= 0; j -= 1) {
119884
119920
  const pc = chars[j];
@@ -119892,27 +119928,17 @@ function escapeProblematicBraces(content) {
119892
119928
  // Nested `{ ... }` inside an attribute value (e.g. `data={[{ ... }]}` or
119893
119929
  // `data={{ a: { b: 1 } }}`) must inherit the same exemption; only the
119894
119930
  // outer `{` is directly after `=`.
119895
- if (!isAttrExpr && openStack.length > 0) {
119896
- const parent = openStack[openStack.length - 1];
119897
- if (parent.isAttrExpr) {
119898
- isAttrExpr = true;
119899
- }
119931
+ if (!isAttrExpr && openStack.length > 0 && openStack[openStack.length - 1].isAttrExpr) {
119932
+ isAttrExpr = true;
119900
119933
  }
119901
119934
  openStack.push({ pos: i, hasBlankLine: false, isAttrExpr });
119902
- lastNewlinePos = -2; // Reset newline tracking for new expression
119935
+ lastNewlinePos = -2;
119903
119936
  }
119904
119937
  else if (ch === '}') {
119905
119938
  if (openStack.length > 0) {
119906
119939
  const entry = openStack.pop();
119907
- // Don't escape pure JSX comments, the `jsxComment` tokenizer downstream
119908
- // already knows how to swallow a whole `{/* ... */}` block in one go,
119909
- // even if the body has blank lines in it. If we escape the braces here
119910
- // the tokenizer never gets a shot at it.
119911
- //
119912
- // "Pure" means the braces open with `{/*` and close with `*/}` right
119913
- // next to each other. Something like `{/* c */ expr\n\nmore}` is just
119914
- // a regular expression that happens to start with a comment, so it
119915
- // still needs the normal blank-line protection.
119940
+ // Pure `{/* ... */}` comments are handled downstream by the jsxComment
119941
+ // tokenizer escaping their braces would prevent it from running.
119916
119942
  const isPureJsxComment = chars[entry.pos + 1] === '/' &&
119917
119943
  chars[entry.pos + 2] === '*' &&
119918
119944
  chars[i - 1] === '/' &&
@@ -119923,21 +119949,15 @@ function escapeProblematicBraces(content) {
119923
119949
  }
119924
119950
  }
119925
119951
  else {
119926
- // Unbalanced closing brace (no matching open)
119927
119952
  toEscape.add(i);
119928
119953
  }
119929
119954
  }
119930
119955
  }
119931
- // Any remaining open braces are unbalanced
119956
+ // Anything still open is unbalanced.
119932
119957
  openStack.forEach(entry => toEscape.add(entry.pos));
119933
- // If there are no problematic braces, return safe content as-is;
119934
- // otherwise, escape each problematic `{` or `}` so MDX doesn't treat them as expressions.
119935
- let result = toEscape.size === 0 ? safe : chars.map((ch, i) => (toEscape.has(i) ? `\\${ch}` : ch)).join('');
119936
- // Restore HTML elements
119937
- if (htmlElements.length > 0) {
119938
- result = result.replace(/___HTML_ELEM_(\d+)___/g, (_m, idx) => htmlElements[parseInt(idx, 10)]);
119939
- }
119940
- return result;
119958
+ // Reconstruct the content with the escaped braces.
119959
+ const escapedContent = toEscape.size === 0 ? protectedContent : chars.map((ch, i) => (toEscape.has(i) ? `\\${ch}` : ch)).join('');
119960
+ return restoreHTMLElements(escapedContent, htmlElements);
119941
119961
  }
119942
119962
  /**
119943
119963
  * Preprocesses JSX-like markdown content before parsing.