@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 +102 -82
- package/dist/main.node.js +102 -82
- package/dist/main.node.js.map +1 -1
- package/package.json +1 -1
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
|
|
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)
|
|
99503
|
-
|
|
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)
|
|
99513
|
+
if (isBlockNode(child)) {
|
|
99514
|
+
blockNodes.push(child);
|
|
99515
|
+
}
|
|
99508
99516
|
});
|
|
99509
|
-
|
|
99510
|
-
|
|
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
|
|
99523
|
-
//
|
|
99524
|
-
|
|
99525
|
-
|
|
99526
|
-
const
|
|
99527
|
-
const
|
|
99528
|
-
|
|
99529
|
-
|
|
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
|
-
|
|
99534
|
-
|
|
99535
|
-
|
|
99536
|
-
|
|
99537
|
-
|
|
99538
|
-
|
|
99539
|
-
|
|
99540
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
99618
|
-
*
|
|
99619
|
-
*
|
|
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
|
|
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
|
|
99626
|
-
|
|
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
|
|
99657
|
+
return `${HTML_ELEM_PLACEHOLDER_PREFIX}${htmlElements.length - 1}___`;
|
|
99629
99658
|
});
|
|
99630
|
-
|
|
99631
|
-
|
|
99632
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
//
|
|
99685
|
-
//
|
|
99686
|
-
//
|
|
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
|
-
|
|
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;
|
|
99741
|
+
lastNewlinePos = -2;
|
|
99709
99742
|
}
|
|
99710
99743
|
else if (ch === '}') {
|
|
99711
99744
|
if (openStack.length > 0) {
|
|
99712
99745
|
const entry = openStack.pop();
|
|
99713
|
-
//
|
|
99714
|
-
//
|
|
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
|
-
//
|
|
99762
|
+
// Anything still open is unbalanced.
|
|
99738
99763
|
openStack.forEach(entry => toEscape.add(entry.pos));
|
|
99739
|
-
//
|
|
99740
|
-
|
|
99741
|
-
|
|
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
|
|
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)
|
|
119697
|
-
|
|
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)
|
|
119707
|
+
if (isBlockNode(child)) {
|
|
119708
|
+
blockNodes.push(child);
|
|
119709
|
+
}
|
|
119702
119710
|
});
|
|
119703
|
-
|
|
119704
|
-
|
|
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
|
|
119717
|
-
//
|
|
119718
|
-
|
|
119719
|
-
|
|
119720
|
-
const
|
|
119721
|
-
const
|
|
119722
|
-
|
|
119723
|
-
|
|
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
|
-
|
|
119728
|
-
|
|
119729
|
-
|
|
119730
|
-
|
|
119731
|
-
|
|
119732
|
-
|
|
119733
|
-
|
|
119734
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
119812
|
-
*
|
|
119813
|
-
*
|
|
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
|
|
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
|
|
119820
|
-
|
|
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
|
|
119851
|
+
return `${HTML_ELEM_PLACEHOLDER_PREFIX}${htmlElements.length - 1}___`;
|
|
119823
119852
|
});
|
|
119824
|
-
|
|
119825
|
-
|
|
119826
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
//
|
|
119879
|
-
//
|
|
119880
|
-
//
|
|
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
|
-
|
|
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;
|
|
119935
|
+
lastNewlinePos = -2;
|
|
119903
119936
|
}
|
|
119904
119937
|
else if (ch === '}') {
|
|
119905
119938
|
if (openStack.length > 0) {
|
|
119906
119939
|
const entry = openStack.pop();
|
|
119907
|
-
//
|
|
119908
|
-
//
|
|
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
|
-
//
|
|
119956
|
+
// Anything still open is unbalanced.
|
|
119932
119957
|
openStack.forEach(entry => toEscape.add(entry.pos));
|
|
119933
|
-
//
|
|
119934
|
-
|
|
119935
|
-
|
|
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.
|