@readme/markdown 14.1.2 → 14.1.4

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
@@ -71907,7 +71907,7 @@ const hasFlowContent = (nodes) => {
71907
71907
  * a markdown table (phrasing-only) or keep as JSX <Table> (has flow content).
71908
71908
  */
71909
71909
  const processTableNode = (node, index, parent, documentPosition) => {
71910
- if (node.name !== 'Table')
71910
+ if (node.name !== 'Table' && node.name !== 'table')
71911
71911
  return;
71912
71912
  const position = documentPosition ?? node.position;
71913
71913
  const { align: alignAttr } = getAttrs(node);
@@ -72021,7 +72021,7 @@ const mdxishTables = () => tree => {
72021
72021
  const node = _node;
72022
72022
  if (typeof index !== 'number' || !parent || !('children' in parent))
72023
72023
  return;
72024
- if (!node.value.startsWith('<Table'))
72024
+ if (!node.value.startsWith('<Table') && !node.value.startsWith('<table'))
72025
72025
  return;
72026
72026
  try {
72027
72027
  const parsed = tableNodeProcessor.runSync(tableNodeProcessor.parse(node.value));
@@ -72043,13 +72043,10 @@ const mdxishTables = () => tree => {
72043
72043
  }
72044
72044
  });
72045
72045
  visit(parsed, isMDXElement, (tableNode) => {
72046
- if (tableNode.name === 'Table') {
72047
- processTableNode(tableNode, index, parent, node.position);
72048
- // Stop after the outermost Table so nested Tables don't overwrite parent.children[index]
72049
- // we let it get handled naturally
72050
- return EXIT;
72051
- }
72052
- return undefined;
72046
+ if (tableNode.name !== 'Table' && tableNode.name !== 'table')
72047
+ return undefined;
72048
+ processTableNode(tableNode, index, parent, node.position);
72049
+ return EXIT;
72053
72050
  });
72054
72051
  }
72055
72052
  catch {
@@ -98914,8 +98911,8 @@ const HTML_ELEMENT_BLOCK_RE = /<([a-zA-Z][a-zA-Z0-9-]*)[\s>][\s\S]*?<\/\1>/g;
98914
98911
  const NEWLINE_WITH_WHITESPACE_RE = /[^\S\n]*\n[^\S\n]*/g;
98915
98912
  /** Matches a closing block-level tag followed by non-tag text or by a newline then non-blank content. */
98916
98913
  const CLOSE_BLOCK_TAG_BOUNDARY_RE = /<\/([a-zA-Z][a-zA-Z0-9-]*)>\s*(?:(?!<)(\S)|\n([^\n]))/g;
98917
- /** Tests whether a string contains a complete HTML element (open + close tag). */
98918
- const COMPLETE_HTML_ELEMENT_RE = /<[a-zA-Z][^>]*>[\s\S]*<\/[a-zA-Z]/;
98914
+ /** Strips HTML open/close tags. Used to detect non-tag inner text content. */
98915
+ const HTML_TAG_STRIP_RE = /<\/?[a-zA-Z][^>]*>/g;
98919
98916
 
98920
98917
  ;// ./processor/transform/mdxish/magic-blocks/placeholder.ts
98921
98918
  const EMPTY_IMAGE_PLACEHOLDER = {
@@ -99155,10 +99152,12 @@ const parseTableCell = (text) => {
99155
99152
  const trimmedLines = normalized.split('\n').map(line => line.trimStart());
99156
99153
  const processed = trimmedLines.join('\n');
99157
99154
  const tree = contentParser.runSync(contentParser.parse(processed));
99158
- // Process markdown inside complete HTML elements (e.g. _emphasis_ within <li>).
99159
- // Bare tags like "<i>" are left for rehypeRaw since rehype-parse would mangle them.
99155
+ // Process markdown inside HTML blocks that have non-tag inner text (e.g. `<div>**x**`
99156
+ // or `<ul><li>_x_</li></ul>`). Pure bare tags like "<i>" or "<br>" are left for rehypeRaw
99157
+ // since rehype-parse would mangle them (auto-closing void/inline elements).
99160
99158
  visit(tree, 'html', (node) => {
99161
- if (COMPLETE_HTML_ELEMENT_RE.test(node.value)) {
99159
+ const hasInnerText = node.value.replace(HTML_TAG_STRIP_RE, '').trim().length > 0;
99160
+ if (hasInnerText) {
99162
99161
  node.value = processMarkdownInHtmlString(node.value);
99163
99162
  }
99164
99163
  else {
@@ -99481,11 +99480,17 @@ const blockTypes = [
99481
99480
  * Check if a node is a block-level node (cannot be inside a paragraph)
99482
99481
  */
99483
99482
  const isBlockNode = (node) => blockTypes.includes(node.type);
99483
+ const isParagraph = (node) => node.type === 'paragraph';
99484
+ /**
99485
+ * True for phrasing content that contributes only whitespace at render time
99486
+ * (a soft `break` node or a text node with no non-whitespace characters).
99487
+ */
99488
+ const isWhitespacePhrasing = (node) => node.type === 'break' || (node.type === 'text' && !node.value.trim());
99484
99489
  /**
99485
99490
  * Unified plugin that transforms magicBlock nodes into final MDAST nodes.
99486
99491
  */
99487
99492
  const magicBlockTransformer = (options = {}) => tree => {
99488
- const replacements = [];
99493
+ const lifts = [];
99489
99494
  visitParents(tree, 'magicBlock', (node, ancestors) => {
99490
99495
  const parent = ancestors[ancestors.length - 1]; // direct parent of the current node
99491
99496
  const index = parent.children.indexOf(node);
@@ -99499,51 +99504,60 @@ const magicBlockTransformer = (options = {}) => tree => {
99499
99504
  parent.children.splice(index, 1);
99500
99505
  return;
99501
99506
  }
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))) {
99507
+ // If parent is a paragraph and we're inserting block nodes (which must not be in paragraphs)
99508
+ // it means we need to lift them out
99509
+ if (isParagraph(parent) && children.some(isBlockNode)) {
99504
99510
  const blockNodes = [];
99505
- const inlineNodes = [];
99506
99511
  children.forEach(child => {
99507
- (isBlockNode(child) ? blockNodes : inlineNodes).push(child);
99512
+ if (isBlockNode(child)) {
99513
+ blockNodes.push(child);
99514
+ }
99508
99515
  });
99509
- replacements.push({
99510
- container: ancestors[ancestors.length - 2] || tree, // grandparent of the current node
99516
+ lifts.push({
99517
+ childrenBlockNodes: blockNodes,
99518
+ grandparent: ancestors[ancestors.length - 2] || tree, // grandparent of the current node
99519
+ node,
99511
99520
  parent,
99512
- blockNodes,
99513
- inlineNodes,
99514
- before: parent.children.slice(0, index),
99515
- after: parent.children.slice(index + 1),
99516
99521
  });
99517
99522
  }
99518
99523
  else {
99519
99524
  parent.children.splice(index, 1, ...children);
99520
99525
  }
99521
99526
  });
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);
99527
+ // Second pass: apply lifts that move block nodes, and the content after them, out of paragraphs.
99528
+ // Operate on live state (find each node's current index now) and process in reverse so
99529
+ // that container insertions don't disturb earlier paragraphs' positions.
99530
+ for (let i = lifts.length - 1; i >= 0; i -= 1) {
99531
+ const { childrenBlockNodes: blockNodes, grandparent, node, parent: parentParagraph } = lifts[i];
99532
+ const nodePosition = parentParagraph.children.indexOf(node);
99533
+ const parentPosition = grandparent.children.indexOf(parentParagraph);
99534
+ if (nodePosition === -1 || parentPosition === -1) {
99530
99535
  // eslint-disable-next-line no-continue
99531
99536
  continue;
99532
99537
  }
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);
99538
+ // Snapshot live siblings to reconstruct the parent paragraph around the lifted node.
99539
+ const parentSiblingsBefore = parentParagraph.children.slice(0, nodePosition);
99540
+ const parentSiblingsAfter = parentParagraph.children.slice(nodePosition + 1);
99541
+ // Remove split-edge whitespace so lifted blocks do not render with extra blank lines.
99542
+ while (parentSiblingsBefore.length > 0 && isWhitespacePhrasing(parentSiblingsBefore[parentSiblingsBefore.length - 1]))
99543
+ parentSiblingsBefore.pop();
99544
+ while (parentSiblingsAfter.length > 0 && isWhitespacePhrasing(parentSiblingsAfter[0]))
99545
+ parentSiblingsAfter.shift();
99546
+ const splitOffContentFromParent = [...blockNodes];
99547
+ if (parentSiblingsAfter.length > 0) {
99548
+ // Keep trailing inline content grouped under a paragraph sibling as it might be an inline node
99549
+ // Even if it contains a block node, it will be hoisted out during its turn in the loop
99550
+ const trailingParagraph = { type: 'paragraph', children: parentSiblingsAfter };
99551
+ splitOffContentFromParent.push(trailingParagraph);
99552
+ }
99553
+ parentParagraph.children = [...parentSiblingsBefore];
99554
+ // If the parent paragraph is empty, just replace it with the lifted block nodes
99555
+ const shouldReplaceParent = parentParagraph.children.length === 0;
99556
+ if (shouldReplaceParent) {
99557
+ grandparent.children.splice(parentPosition, 1, ...splitOffContentFromParent);
99541
99558
  }
99542
99559
  else {
99543
- parent.children = [...before, ...after];
99544
- if (blockNodes.length > 0) {
99545
- containerChildren.splice(paraIndex + 1, 0, ...blockNodes);
99546
- }
99560
+ grandparent.children.splice(parentPosition + 1, 0, ...splitOffContentFromParent);
99547
99561
  }
99548
99562
  }
99549
99563
  };
@@ -99613,29 +99627,56 @@ function protectHTMLBlockContent(content) {
99613
99627
  function removeJSXComments(content) {
99614
99628
  return content.replace(JSX_COMMENT_REGEX, '');
99615
99629
  }
99630
+ const HTML_ELEM_PLACEHOLDER_PREFIX = '___MDXISH_HTML_ELEM_';
99631
+ const HTML_ELEM_PLACEHOLDER = new RegExp(`${HTML_ELEM_PLACEHOLDER_PREFIX}(\\d+)___`, 'g');
99632
+ // Matches an HTML element that starts at a line boundary and ends at a line boundary.
99633
+ // Allows optional leading indentation and lazily matches until the same closing tag.
99634
+ const BLOCK_HTML_RE = /(?<=^|\n)[ \t]*<([a-z][a-zA-Z0-9]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>[ \t]*(?=\n|$)/g;
99616
99635
  /**
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.
99636
+ * Hides line-anchored HTML elements from the brace-escaping pass so we don't leak `\{`
99637
+ * into rendered output (rehypeRaw renders the `\` literally, e.g. `<div>{foo</div>`).
99638
+ *
99639
+ * One carve-out: if an interior line at column 0 has bare text containing `{`, mdxish
99640
+ * parses that line as a paragraph and the mdxExpression step would throw without an
99641
+ * escape — so we leave that case to the brace balancer.
99620
99642
  */
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
99643
+ function protectHTMLElements(content) {
99624
99644
  const htmlElements = [];
99625
- const safe = content.replace(/<([a-z][a-zA-Z0-9]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>/g, match => {
99626
- const idx = htmlElements.length;
99645
+ const protectedContent = content.replace(BLOCK_HTML_RE, match => {
99646
+ // Look at the lines between the open and close tags. If any of them starts
99647
+ // at column 0 with bare text (not whitespace, not another tag) and contains
99648
+ // `{`, mdxish will parse that line as a paragraph and the brace as an MDX
99649
+ // expression, which would throw an error. So we let the brace balancer escape it.
99650
+ // Otherwise, we need to extract the sequence to protect it from the brace escaping.
99651
+ const interior = match.split('\n').slice(1, -1);
99652
+ const hazard = interior.some(line => line.length > 0 && line[0] !== ' ' && line[0] !== '\t' && line[0] !== '<' && line.includes('{'));
99653
+ if (hazard)
99654
+ return match;
99627
99655
  htmlElements.push(match);
99628
- return `___HTML_ELEM_${idx}___`;
99656
+ return `${HTML_ELEM_PLACEHOLDER_PREFIX}${htmlElements.length - 1}___`;
99629
99657
  });
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);
99658
+ return { htmlElements, protectedContent };
99659
+ }
99660
+ function restoreHTMLElements(content, htmlElements) {
99661
+ if (htmlElements.length === 0)
99662
+ return content;
99663
+ return content.replace(HTML_ELEM_PLACEHOLDER, (_m, idx) => htmlElements[parseInt(idx, 10)]);
99664
+ }
99665
+ /**
99666
+ * Escapes unbalanced and paragraph-spanning braces so MDX doesn't trip on them.
99667
+ */
99668
+ function escapeProblematicBraces(content) {
99669
+ const { htmlElements, protectedContent } = protectHTMLElements(content);
99633
99670
  let strDelim = null;
99634
99671
  let strEscaped = false;
99635
- // Stack of open braces with their state
99636
- const openStack = [];
99637
99672
  // Track position of last newline (outside strings) to detect blank lines
99638
- let lastNewlinePos = -2; // -2 means no recent newline
99673
+ // -2 means no recent newline
99674
+ let lastNewlinePos = -2;
99675
+ // Character state machine trackers
99676
+ const toEscape = new Set();
99677
+ // Convert to array of Unicode code points so that emojis and multi-byte characters are correctly tracked
99678
+ const chars = Array.from(protectedContent);
99679
+ const openStack = [];
99639
99680
  for (let i = 0; i < chars.length; i += 1) {
99640
99681
  const ch = chars[i];
99641
99682
  // Track string delimiters inside expressions to ignore braces within them
@@ -99655,22 +99696,17 @@ function escapeProblematicBraces(content) {
99655
99696
  // eslint-disable-next-line no-continue
99656
99697
  continue;
99657
99698
  }
99658
- // Track newlines to detect blank lines (paragraph boundaries)
99659
99699
  if (ch === '\n') {
99660
- // Check if this newline creates a blank line (only whitespace since last newline)
99661
99700
  if (lastNewlinePos >= 0) {
99662
99701
  const between = chars.slice(lastNewlinePos + 1, i).join('');
99663
99702
  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
- });
99703
+ openStack.forEach(entry => { entry.hasBlankLine = true; });
99668
99704
  }
99669
99705
  }
99670
99706
  lastNewlinePos = i;
99671
99707
  }
99672
99708
  }
99673
- // Skip already-escaped braces (count preceding backslashes)
99709
+ // Skip already-escaped braces (odd run of preceding backslashes).
99674
99710
  if (ch === '{' || ch === '}') {
99675
99711
  let bs = 0;
99676
99712
  for (let j = i - 1; j >= 0 && chars[j] === '\\'; j -= 1)
@@ -99681,10 +99717,9 @@ function escapeProblematicBraces(content) {
99681
99717
  }
99682
99718
  }
99683
99719
  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.
99720
+ // `=` (after whitespace) before `{` JSX attribute expression. The
99721
+ // mdxComponent tokenizer captures the whole component, so blank lines
99722
+ // inside attribute values are harmless. Nested `{` inherits the flag.
99688
99723
  let isAttrExpr = false;
99689
99724
  for (let j = i - 1; j >= 0; j -= 1) {
99690
99725
  const pc = chars[j];
@@ -99698,27 +99733,17 @@ function escapeProblematicBraces(content) {
99698
99733
  // Nested `{ ... }` inside an attribute value (e.g. `data={[{ ... }]}` or
99699
99734
  // `data={{ a: { b: 1 } }}`) must inherit the same exemption; only the
99700
99735
  // 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
- }
99736
+ if (!isAttrExpr && openStack.length > 0 && openStack[openStack.length - 1].isAttrExpr) {
99737
+ isAttrExpr = true;
99706
99738
  }
99707
99739
  openStack.push({ pos: i, hasBlankLine: false, isAttrExpr });
99708
- lastNewlinePos = -2; // Reset newline tracking for new expression
99740
+ lastNewlinePos = -2;
99709
99741
  }
99710
99742
  else if (ch === '}') {
99711
99743
  if (openStack.length > 0) {
99712
99744
  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.
99745
+ // Pure `{/* ... */}` comments are handled downstream by the jsxComment
99746
+ // tokenizer escaping their braces would prevent it from running.
99722
99747
  const isPureJsxComment = chars[entry.pos + 1] === '/' &&
99723
99748
  chars[entry.pos + 2] === '*' &&
99724
99749
  chars[i - 1] === '/' &&
@@ -99729,21 +99754,15 @@ function escapeProblematicBraces(content) {
99729
99754
  }
99730
99755
  }
99731
99756
  else {
99732
- // Unbalanced closing brace (no matching open)
99733
99757
  toEscape.add(i);
99734
99758
  }
99735
99759
  }
99736
99760
  }
99737
- // Any remaining open braces are unbalanced
99761
+ // Anything still open is unbalanced.
99738
99762
  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;
99763
+ // Reconstruct the content with the escaped braces.
99764
+ const escapedContent = toEscape.size === 0 ? protectedContent : chars.map((ch, i) => (toEscape.has(i) ? `\\${ch}` : ch)).join('');
99765
+ return restoreHTMLElements(escapedContent, htmlElements);
99747
99766
  }
99748
99767
  /**
99749
99768
  * Preprocesses JSX-like markdown content before parsing.
@@ -101527,8 +101546,7 @@ function tokenizeJsxTable(effects, ok, nok) {
101527
101546
  let codeSpanOpenSize = 0;
101528
101547
  let codeSpanCloseSize = 0;
101529
101548
  let depth = 1;
101530
- const TABLE_NAME = [codes.uppercaseT, codes.lowercaseA, codes.lowercaseB, codes.lowercaseL, codes.lowercaseE];
101531
- const ABLE_SUFFIX = TABLE_NAME.slice(1);
101549
+ const ABLE_SUFFIX = [codes.lowercaseA, codes.lowercaseB, codes.lowercaseL, codes.lowercaseE];
101532
101550
  /** Build a state chain that matches a sequence of character codes. */
101533
101551
  function matchChars(chars, onMatch, onFail) {
101534
101552
  if (chars.length === 0)
@@ -101548,7 +101566,14 @@ function tokenizeJsxTable(effects, ok, nok) {
101548
101566
  effects.enter('jsxTable');
101549
101567
  effects.enter('jsxTableData');
101550
101568
  effects.consume(code);
101551
- return matchChars(TABLE_NAME, afterTagName, nok);
101569
+ return afterLessThan;
101570
+ }
101571
+ function afterLessThan(code) {
101572
+ if (code === codes.uppercaseT || code === codes.lowercaseT) {
101573
+ effects.consume(code);
101574
+ return matchChars(ABLE_SUFFIX, afterTagName, nok);
101575
+ }
101576
+ return nok(code);
101552
101577
  }
101553
101578
  function afterTagName(code) {
101554
101579
  if (code === codes.greaterThan || code === codes.slash || code === codes.space || code === codes.horizontalTab) {
@@ -101620,14 +101645,21 @@ function tokenizeJsxTable(effects, ok, nok) {
101620
101645
  function closeSlash(code) {
101621
101646
  if (code === codes.slash) {
101622
101647
  effects.consume(code);
101623
- return matchChars(TABLE_NAME, closeGt, body);
101648
+ return closeTagFirstChar;
101624
101649
  }
101625
- if (code === codes.uppercaseT) {
101650
+ if (code === codes.uppercaseT || code === codes.lowercaseT) {
101626
101651
  effects.consume(code);
101627
101652
  return matchChars(ABLE_SUFFIX, openAfterTagName, body);
101628
101653
  }
101629
101654
  return body(code);
101630
101655
  }
101656
+ function closeTagFirstChar(code) {
101657
+ if (code === codes.uppercaseT || code === codes.lowercaseT) {
101658
+ effects.consume(code);
101659
+ return matchChars(ABLE_SUFFIX, closeGt, body);
101660
+ }
101661
+ return body(code);
101662
+ }
101631
101663
  function openAfterTagName(code) {
101632
101664
  if (code === codes.greaterThan || code === codes.slash || code === codes.space || code === codes.horizontalTab) {
101633
101665
  depth += 1;
@@ -101703,10 +101735,10 @@ function jsx_table_syntax_tokenizeNonLazyContinuationStart(effects, ok, nok) {
101703
101735
  }
101704
101736
  }
101705
101737
  /**
101706
- * Micromark extension that tokenizes `<Table>...</Table>` as a single flow block.
101738
+ * Micromark extension that tokenizes `<Table>...</Table>` and `<table>...</table>`
101739
+ * as a single flow block.
101707
101740
  *
101708
- * Prevents CommonMark HTML block type 6 from matching `<Table>` (case-insensitive
101709
- * match against `table`) and fragmenting it at blank lines.
101741
+ * Prevents CommonMark HTML block type 6 from fragmenting table blocks at blank lines.
101710
101742
  */
101711
101743
  function jsxTable() {
101712
101744
  return {