@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.node.js CHANGED
@@ -92101,7 +92101,7 @@ const hasFlowContent = (nodes) => {
92101
92101
  * a markdown table (phrasing-only) or keep as JSX <Table> (has flow content).
92102
92102
  */
92103
92103
  const processTableNode = (node, index, parent, documentPosition) => {
92104
- if (node.name !== 'Table')
92104
+ if (node.name !== 'Table' && node.name !== 'table')
92105
92105
  return;
92106
92106
  const position = documentPosition ?? node.position;
92107
92107
  const { align: alignAttr } = getAttrs(node);
@@ -92215,7 +92215,7 @@ const mdxishTables = () => tree => {
92215
92215
  const node = _node;
92216
92216
  if (typeof index !== 'number' || !parent || !('children' in parent))
92217
92217
  return;
92218
- if (!node.value.startsWith('<Table'))
92218
+ if (!node.value.startsWith('<Table') && !node.value.startsWith('<table'))
92219
92219
  return;
92220
92220
  try {
92221
92221
  const parsed = tableNodeProcessor.runSync(tableNodeProcessor.parse(node.value));
@@ -92237,13 +92237,10 @@ const mdxishTables = () => tree => {
92237
92237
  }
92238
92238
  });
92239
92239
  visit(parsed, isMDXElement, (tableNode) => {
92240
- if (tableNode.name === 'Table') {
92241
- processTableNode(tableNode, index, parent, node.position);
92242
- // Stop after the outermost Table so nested Tables don't overwrite parent.children[index]
92243
- // we let it get handled naturally
92244
- return EXIT;
92245
- }
92246
- return undefined;
92240
+ if (tableNode.name !== 'Table' && tableNode.name !== 'table')
92241
+ return undefined;
92242
+ processTableNode(tableNode, index, parent, node.position);
92243
+ return EXIT;
92247
92244
  });
92248
92245
  }
92249
92246
  catch {
@@ -119108,8 +119105,8 @@ const HTML_ELEMENT_BLOCK_RE = /<([a-zA-Z][a-zA-Z0-9-]*)[\s>][\s\S]*?<\/\1>/g;
119108
119105
  const NEWLINE_WITH_WHITESPACE_RE = /[^\S\n]*\n[^\S\n]*/g;
119109
119106
  /** Matches a closing block-level tag followed by non-tag text or by a newline then non-blank content. */
119110
119107
  const CLOSE_BLOCK_TAG_BOUNDARY_RE = /<\/([a-zA-Z][a-zA-Z0-9-]*)>\s*(?:(?!<)(\S)|\n([^\n]))/g;
119111
- /** Tests whether a string contains a complete HTML element (open + close tag). */
119112
- const COMPLETE_HTML_ELEMENT_RE = /<[a-zA-Z][^>]*>[\s\S]*<\/[a-zA-Z]/;
119108
+ /** Strips HTML open/close tags. Used to detect non-tag inner text content. */
119109
+ const HTML_TAG_STRIP_RE = /<\/?[a-zA-Z][^>]*>/g;
119113
119110
 
119114
119111
  ;// ./processor/transform/mdxish/magic-blocks/placeholder.ts
119115
119112
  const EMPTY_IMAGE_PLACEHOLDER = {
@@ -119349,10 +119346,12 @@ const parseTableCell = (text) => {
119349
119346
  const trimmedLines = normalized.split('\n').map(line => line.trimStart());
119350
119347
  const processed = trimmedLines.join('\n');
119351
119348
  const tree = contentParser.runSync(contentParser.parse(processed));
119352
- // Process markdown inside complete HTML elements (e.g. _emphasis_ within <li>).
119353
- // Bare tags like "<i>" are left for rehypeRaw since rehype-parse would mangle them.
119349
+ // Process markdown inside HTML blocks that have non-tag inner text (e.g. `<div>**x**`
119350
+ // or `<ul><li>_x_</li></ul>`). Pure bare tags like "<i>" or "<br>" are left for rehypeRaw
119351
+ // since rehype-parse would mangle them (auto-closing void/inline elements).
119354
119352
  visit(tree, 'html', (node) => {
119355
- if (COMPLETE_HTML_ELEMENT_RE.test(node.value)) {
119353
+ const hasInnerText = node.value.replace(HTML_TAG_STRIP_RE, '').trim().length > 0;
119354
+ if (hasInnerText) {
119356
119355
  node.value = processMarkdownInHtmlString(node.value);
119357
119356
  }
119358
119357
  else {
@@ -119675,11 +119674,17 @@ const blockTypes = [
119675
119674
  * Check if a node is a block-level node (cannot be inside a paragraph)
119676
119675
  */
119677
119676
  const isBlockNode = (node) => blockTypes.includes(node.type);
119677
+ const isParagraph = (node) => node.type === 'paragraph';
119678
+ /**
119679
+ * True for phrasing content that contributes only whitespace at render time
119680
+ * (a soft `break` node or a text node with no non-whitespace characters).
119681
+ */
119682
+ const isWhitespacePhrasing = (node) => node.type === 'break' || (node.type === 'text' && !node.value.trim());
119678
119683
  /**
119679
119684
  * Unified plugin that transforms magicBlock nodes into final MDAST nodes.
119680
119685
  */
119681
119686
  const magicBlockTransformer = (options = {}) => tree => {
119682
- const replacements = [];
119687
+ const lifts = [];
119683
119688
  visitParents(tree, 'magicBlock', (node, ancestors) => {
119684
119689
  const parent = ancestors[ancestors.length - 1]; // direct parent of the current node
119685
119690
  const index = parent.children.indexOf(node);
@@ -119693,51 +119698,60 @@ const magicBlockTransformer = (options = {}) => tree => {
119693
119698
  parent.children.splice(index, 1);
119694
119699
  return;
119695
119700
  }
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))) {
119701
+ // If parent is a paragraph and we're inserting block nodes (which must not be in paragraphs)
119702
+ // it means we need to lift them out
119703
+ if (isParagraph(parent) && children.some(isBlockNode)) {
119698
119704
  const blockNodes = [];
119699
- const inlineNodes = [];
119700
119705
  children.forEach(child => {
119701
- (isBlockNode(child) ? blockNodes : inlineNodes).push(child);
119706
+ if (isBlockNode(child)) {
119707
+ blockNodes.push(child);
119708
+ }
119702
119709
  });
119703
- replacements.push({
119704
- container: ancestors[ancestors.length - 2] || tree, // grandparent of the current node
119710
+ lifts.push({
119711
+ childrenBlockNodes: blockNodes,
119712
+ grandparent: ancestors[ancestors.length - 2] || tree, // grandparent of the current node
119713
+ node,
119705
119714
  parent,
119706
- blockNodes,
119707
- inlineNodes,
119708
- before: parent.children.slice(0, index),
119709
- after: parent.children.slice(index + 1),
119710
119715
  });
119711
119716
  }
119712
119717
  else {
119713
119718
  parent.children.splice(index, 1, ...children);
119714
119719
  }
119715
119720
  });
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);
119721
+ // Second pass: apply lifts that move block nodes, and the content after them, out of paragraphs.
119722
+ // Operate on live state (find each node's current index now) and process in reverse so
119723
+ // that container insertions don't disturb earlier paragraphs' positions.
119724
+ for (let i = lifts.length - 1; i >= 0; i -= 1) {
119725
+ const { childrenBlockNodes: blockNodes, grandparent, node, parent: parentParagraph } = lifts[i];
119726
+ const nodePosition = parentParagraph.children.indexOf(node);
119727
+ const parentPosition = grandparent.children.indexOf(parentParagraph);
119728
+ if (nodePosition === -1 || parentPosition === -1) {
119724
119729
  // eslint-disable-next-line no-continue
119725
119730
  continue;
119726
119731
  }
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);
119732
+ // Snapshot live siblings to reconstruct the parent paragraph around the lifted node.
119733
+ const parentSiblingsBefore = parentParagraph.children.slice(0, nodePosition);
119734
+ const parentSiblingsAfter = parentParagraph.children.slice(nodePosition + 1);
119735
+ // Remove split-edge whitespace so lifted blocks do not render with extra blank lines.
119736
+ while (parentSiblingsBefore.length > 0 && isWhitespacePhrasing(parentSiblingsBefore[parentSiblingsBefore.length - 1]))
119737
+ parentSiblingsBefore.pop();
119738
+ while (parentSiblingsAfter.length > 0 && isWhitespacePhrasing(parentSiblingsAfter[0]))
119739
+ parentSiblingsAfter.shift();
119740
+ const splitOffContentFromParent = [...blockNodes];
119741
+ if (parentSiblingsAfter.length > 0) {
119742
+ // Keep trailing inline content grouped under a paragraph sibling as it might be an inline node
119743
+ // Even if it contains a block node, it will be hoisted out during its turn in the loop
119744
+ const trailingParagraph = { type: 'paragraph', children: parentSiblingsAfter };
119745
+ splitOffContentFromParent.push(trailingParagraph);
119746
+ }
119747
+ parentParagraph.children = [...parentSiblingsBefore];
119748
+ // If the parent paragraph is empty, just replace it with the lifted block nodes
119749
+ const shouldReplaceParent = parentParagraph.children.length === 0;
119750
+ if (shouldReplaceParent) {
119751
+ grandparent.children.splice(parentPosition, 1, ...splitOffContentFromParent);
119735
119752
  }
119736
119753
  else {
119737
- parent.children = [...before, ...after];
119738
- if (blockNodes.length > 0) {
119739
- containerChildren.splice(paraIndex + 1, 0, ...blockNodes);
119740
- }
119754
+ grandparent.children.splice(parentPosition + 1, 0, ...splitOffContentFromParent);
119741
119755
  }
119742
119756
  }
119743
119757
  };
@@ -119807,29 +119821,56 @@ function protectHTMLBlockContent(content) {
119807
119821
  function removeJSXComments(content) {
119808
119822
  return content.replace(JSX_COMMENT_REGEX, '');
119809
119823
  }
119824
+ const HTML_ELEM_PLACEHOLDER_PREFIX = '___MDXISH_HTML_ELEM_';
119825
+ const HTML_ELEM_PLACEHOLDER = new RegExp(`${HTML_ELEM_PLACEHOLDER_PREFIX}(\\d+)___`, 'g');
119826
+ // Matches an HTML element that starts at a line boundary and ends at a line boundary.
119827
+ // Allows optional leading indentation and lazily matches until the same closing tag.
119828
+ const BLOCK_HTML_RE = /(?<=^|\n)[ \t]*<([a-z][a-zA-Z0-9]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>[ \t]*(?=\n|$)/g;
119810
119829
  /**
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.
119830
+ * Hides line-anchored HTML elements from the brace-escaping pass so we don't leak `\{`
119831
+ * into rendered output (rehypeRaw renders the `\` literally, e.g. `<div>{foo</div>`).
119832
+ *
119833
+ * One carve-out: if an interior line at column 0 has bare text containing `{`, mdxish
119834
+ * parses that line as a paragraph and the mdxExpression step would throw without an
119835
+ * escape — so we leave that case to the brace balancer.
119814
119836
  */
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
119837
+ function protectHTMLElements(content) {
119818
119838
  const htmlElements = [];
119819
- const safe = content.replace(/<([a-z][a-zA-Z0-9]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>/g, match => {
119820
- const idx = htmlElements.length;
119839
+ const protectedContent = content.replace(BLOCK_HTML_RE, match => {
119840
+ // Look at the lines between the open and close tags. If any of them starts
119841
+ // at column 0 with bare text (not whitespace, not another tag) and contains
119842
+ // `{`, mdxish will parse that line as a paragraph and the brace as an MDX
119843
+ // expression, which would throw an error. So we let the brace balancer escape it.
119844
+ // Otherwise, we need to extract the sequence to protect it from the brace escaping.
119845
+ const interior = match.split('\n').slice(1, -1);
119846
+ const hazard = interior.some(line => line.length > 0 && line[0] !== ' ' && line[0] !== '\t' && line[0] !== '<' && line.includes('{'));
119847
+ if (hazard)
119848
+ return match;
119821
119849
  htmlElements.push(match);
119822
- return `___HTML_ELEM_${idx}___`;
119850
+ return `${HTML_ELEM_PLACEHOLDER_PREFIX}${htmlElements.length - 1}___`;
119823
119851
  });
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);
119852
+ return { htmlElements, protectedContent };
119853
+ }
119854
+ function restoreHTMLElements(content, htmlElements) {
119855
+ if (htmlElements.length === 0)
119856
+ return content;
119857
+ return content.replace(HTML_ELEM_PLACEHOLDER, (_m, idx) => htmlElements[parseInt(idx, 10)]);
119858
+ }
119859
+ /**
119860
+ * Escapes unbalanced and paragraph-spanning braces so MDX doesn't trip on them.
119861
+ */
119862
+ function escapeProblematicBraces(content) {
119863
+ const { htmlElements, protectedContent } = protectHTMLElements(content);
119827
119864
  let strDelim = null;
119828
119865
  let strEscaped = false;
119829
- // Stack of open braces with their state
119830
- const openStack = [];
119831
119866
  // Track position of last newline (outside strings) to detect blank lines
119832
- let lastNewlinePos = -2; // -2 means no recent newline
119867
+ // -2 means no recent newline
119868
+ let lastNewlinePos = -2;
119869
+ // Character state machine trackers
119870
+ const toEscape = new Set();
119871
+ // Convert to array of Unicode code points so that emojis and multi-byte characters are correctly tracked
119872
+ const chars = Array.from(protectedContent);
119873
+ const openStack = [];
119833
119874
  for (let i = 0; i < chars.length; i += 1) {
119834
119875
  const ch = chars[i];
119835
119876
  // Track string delimiters inside expressions to ignore braces within them
@@ -119849,22 +119890,17 @@ function escapeProblematicBraces(content) {
119849
119890
  // eslint-disable-next-line no-continue
119850
119891
  continue;
119851
119892
  }
119852
- // Track newlines to detect blank lines (paragraph boundaries)
119853
119893
  if (ch === '\n') {
119854
- // Check if this newline creates a blank line (only whitespace since last newline)
119855
119894
  if (lastNewlinePos >= 0) {
119856
119895
  const between = chars.slice(lastNewlinePos + 1, i).join('');
119857
119896
  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
- });
119897
+ openStack.forEach(entry => { entry.hasBlankLine = true; });
119862
119898
  }
119863
119899
  }
119864
119900
  lastNewlinePos = i;
119865
119901
  }
119866
119902
  }
119867
- // Skip already-escaped braces (count preceding backslashes)
119903
+ // Skip already-escaped braces (odd run of preceding backslashes).
119868
119904
  if (ch === '{' || ch === '}') {
119869
119905
  let bs = 0;
119870
119906
  for (let j = i - 1; j >= 0 && chars[j] === '\\'; j -= 1)
@@ -119875,10 +119911,9 @@ function escapeProblematicBraces(content) {
119875
119911
  }
119876
119912
  }
119877
119913
  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.
119914
+ // `=` (after whitespace) before `{` JSX attribute expression. The
119915
+ // mdxComponent tokenizer captures the whole component, so blank lines
119916
+ // inside attribute values are harmless. Nested `{` inherits the flag.
119882
119917
  let isAttrExpr = false;
119883
119918
  for (let j = i - 1; j >= 0; j -= 1) {
119884
119919
  const pc = chars[j];
@@ -119892,27 +119927,17 @@ function escapeProblematicBraces(content) {
119892
119927
  // Nested `{ ... }` inside an attribute value (e.g. `data={[{ ... }]}` or
119893
119928
  // `data={{ a: { b: 1 } }}`) must inherit the same exemption; only the
119894
119929
  // 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
- }
119930
+ if (!isAttrExpr && openStack.length > 0 && openStack[openStack.length - 1].isAttrExpr) {
119931
+ isAttrExpr = true;
119900
119932
  }
119901
119933
  openStack.push({ pos: i, hasBlankLine: false, isAttrExpr });
119902
- lastNewlinePos = -2; // Reset newline tracking for new expression
119934
+ lastNewlinePos = -2;
119903
119935
  }
119904
119936
  else if (ch === '}') {
119905
119937
  if (openStack.length > 0) {
119906
119938
  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.
119939
+ // Pure `{/* ... */}` comments are handled downstream by the jsxComment
119940
+ // tokenizer escaping their braces would prevent it from running.
119916
119941
  const isPureJsxComment = chars[entry.pos + 1] === '/' &&
119917
119942
  chars[entry.pos + 2] === '*' &&
119918
119943
  chars[i - 1] === '/' &&
@@ -119923,21 +119948,15 @@ function escapeProblematicBraces(content) {
119923
119948
  }
119924
119949
  }
119925
119950
  else {
119926
- // Unbalanced closing brace (no matching open)
119927
119951
  toEscape.add(i);
119928
119952
  }
119929
119953
  }
119930
119954
  }
119931
- // Any remaining open braces are unbalanced
119955
+ // Anything still open is unbalanced.
119932
119956
  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;
119957
+ // Reconstruct the content with the escaped braces.
119958
+ const escapedContent = toEscape.size === 0 ? protectedContent : chars.map((ch, i) => (toEscape.has(i) ? `\\${ch}` : ch)).join('');
119959
+ return restoreHTMLElements(escapedContent, htmlElements);
119941
119960
  }
119942
119961
  /**
119943
119962
  * Preprocesses JSX-like markdown content before parsing.
@@ -121721,8 +121740,7 @@ function tokenizeJsxTable(effects, ok, nok) {
121721
121740
  let codeSpanOpenSize = 0;
121722
121741
  let codeSpanCloseSize = 0;
121723
121742
  let depth = 1;
121724
- const TABLE_NAME = [codes.uppercaseT, codes.lowercaseA, codes.lowercaseB, codes.lowercaseL, codes.lowercaseE];
121725
- const ABLE_SUFFIX = TABLE_NAME.slice(1);
121743
+ const ABLE_SUFFIX = [codes.lowercaseA, codes.lowercaseB, codes.lowercaseL, codes.lowercaseE];
121726
121744
  /** Build a state chain that matches a sequence of character codes. */
121727
121745
  function matchChars(chars, onMatch, onFail) {
121728
121746
  if (chars.length === 0)
@@ -121742,7 +121760,14 @@ function tokenizeJsxTable(effects, ok, nok) {
121742
121760
  effects.enter('jsxTable');
121743
121761
  effects.enter('jsxTableData');
121744
121762
  effects.consume(code);
121745
- return matchChars(TABLE_NAME, afterTagName, nok);
121763
+ return afterLessThan;
121764
+ }
121765
+ function afterLessThan(code) {
121766
+ if (code === codes.uppercaseT || code === codes.lowercaseT) {
121767
+ effects.consume(code);
121768
+ return matchChars(ABLE_SUFFIX, afterTagName, nok);
121769
+ }
121770
+ return nok(code);
121746
121771
  }
121747
121772
  function afterTagName(code) {
121748
121773
  if (code === codes.greaterThan || code === codes.slash || code === codes.space || code === codes.horizontalTab) {
@@ -121814,14 +121839,21 @@ function tokenizeJsxTable(effects, ok, nok) {
121814
121839
  function closeSlash(code) {
121815
121840
  if (code === codes.slash) {
121816
121841
  effects.consume(code);
121817
- return matchChars(TABLE_NAME, closeGt, body);
121842
+ return closeTagFirstChar;
121818
121843
  }
121819
- if (code === codes.uppercaseT) {
121844
+ if (code === codes.uppercaseT || code === codes.lowercaseT) {
121820
121845
  effects.consume(code);
121821
121846
  return matchChars(ABLE_SUFFIX, openAfterTagName, body);
121822
121847
  }
121823
121848
  return body(code);
121824
121849
  }
121850
+ function closeTagFirstChar(code) {
121851
+ if (code === codes.uppercaseT || code === codes.lowercaseT) {
121852
+ effects.consume(code);
121853
+ return matchChars(ABLE_SUFFIX, closeGt, body);
121854
+ }
121855
+ return body(code);
121856
+ }
121825
121857
  function openAfterTagName(code) {
121826
121858
  if (code === codes.greaterThan || code === codes.slash || code === codes.space || code === codes.horizontalTab) {
121827
121859
  depth += 1;
@@ -121897,10 +121929,10 @@ function jsx_table_syntax_tokenizeNonLazyContinuationStart(effects, ok, nok) {
121897
121929
  }
121898
121930
  }
121899
121931
  /**
121900
- * Micromark extension that tokenizes `<Table>...</Table>` as a single flow block.
121932
+ * Micromark extension that tokenizes `<Table>...</Table>` and `<table>...</table>`
121933
+ * as a single flow block.
121901
121934
  *
121902
- * Prevents CommonMark HTML block type 6 from matching `<Table>` (case-insensitive
121903
- * match against `table`) and fragmenting it at blank lines.
121935
+ * Prevents CommonMark HTML block type 6 from fragmenting table blocks at blank lines.
121904
121936
  */
121905
121937
  function jsxTable() {
121906
121938
  return {