@readme/markdown 11.14.1 → 12.0.0

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
@@ -11362,7 +11362,6 @@ __webpack_require__.d(__webpack_exports__, {
11362
11362
  exports: () => (/* reexport */ lib_exports),
11363
11363
  gemojiRegex: () => (/* reexport */ gemoji_regex),
11364
11364
  hast: () => (/* reexport */ lib_hast),
11365
- isPlainText: () => (/* reexport */ isPlainText),
11366
11365
  mdast: () => (/* reexport */ lib_mdast),
11367
11366
  mdastV6: () => (/* reexport */ lib_mdastV6),
11368
11367
  mdx: () => (/* reexport */ lib_mdx),
@@ -52944,12 +52943,40 @@ const plain = (node, opts = {}) => {
52944
52943
  };
52945
52944
  /* harmony default export */ const lib_plain = (plain);
52946
52945
 
52946
+ ;// ./processor/transform/extract-text.ts
52947
+ /**
52948
+ * Extracts text content from a single AST node recursively.
52949
+ * Works with both MDAST and HAST-like node structures.
52950
+ *
52951
+ * Placed this outside of the utils.ts file to avoid circular dependencies.
52952
+ *
52953
+ * @param node - The node to extract text from (can be MDAST Node or HAST-like structure)
52954
+ * @returns The concatenated text content
52955
+ */
52956
+ const extractText = (node) => {
52957
+ if (node.type === 'text' && typeof node.value === 'string') {
52958
+ return node.value;
52959
+ }
52960
+ if (node.children && Array.isArray(node.children)) {
52961
+ return node.children
52962
+ .map(child => {
52963
+ if (child && typeof child === 'object' && 'type' in child) {
52964
+ return extractText(child);
52965
+ }
52966
+ return '';
52967
+ })
52968
+ .join('');
52969
+ }
52970
+ return '';
52971
+ };
52972
+
52947
52973
  ;// ./processor/transform/callouts.ts
52948
52974
 
52949
52975
 
52950
52976
 
52951
52977
 
52952
52978
 
52979
+
52953
52980
  const callouts_regex = `^(${emoji_regex().source}|⚠)(\\s+|$)`;
52954
52981
  const findFirst = (node) => {
52955
52982
  if ('children' in node)
@@ -52970,42 +52997,76 @@ const wrapHeading = (node) => {
52970
52997
  },
52971
52998
  };
52972
52999
  };
53000
+ /**
53001
+ * Checks if a blockquote matches the expected callout structure:
53002
+ * blockquote > paragraph > text node
53003
+ */
53004
+ const isCalloutStructure = (node) => {
53005
+ const firstChild = node.children?.[0];
53006
+ if (!firstChild || firstChild.type !== 'paragraph')
53007
+ return false;
53008
+ if (!('children' in firstChild))
53009
+ return false;
53010
+ const firstTextChild = firstChild.children?.[0];
53011
+ return firstTextChild?.type === 'text';
53012
+ };
53013
+ const processBlockquote = (node, index, parent) => {
53014
+ if (!isCalloutStructure(node)) {
53015
+ // Only stringify empty blockquotes (no extractable text content)
53016
+ // Preserve blockquotes with actual content (e.g., headings, lists, etc.)
53017
+ const content = extractText(node);
53018
+ const isEmpty = !content || content.trim() === '';
53019
+ if (isEmpty && index !== undefined && parent) {
53020
+ const textNode = {
53021
+ type: 'text',
53022
+ value: '>',
53023
+ };
53024
+ const paragraphNode = {
53025
+ type: 'paragraph',
53026
+ children: [textNode],
53027
+ position: node.position,
53028
+ };
53029
+ parent.children.splice(index, 1, paragraphNode);
53030
+ }
53031
+ return;
53032
+ }
53033
+ // isCalloutStructure ensures node.children[0] is a Paragraph with children
53034
+ const firstParagraph = node.children[0];
53035
+ const startText = lib_plain(firstParagraph).toString();
53036
+ const [match, icon] = startText.match(callouts_regex) || [];
53037
+ if (icon && match) {
53038
+ const heading = startText.slice(match.length);
53039
+ const empty = !heading.length && firstParagraph.children.length === 1;
53040
+ const theme = themes[icon] || 'default';
53041
+ const firstChild = findFirst(node.children[0]);
53042
+ if (firstChild && 'value' in firstChild && typeof firstChild.value === 'string') {
53043
+ firstChild.value = firstChild.value.slice(match.length);
53044
+ }
53045
+ if (heading) {
53046
+ node.children[0] = wrapHeading(node);
53047
+ // @note: We add to the offset/column the length of the unicode
53048
+ // character that was stripped off, so that the start position of the
53049
+ // heading/text matches where it actually starts.
53050
+ node.children[0].position.start.offset += match.length;
53051
+ node.children[0].position.start.column += match.length;
53052
+ }
53053
+ Object.assign(node, {
53054
+ type: NodeTypes.callout,
53055
+ data: {
53056
+ hName: 'Callout',
53057
+ hProperties: {
53058
+ icon,
53059
+ ...(empty && { empty }),
53060
+ theme,
53061
+ },
53062
+ },
53063
+ });
53064
+ }
53065
+ };
52973
53066
  const calloutTransformer = () => {
52974
53067
  return (tree) => {
52975
- visit(tree, 'blockquote', (node) => {
52976
- if (!(node.children[0].type === 'paragraph' && node.children[0].children[0].type === 'text'))
52977
- return;
52978
- // @ts-expect-error -- @todo: update plain to accept mdast
52979
- const startText = lib_plain(node.children[0]).toString();
52980
- const [match, icon] = startText.match(callouts_regex) || [];
52981
- if (icon && match) {
52982
- const heading = startText.slice(match.length);
52983
- const empty = !heading.length && node.children[0].children.length === 1;
52984
- const theme = themes[icon] || 'default';
52985
- const firstChild = findFirst(node.children[0]);
52986
- if (firstChild && 'value' in firstChild && typeof firstChild.value === 'string') {
52987
- firstChild.value = firstChild.value.slice(match.length);
52988
- }
52989
- if (heading) {
52990
- node.children[0] = wrapHeading(node);
52991
- // @note: We add to the offset/column the length of the unicode
52992
- // character that was stripped off, so that the start position of the
52993
- // heading/text matches where it actually starts.
52994
- node.children[0].position.start.offset += match.length;
52995
- node.children[0].position.start.column += match.length;
52996
- }
52997
- Object.assign(node, {
52998
- type: NodeTypes.callout,
52999
- data: {
53000
- hName: 'Callout',
53001
- hProperties: {
53002
- icon,
53003
- ...(empty && { empty }),
53004
- theme,
53005
- },
53006
- },
53007
- });
53008
- }
53068
+ visit(tree, 'blockquote', (node, index, parent) => {
53069
+ processBlockquote(node, index, parent);
53009
53070
  });
53010
53071
  };
53011
53072
  };
@@ -70621,10 +70682,13 @@ const tagAttributePattern = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*("[^"]*"|'[^'
70621
70682
  const MAX_LOOKAHEAD = 30;
70622
70683
  /**
70623
70684
  * Tags that have dedicated transformers and should NOT be handled by this plugin.
70624
- * These components have special parsing requirements that the generic component
70625
- * block transformer cannot handle correctly.
70685
+ * These components either have special parsing requirements that the generic component
70686
+ * block transformer cannot handle correctly, or are inline components that we don't
70687
+ * want to convert to mdxJsxFlowElement which is a block level element.
70688
+ *
70689
+ * Glossary and Anchor are inline components.
70626
70690
  */
70627
- const EXCLUDED_TAGS = new Set(['HTMLBlock', 'Table']);
70691
+ const EXCLUDED_TAGS = new Set(['HTMLBlock', 'Table', 'Glossary', 'Anchor']);
70628
70692
  const inlineMdProcessor = unified().use(remarkParse);
70629
70693
  const isClosingTag = (value, tag) => value.trim() === `</${tag}>`;
70630
70694
  /**
@@ -70668,6 +70732,17 @@ const parseTag = (value) => {
70668
70732
  contentAfterTag,
70669
70733
  };
70670
70734
  };
70735
+ /**
70736
+ * Parse substring content of a node and update the parent's children to include the new nodes.
70737
+ */
70738
+ const parseSibling = (stack, parent, index, sibling) => {
70739
+ const siblingNodes = parseMdChildren(sibling);
70740
+ // The new sibling nodes might contain new components to be processed
70741
+ if (siblingNodes.length > 0) {
70742
+ parent.children.splice(index + 1, 0, ...siblingNodes);
70743
+ stack.push(parent);
70744
+ }
70745
+ };
70671
70746
  /**
70672
70747
  * Create an MdxJsxFlowElement node from component data.
70673
70748
  */
@@ -70810,11 +70885,12 @@ const mdxishComponentBlocks = () => tree => {
70810
70885
  if ('children' in node && Array.isArray(node.children)) {
70811
70886
  stack.push(node);
70812
70887
  }
70813
- // Only visit HTML nodes with an actual html tag
70888
+ // Only visit HTML nodes with an actual html tag,
70889
+ // which means a potential unparsed MDX component
70814
70890
  const value = node.value;
70815
70891
  if (node.type !== 'html' || typeof value !== 'string')
70816
70892
  return;
70817
- const parsed = parseTag(value);
70893
+ const parsed = parseTag(value.trim());
70818
70894
  if (!parsed)
70819
70895
  return;
70820
70896
  const { tag, attributes, selfClosing, contentAfterTag = '' } = parsed;
@@ -70831,11 +70907,19 @@ const mdxishComponentBlocks = () => tree => {
70831
70907
  startPosition: node.position,
70832
70908
  });
70833
70909
  substituteNodeWithMdxNode(parent, index, componentNode);
70910
+ // Check and parse if there's relevant content after the current closing tag
70911
+ const remainingContent = contentAfterTag.trim();
70912
+ if (remainingContent) {
70913
+ parseSibling(stack, parent, index, remainingContent);
70914
+ }
70834
70915
  return;
70835
70916
  }
70836
70917
  // Case 2: Self-contained block (closing tag in content)
70837
70918
  if (contentAfterTag.includes(closingTagStr)) {
70838
- const componentInnerContent = contentAfterTag.substring(0, contentAfterTag.lastIndexOf(closingTagStr)).trim();
70919
+ // Find the first closing tag
70920
+ const closingTagIndex = contentAfterTag.indexOf(closingTagStr);
70921
+ const componentInnerContent = contentAfterTag.substring(0, closingTagIndex).trim();
70922
+ const contentAfterClose = contentAfterTag.substring(closingTagIndex + closingTagStr.length).trim();
70839
70923
  const componentNode = createComponentNode({
70840
70924
  tag,
70841
70925
  attributes,
@@ -70843,6 +70927,14 @@ const mdxishComponentBlocks = () => tree => {
70843
70927
  startPosition: node.position,
70844
70928
  });
70845
70929
  substituteNodeWithMdxNode(parent, index, componentNode);
70930
+ // After the closing tag, there might be more content to be processed
70931
+ if (contentAfterClose) {
70932
+ parseSibling(stack, parent, index, contentAfterClose);
70933
+ }
70934
+ else if (componentNode.children.length > 0) {
70935
+ // The content inside the component block might contain new components to be processed
70936
+ stack.push(componentNode);
70937
+ }
70846
70938
  return;
70847
70939
  }
70848
70940
  // Case 3: Multi-sibling component (closing tag in a following sibling)
@@ -70868,8 +70960,13 @@ const mdxishComponentBlocks = () => tree => {
70868
70960
  });
70869
70961
  // Remove all nodes from opening tag to closing tag (inclusive) and replace with component node
70870
70962
  parent.children.splice(index, closingIndex - index + 1, componentNode);
70963
+ // Since we might be merging sibling nodes together and combining content,
70964
+ // there might be new components to process
70965
+ if (componentNode.children.length > 0) {
70966
+ stack.push(componentNode);
70967
+ }
70871
70968
  };
70872
- // Travel the tree depth-first
70969
+ // Process the nodes with the components depth-first to maintain the correct order of the nodes
70873
70970
  while (stack.length) {
70874
70971
  const parent = stack.pop();
70875
70972
  if (parent?.children) {
@@ -70890,6 +70987,7 @@ const mdxishComponentBlocks = () => tree => {
70890
70987
 
70891
70988
 
70892
70989
 
70990
+
70893
70991
  const isTableCell = (node) => isMDXElement(node) && ['th', 'td'].includes(node.name);
70894
70992
  const tableTypes = {
70895
70993
  tr: 'tableRow',
@@ -70914,18 +71012,13 @@ const isTextOnly = (children) => {
70914
71012
  });
70915
71013
  };
70916
71014
  /**
70917
- * Extract text content from children nodes
71015
+ * Convenience wrapper that extracts text content from an array of children nodes.
70918
71016
  */
70919
- const extractText = (children) => {
71017
+ const extractTextFromChildren = (children) => {
70920
71018
  return children
70921
71019
  .map(child => {
70922
71020
  if (child && typeof child === 'object' && 'type' in child) {
70923
- if (child.type === 'text' && 'value' in child && typeof child.value === 'string') {
70924
- return child.value;
70925
- }
70926
- if (child.type === 'mdxJsxTextElement' && 'children' in child && Array.isArray(child.children)) {
70927
- return extractText(child.children);
70928
- }
71021
+ return extractText(child);
70929
71022
  }
70930
71023
  return '';
70931
71024
  })
@@ -70956,7 +71049,7 @@ const processTableNode = (node, index, parent) => {
70956
71049
  let parsedChildren = cellChildren;
70957
71050
  // If cell contains only text nodes, try to re-parse as markdown
70958
71051
  if (isTextOnly(cellChildren)) {
70959
- const textContent = extractText(cellChildren);
71052
+ const textContent = extractTextFromChildren(cellChildren);
70960
71053
  if (textContent.trim()) {
70961
71054
  try {
70962
71055
  const parsed = parseMarkdown(textContent);
@@ -86371,7 +86464,7 @@ const CUSTOM_PROP_BOUNDARIES = [
86371
86464
  /**
86372
86465
  * Tags that should be passed through and handled at runtime (not by the mdxish plugin)
86373
86466
  */
86374
- const RUNTIME_COMPONENT_TAGS = new Set(['Variable', 'variable']);
86467
+ const RUNTIME_COMPONENT_TAGS = new Set(['Variable', 'variable', 'rdme-pin']);
86375
86468
  /**
86376
86469
  * Standard HTML tags that should never be treated as custom components.
86377
86470
  * Uses the html-tags package, converted to a Set<string> for efficient lookups.
@@ -93002,6 +93095,33 @@ const INLINE_COMPONENT_TAGS = new Set(['anchor', 'glossary']);
93002
93095
  function isElementContentNode(node) {
93003
93096
  return node.type === 'element' || node.type === 'text' || node.type === 'comment';
93004
93097
  }
93098
+ /**
93099
+ * Components are assumed to be block-level, so whitespace between them can be removed
93100
+ * We want to remove them because it can be treated as actual children of the component
93101
+ * when it doesn't need to.
93102
+ *
93103
+ * It can be a problem because it can be passed as args to the components, like the ones
93104
+ * defined in /components, which can break the type assumptions of the components and cause
93105
+ * type errors, accessing properties that don't exist.
93106
+ */
93107
+ function areAllChildrenComponents(children) {
93108
+ return children.every(child => {
93109
+ // Whitespace-only text nodes don't affect the check
93110
+ if (child.type === 'text' && !child.value.trim())
93111
+ return true;
93112
+ // Text with actual content means we have mixed content
93113
+ if (child.type === 'text')
93114
+ return false;
93115
+ // Comments don't affect the check
93116
+ if (child.type === 'comment')
93117
+ return true;
93118
+ // Standard HTML tags are not considered components
93119
+ if (child.type === 'element' && 'tagName' in child) {
93120
+ return !STANDARD_HTML_TAGS.has(child.tagName.toLowerCase());
93121
+ }
93122
+ return false;
93123
+ });
93124
+ }
93005
93125
  /** Check if nodes represent a single paragraph with only text (no markdown formatting) */
93006
93126
  function isSingleParagraphTextNode(nodes) {
93007
93127
  return (nodes.length === 1 &&
@@ -93046,6 +93166,7 @@ function isActualHtmlTag(tagName, originalExcerpt) {
93046
93166
  function parseTextChildren(node, processMarkdown) {
93047
93167
  if (!node.children?.length)
93048
93168
  return;
93169
+ // First pass: Recursively process text children as they may contain stringified markdown / mdx content
93049
93170
  node.children = node.children.flatMap(child => {
93050
93171
  if (child.type !== 'text' || !child.value.trim())
93051
93172
  return [child];
@@ -93057,6 +93178,15 @@ function parseTextChildren(node, processMarkdown) {
93057
93178
  }
93058
93179
  return children;
93059
93180
  });
93181
+ // Post-processing: remove whitespace-only text nodes if all siblings are components
93182
+ // This prevents whitespace between component children from being counted as extra children
93183
+ if (areAllChildrenComponents(node.children)) {
93184
+ node.children = node.children.filter(child => {
93185
+ if (child.type === 'text' && !child.value.trim())
93186
+ return false;
93187
+ return true;
93188
+ });
93189
+ }
93060
93190
  }
93061
93191
  /** Convert node properties from kebab-case/lowercase to camelCase */
93062
93192
  function normalizeProperties(node) {
@@ -93119,6 +93249,15 @@ const mdxExpressionHandler = (_state, node) => ({
93119
93249
  type: 'text',
93120
93250
  value: node.value || '',
93121
93251
  });
93252
+ // Since we serialize component / html tag attributes
93253
+ function decodeHtmlEntities(value) {
93254
+ return value
93255
+ .replace(/&quot;/g, '"')
93256
+ .replace(/&lt;/g, '<')
93257
+ .replace(/&gt;/g, '>')
93258
+ .replace(/&#10;/g, '\n')
93259
+ .replace(/&amp;/g, '&');
93260
+ }
93122
93261
  // Convert MDX JSX elements to HAST elements, preserving attributes and children
93123
93262
  const mdxJsxElementHandler = (state, node) => {
93124
93263
  const { attributes = [], name } = node;
@@ -93130,7 +93269,7 @@ const mdxJsxElementHandler = (state, node) => {
93130
93269
  properties[attribute.name] = true;
93131
93270
  }
93132
93271
  else if (typeof attribute.value === 'string') {
93133
- properties[attribute.name] = attribute.value;
93272
+ properties[attribute.name] = decodeHtmlEntities(attribute.value);
93134
93273
  }
93135
93274
  else {
93136
93275
  properties[attribute.name] = attribute.value.value;
@@ -93192,6 +93331,17 @@ function base64Decode(str) {
93192
93331
  }
93193
93332
  return decodeURIComponent(escape(atob(str)));
93194
93333
  }
93334
+ function escapeHtmlAttribute(value) {
93335
+ return value
93336
+ .replace(/&/g, '&amp;')
93337
+ .replace(/"/g, '&quot;')
93338
+ .replace(/</g, '&lt;')
93339
+ .replace(/>/g, '&gt;')
93340
+ .replace(/\n/g, '&#10;');
93341
+ }
93342
+ // Marker prefix for JSON-serialized complex values (arrays/objects)
93343
+ // Using a prefix that won't conflict with regular string values
93344
+ const JSON_VALUE_MARKER = '__MDXISH_JSON__';
93195
93345
  // Markers for protected HTMLBlock content (HTML comments avoid markdown parsing issues)
93196
93346
  const HTML_BLOCK_CONTENT_START = '<!--RDMX_HTMLBLOCK:';
93197
93347
  const HTML_BLOCK_CONTENT_END = ':RDMX_HTMLBLOCK-->';
@@ -93321,6 +93471,16 @@ function extractBalancedBraces(content, start) {
93321
93471
  return null;
93322
93472
  return { content: content.slice(start, pos - 1), end: pos };
93323
93473
  }
93474
+ function restoreInlineCode(content, protectedCode) {
93475
+ return content.replace(/___INLINE_CODE_(\d+)___/g, (_m, idx) => {
93476
+ return protectedCode.inlineCode[parseInt(idx, 10)];
93477
+ });
93478
+ }
93479
+ function restoreCodeBlocks(content, protectedCode) {
93480
+ return content.replace(/___CODE_BLOCK_(\d+)___/g, (_m, idx) => {
93481
+ return protectedCode.codeBlocks[parseInt(idx, 10)];
93482
+ });
93483
+ }
93324
93484
  /**
93325
93485
  * Converts JSX attribute expressions (attribute={expression}) to HTML attributes (attribute="value").
93326
93486
  * Handles style objects (camelCase → kebab-case), className → class, and JSON stringifies objects.
@@ -93336,7 +93496,7 @@ function extractBalancedBraces(content, start) {
93336
93496
  * // Returns: '<a href="https://example.com">Link</a>'
93337
93497
  * ```
93338
93498
  */
93339
- function evaluateAttributeExpressions(content, context) {
93499
+ function evaluateAttributeExpressions(content, context, protectedCode) {
93340
93500
  const attrStartRegex = /(\w+)=\{/g;
93341
93501
  let result = '';
93342
93502
  let lastEnd = 0;
@@ -93346,7 +93506,14 @@ function evaluateAttributeExpressions(content, context) {
93346
93506
  const braceStart = match.index + match[0].length;
93347
93507
  const extracted = extractBalancedBraces(content, braceStart);
93348
93508
  if (extracted) {
93349
- const expression = extracted.content;
93509
+ // The expression might contain template literals in MDX component tag props
93510
+ // E.g. <Component greeting={`Hello World!`} />
93511
+ // that is marked as inline code. So we need to restore the inline codes
93512
+ // in the expression to evaluate it
93513
+ let expression = extracted.content;
93514
+ if (protectedCode) {
93515
+ expression = restoreInlineCode(expression, protectedCode);
93516
+ }
93350
93517
  const fullMatchEnd = extracted.end;
93351
93518
  result += content.slice(lastEnd, match.index);
93352
93519
  try {
@@ -93362,14 +93529,20 @@ function evaluateAttributeExpressions(content, context) {
93362
93529
  result += `style="${cssString}"`;
93363
93530
  }
93364
93531
  else {
93365
- result += `${attributeName}='${JSON.stringify(evalResult)}'`;
93532
+ // These are arrays / objects attribute values
93533
+ // Mark JSON-serialized values with a prefix so they can be parsed back correctly
93534
+ const jsonValue = escapeHtmlAttribute(JSON_VALUE_MARKER + JSON.stringify(evalResult));
93535
+ // Use double quotes so that multi-paragraph values are not split into multiple attributes by the processors
93536
+ result += `${attributeName}="${jsonValue}"`;
93366
93537
  }
93367
93538
  }
93368
93539
  else if (attributeName === 'className') {
93369
- result += `class="${evalResult}"`;
93540
+ // Escape special characters so that it doesn't break and split the attribute value to nodes
93541
+ // This will be restored later in the pipeline
93542
+ result += `class="${escapeHtmlAttribute(String(evalResult))}"`;
93370
93543
  }
93371
93544
  else {
93372
- result += `${attributeName}="${evalResult}"`;
93545
+ result += `${attributeName}="${escapeHtmlAttribute(String(evalResult))}"`;
93373
93546
  }
93374
93547
  }
93375
93548
  catch (_error) {
@@ -93400,13 +93573,9 @@ function evaluateAttributeExpressions(content, context) {
93400
93573
  * // Returns: 'Text with `inline` and ```js\ncode\n```'
93401
93574
  * ```
93402
93575
  */
93403
- function restoreCodeBlocks(content, protectedCode) {
93404
- let restored = content.replace(/___CODE_BLOCK_(\d+)___/g, (_match, index) => {
93405
- return protectedCode.codeBlocks[parseInt(index, 10)];
93406
- });
93407
- restored = restored.replace(/___INLINE_CODE_(\d+)___/g, (_match, index) => {
93408
- return protectedCode.inlineCode[parseInt(index, 10)];
93409
- });
93576
+ function restoreProtectedCodes(content, protectedCode) {
93577
+ let restored = restoreCodeBlocks(content, protectedCode);
93578
+ restored = restoreInlineCode(restored, protectedCode);
93410
93579
  return restored;
93411
93580
  }
93412
93581
  /**
@@ -93427,9 +93596,9 @@ function preprocessJSXExpressions(content, context = {}) {
93427
93596
  // Step 3: Evaluate attribute expressions (JSX attribute syntax: href={baseUrl})
93428
93597
  // For inline expressions, we use a library to parse the expression & evaluate it later
93429
93598
  // For attribute expressions, it was difficult to use a library to parse them, so do it manually
93430
- processed = evaluateAttributeExpressions(processed, context);
93599
+ processed = evaluateAttributeExpressions(processed, context, protectedCode);
93431
93600
  // Step 4: Restore protected code blocks
93432
- processed = restoreCodeBlocks(processed, protectedCode);
93601
+ processed = restoreProtectedCodes(processed, protectedCode);
93433
93602
  return processed;
93434
93603
  }
93435
93604
 
@@ -93841,7 +94010,7 @@ const wrapPinnedBlocks = (node, json) => {
93841
94010
  return node;
93842
94011
  return {
93843
94012
  children: [node],
93844
- data: { className: 'pin', hName: 'rdme-pin' },
94013
+ data: { hName: 'rdme-pin', hProperties: { className: 'pin' } },
93845
94014
  type: 'rdme-pin',
93846
94015
  };
93847
94016
  };
@@ -93929,15 +94098,16 @@ function parseMagicBlock(raw, options = {}) {
93929
94098
  type: 'code',
93930
94099
  value: obj.code.trim(),
93931
94100
  }));
93932
- // Single code block without a tab name renders as a plain code block
94101
+ // Single code block without a tab name (meta or language) renders as a plain code block
94102
+ // Otherwise, we want to render it as a code tabs block
93933
94103
  if (children.length === 1) {
93934
94104
  if (!children[0].value)
93935
94105
  return [];
93936
- if (children[0].meta)
94106
+ if (!(children[0].meta || children[0].lang))
93937
94107
  return [wrapPinnedBlocks(children[0], json)];
93938
94108
  }
93939
- // Multiple code blocks render as tabbed code blocks
93940
- return [wrapPinnedBlocks({ children, className: 'tabs', data: { hName: 'code-tabs' }, type: 'code-tabs' }, json)];
94109
+ // Multiple code blocks or a single code block with a tab name (meta or language) renders as a code tabs block
94110
+ return [wrapPinnedBlocks({ children, className: 'tabs', data: { hName: 'CodeTabs' }, type: 'code-tabs' }, json)];
93941
94111
  }
93942
94112
  // API header: renders as a heading element (h1-h6)
93943
94113
  case 'api-header': {
@@ -94138,11 +94308,31 @@ function parseMagicBlock(raw, options = {}) {
94138
94308
  }
94139
94309
  }
94140
94310
  /**
94141
- * Check if a child node is a flow element that needs unwrapping (mdxJsxFlowElement, etc.)
94311
+ * Block-level node types that cannot be nested inside paragraphs.
94142
94312
  */
94143
- const needsUnwrapping = (child) => {
94144
- return child.type === 'mdxJsxFlowElement';
94145
- };
94313
+ const blockTypes = [
94314
+ 'heading',
94315
+ 'code',
94316
+ 'code-tabs',
94317
+ 'paragraph',
94318
+ 'blockquote',
94319
+ 'list',
94320
+ 'table',
94321
+ 'thematicBreak',
94322
+ 'html',
94323
+ 'yaml',
94324
+ 'toml',
94325
+ 'rdme-pin',
94326
+ 'rdme-callout',
94327
+ 'html-block',
94328
+ 'embed',
94329
+ 'figure',
94330
+ 'mdxJsxFlowElement',
94331
+ ];
94332
+ /**
94333
+ * Check if a node is a block-level node (cannot be inside a paragraph)
94334
+ */
94335
+ const isBlockNode = (node) => blockTypes.includes(node.type);
94146
94336
  /**
94147
94337
  * Unified plugin that restores magic blocks from placeholder tokens.
94148
94338
  *
@@ -94155,8 +94345,9 @@ const magicBlockRestorer = ({ blocks }) => tree => {
94155
94345
  return;
94156
94346
  // Map: key → original raw magic block content
94157
94347
  const magicBlockKeys = new Map(blocks.map(({ key, raw }) => [key, raw]));
94158
- // Find inlineCode nodes that match our placeholder tokens
94159
- const modifications = [];
94348
+ // Collect replacements to apply (we need to visit in reverse to maintain indices)
94349
+ const replacements = [];
94350
+ // First pass: collect all replacements
94160
94351
  visit(tree, 'inlineCode', (node, index, parent) => {
94161
94352
  if (!parent || index == null)
94162
94353
  return undefined;
@@ -94166,31 +94357,73 @@ const magicBlockRestorer = ({ blocks }) => tree => {
94166
94357
  const children = parseMagicBlock(raw);
94167
94358
  if (!children.length)
94168
94359
  return undefined;
94169
- if (children[0] && needsUnwrapping(children[0]) && parent.type === 'paragraph') {
94170
- // Find paragraph's parent and unwrap
94171
- let paragraphParent;
94172
- visit(tree, 'paragraph', (p, pIndex, pParent) => {
94173
- if (p === parent && pParent && 'children' in pParent) {
94174
- paragraphParent = pParent;
94175
- return false;
94360
+ // If parent is a paragraph and we're inserting block nodes (which must not be in paragraphs), lift them out
94361
+ if (parent.type === 'paragraph' && children.some(child => isBlockNode(child))) {
94362
+ const blockNodes = [];
94363
+ const inlineNodes = [];
94364
+ // Separate block and inline nodes
94365
+ children.forEach(child => {
94366
+ if (isBlockNode(child)) {
94367
+ blockNodes.push(child);
94176
94368
  }
94177
- return undefined;
94178
- });
94179
- if (paragraphParent) {
94180
- const paragraphIndex = paragraphParent.children.indexOf(parent);
94181
- if (paragraphIndex !== -1) {
94182
- modifications.push({ children, index: paragraphIndex, parent: paragraphParent });
94369
+ else {
94370
+ inlineNodes.push(child);
94183
94371
  }
94184
- }
94185
- return SKIP;
94372
+ });
94373
+ const before = parent.children.slice(0, index);
94374
+ const after = parent.children.slice(index + 1);
94375
+ replacements.push({
94376
+ parent,
94377
+ blockNodes,
94378
+ inlineNodes,
94379
+ before,
94380
+ after,
94381
+ });
94186
94382
  }
94187
- parent.children.splice(index, 1, ...children);
94188
- return [SKIP, index + children.length];
94189
- });
94190
- // Apply modifications in reverse order to avoid index shifting
94191
- modifications.reverse().forEach(({ children, index, parent }) => {
94192
- parent.children.splice(index, 1, ...children);
94383
+ else {
94384
+ // Normal case: just replace the inlineCode with the children
94385
+ parent.children.splice(index, 1, ...children);
94386
+ }
94387
+ return undefined;
94193
94388
  });
94389
+ // Second pass: apply replacements that require lifting block nodes out of paragraphs
94390
+ // Process in reverse order to maintain correct indices
94391
+ for (let i = replacements.length - 1; i >= 0; i -= 1) {
94392
+ const { after, before, blockNodes, inlineNodes, parent } = replacements[i];
94393
+ // Find the paragraph's position in the root
94394
+ const rootChildren = tree.children;
94395
+ const paraIndex = rootChildren.findIndex(child => child === parent);
94396
+ if (paraIndex === -1) {
94397
+ // Paragraph not found in root - fall back to normal replacement
94398
+ // This shouldn't happen normally, but handle it gracefully
94399
+ // Reconstruct the original index from before.length
94400
+ const originalIndex = before.length;
94401
+ parent.children.splice(originalIndex, 1, ...blockNodes, ...inlineNodes);
94402
+ // eslint-disable-next-line no-continue
94403
+ continue;
94404
+ }
94405
+ // Update or remove the paragraph
94406
+ if (inlineNodes.length > 0) {
94407
+ // Keep paragraph with inline nodes
94408
+ parent.children = [...before, ...inlineNodes, ...after];
94409
+ // Insert block nodes after the paragraph
94410
+ if (blockNodes.length > 0) {
94411
+ rootChildren.splice(paraIndex + 1, 0, ...blockNodes);
94412
+ }
94413
+ }
94414
+ else if (before.length === 0 && after.length === 0) {
94415
+ // Remove empty paragraph and replace with block nodes
94416
+ rootChildren.splice(paraIndex, 1, ...blockNodes);
94417
+ }
94418
+ else {
94419
+ // Keep paragraph with remaining content
94420
+ parent.children = [...before, ...after];
94421
+ // Insert block nodes after the paragraph
94422
+ if (blockNodes.length > 0) {
94423
+ rootChildren.splice(paraIndex + 1, 0, ...blockNodes);
94424
+ }
94425
+ }
94426
+ }
94194
94427
  };
94195
94428
  /* harmony default export */ const mdxish_magic_blocks = (magicBlockRestorer);
94196
94429
 
@@ -94414,6 +94647,92 @@ const normalizeEmphasisAST = () => (tree) => {
94414
94647
  };
94415
94648
  /* harmony default export */ const normalize_malformed_md_syntax = (normalizeEmphasisAST);
94416
94649
 
94650
+ ;// ./processor/transform/mdxish/normalize-table-separator.ts
94651
+ /**
94652
+ * Preprocessor to normalize malformed GFM table separator syntax.
94653
+ *
94654
+ * Fixes the common mistake where the alignment colon is placed after the pipe
94655
+ * instead of before the dashes:
94656
+ *
94657
+ * Invalid: `|: ---` or `|:---` (colon after pipe)
94658
+ * Valid: `| :---` (colon before dashes)
94659
+ *
94660
+ * Also handles right alignment:
94661
+ * Invalid: `| ---:| ` with space before pipe
94662
+ * Valid: `| ---:|` (no space before closing pipe)
94663
+ *
94664
+ * This runs before remark-parse to ensure the table is recognized as a valid GFM table.
94665
+ */
94666
+ /**
94667
+ * Pattern to match a table separator row.
94668
+ * A separator row consists of cells that contain only dashes, colons (for alignment), and spaces.
94669
+ *
94670
+ * Valid GFM separator formats:
94671
+ * - `---` or `----` (no alignment, defaults to left)
94672
+ * - `:---` (left aligned)
94673
+ * - `---:` (right aligned)
94674
+ * - `:---:` (center aligned)
94675
+ *
94676
+ * Invalid formats this fixes:
94677
+ * - `|: ---` → `| :---` (colon wrongly placed after pipe)
94678
+ * - `|:---` → `| :---` (colon directly after pipe, missing space)
94679
+ * - `|::---` or `| ::---` → `| :---` (double colon typo)
94680
+ */
94681
+ // Match a line that looks like a table separator row
94682
+ // This regex captures the whole line if it contains only pipe-separated cells with dashes/colons
94683
+ const TABLE_SEPARATOR_LINE_REGEX = /^(\|[:\s-]+)+\|?\s*$/;
94684
+ // Match malformed left-alignment: `|: ` or `|:` followed by dashes
94685
+ // Captures: group 1 = pipe, group 2 = spaces after colon, group 3 = dashes
94686
+ const MALFORMED_LEFT_ALIGN_REGEX = /\|:(\s*)(-+)/g;
94687
+ // Match malformed double colon: `|::---` or `| ::---` → `| :---`
94688
+ const MALFORMED_DOUBLE_COLON_REGEX = /\|\s*::(\s*)(-+)/g;
94689
+ // Match malformed patterns with spaces before closing colons: `| --- : |` → `| ---: |`
94690
+ const MALFORMED_RIGHT_ALIGN_SPACE_REGEX = /(-+)\s+:(\s*\|)/g;
94691
+ // Match malformed center alignment with spaces: `| : --- : |` → `| :---: |`
94692
+ const MALFORMED_CENTER_ALIGN_REGEX = /\|:(\s+)(-+)(\s+):/g;
94693
+ /**
94694
+ * Normalizes a single table separator line.
94695
+ */
94696
+ function normalizeTableSeparatorLine(line) {
94697
+ // Check if this line looks like a table separator
94698
+ if (!TABLE_SEPARATOR_LINE_REGEX.test(line)) {
94699
+ return line;
94700
+ }
94701
+ let normalized = line;
94702
+ // Fix `|::---` → `| :---` (double colon typo)
94703
+ // Must run before single colon fix to avoid partial replacement
94704
+ normalized = normalized.replace(MALFORMED_DOUBLE_COLON_REGEX, '| :$2');
94705
+ // Fix `|: ---` or `|:---` → `| :---`
94706
+ // The colon should be adjacent to the dashes, not the pipe
94707
+ normalized = normalized.replace(MALFORMED_LEFT_ALIGN_REGEX, '| :$2');
94708
+ // Fix `| --- : |` → `| ---: |`
94709
+ // Remove space before right-alignment colon
94710
+ normalized = normalized.replace(MALFORMED_RIGHT_ALIGN_SPACE_REGEX, '$1:$2');
94711
+ // Fix `| : --- : |` → `| :---: |`
94712
+ // Remove spaces around center-aligned dashes
94713
+ normalized = normalized.replace(MALFORMED_CENTER_ALIGN_REGEX, '| :$2:');
94714
+ return normalized;
94715
+ }
94716
+ /**
94717
+ * Preprocesses markdown content to normalize malformed table separator syntax.
94718
+ *
94719
+ * @param content - The raw markdown content
94720
+ * @returns The content with normalized table separators
94721
+ */
94722
+ function normalizeTableSeparator(content) {
94723
+ const lines = content.split('\n');
94724
+ const normalizedLines = lines.map((line, index) => {
94725
+ const prevLine = index > 0 ? lines[index - 1] : '';
94726
+ const isPrevLineTableRow = prevLine.trim().startsWith('|');
94727
+ if (isPrevLineTableRow) {
94728
+ return normalizeTableSeparatorLine(line);
94729
+ }
94730
+ return line;
94731
+ });
94732
+ return normalizedLines.join('\n');
94733
+ }
94734
+ /* harmony default export */ const normalize_table_separator = ((/* unused pure expression or super */ null && (normalizeTableSeparator)));
94735
+
94417
94736
  ;// ./processor/transform/mdxish/restore-snake-case-component-name.ts
94418
94737
 
94419
94738
 
@@ -94677,6 +94996,7 @@ function loadComponents() {
94677
94996
 
94678
94997
 
94679
94998
 
94999
+
94680
95000
 
94681
95001
 
94682
95002
  const defaultTransformers = [callouts, code_tabs, gemoji_, transform_embeds];
@@ -94689,9 +95009,11 @@ function mdxishAstProcessor(mdContent, opts = {}) {
94689
95009
  // Preprocessing pipeline: Transform content to be parser-ready
94690
95010
  // Step 1: Extract legacy magic blocks
94691
95011
  const { replaced: contentAfterMagicBlocks, blocks } = extractMagicBlocks(mdContent);
94692
- // Step 2: Evaluate JSX expressions in attributes
94693
- const contentAfterJSXEvaluation = preprocessJSXExpressions(contentAfterMagicBlocks, jsxContext);
94694
- // Step 3: Replace snake_case component names with parser-safe placeholders
95012
+ // Step 2: Normalize malformed table separator syntax (e.g., `|: ---` → `| :---`)
95013
+ const contentAfterTableNormalization = normalizeTableSeparator(contentAfterMagicBlocks);
95014
+ // Step 3: Evaluate JSX expressions in attributes
95015
+ const contentAfterJSXEvaluation = preprocessJSXExpressions(contentAfterTableNormalization, jsxContext);
95016
+ // Step 4: Replace snake_case component names with parser-safe placeholders
94695
95017
  // (e.g., <Snake_case /> → <MDXishSnakeCase0 /> which will be restored after parsing)
94696
95018
  const { content: parserReadyContent, mapping: snakeCaseMapping } = processSnakeCaseComponent(contentAfterJSXEvaluation);
94697
95019
  // Create string map for tailwind transformer
@@ -94955,6 +95277,40 @@ const makeUseMDXComponents = (more = {}) => {
94955
95277
 
94956
95278
 
94957
95279
 
95280
+
95281
+ /**
95282
+ * Parse JSON-marked string values in props back to their original types.
95283
+ * This handles arrays and objects that were serialized during JSX preprocessing.
95284
+ */
95285
+ function parseJsonProps(props) {
95286
+ if (!props)
95287
+ return props;
95288
+ const parsed = {};
95289
+ Object.entries(props).forEach(([key, value]) => {
95290
+ if (typeof value === 'string' && value.startsWith(JSON_VALUE_MARKER)) {
95291
+ try {
95292
+ parsed[key] = JSON.parse(value.slice(JSON_VALUE_MARKER.length));
95293
+ }
95294
+ catch {
95295
+ // If parsing fails, use the value without the marker
95296
+ parsed[key] = value.slice(JSON_VALUE_MARKER.length);
95297
+ }
95298
+ }
95299
+ else {
95300
+ parsed[key] = value;
95301
+ }
95302
+ });
95303
+ return parsed;
95304
+ }
95305
+ /**
95306
+ * Custom createElement wrapper that parses JSON-marked string props.
95307
+ * This is needed because rehype-react converts HAST to React, but complex
95308
+ * types (arrays/objects) get serialized to strings during markdown parsing.
95309
+ */
95310
+ function createElementWithJsonProps(type, props, ...children) {
95311
+ const parsedProps = parseJsonProps(props);
95312
+ return external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_default().createElement(type, parsedProps, ...children);
95313
+ }
94958
95314
  /** Flatten CustomComponents into a component map for rehype-react */
94959
95315
  function exportComponentsForRehype(components) {
94960
95316
  const exported = Object.entries(components).reduce((memo, [tag, mod]) => {
@@ -94986,7 +95342,7 @@ function exportComponentsForRehype(components) {
94986
95342
  function createRehypeReactProcessor(components) {
94987
95343
  // @ts-expect-error - rehype-react types are incompatible with React.Fragment return type
94988
95344
  return unified().use((rehype_react_default()), {
94989
- createElement: (external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_default()).createElement,
95345
+ createElement: createElementWithJsonProps,
94990
95346
  Fragment: (external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_default()).Fragment,
94991
95347
  components,
94992
95348
  });
@@ -95295,89 +95651,6 @@ async function stripComments(doc, { mdx } = {}) {
95295
95651
  }
95296
95652
  /* harmony default export */ const lib_stripComments = (stripComments);
95297
95653
 
95298
- ;// ./lib/utils/isPlainText.ts
95299
-
95300
- /**
95301
- * Detects if content contains HTML, magic blocks, or MDX syntax.
95302
- *
95303
- * We can use this in some pipelines to determine if we should have to parse content through
95304
- * `.plain() or if it is already plain text and it should be able to detect everything that would
95305
- * be stripped or sanitized by `.plain()`.
95306
- *
95307
- */
95308
- function isPlainText(content) {
95309
- if (!content || typeof content !== 'string') {
95310
- return true;
95311
- }
95312
- // Exclude markdown code blocks and inline code to avoid false positives
95313
- // Match code blocks with optional language identifier: ```lang\n...\n```
95314
- const codeBlockRegex = /```[^\n]*\n[\s\S]*?```/g;
95315
- // Match inline code: `code` (but not escaped backticks)
95316
- const inlineCodeRegex = /`[^`\n]+`/g;
95317
- // Remove code blocks and inline code to avoid false positives
95318
- let contentWithoutCode = structuredClone(content);
95319
- contentWithoutCode = contentWithoutCode.replace(codeBlockRegex, '');
95320
- contentWithoutCode = contentWithoutCode.replace(inlineCodeRegex, '');
95321
- // Check for magic blocks: `[block:TYPE]...[/block]`
95322
- // Only check after removing code blocks to avoid detecting magic blocks in code
95323
- if (contentWithoutCode.match(MAGIC_BLOCK_REGEX) !== null) {
95324
- return false;
95325
- }
95326
- // Check for markdown links: [text](url) or [text][reference]
95327
- // Pattern matches inline links and reference-style links
95328
- // Exclude images which start with ! before the bracket
95329
- // Only check after removing code blocks
95330
- const markdownLinkPattern = /(?<!!)\[([^\]]+)\]\(([^)]+)\)|(?<!!)\[([^\]]+)\]\[([^\]]*)\]/;
95331
- if (markdownLinkPattern.test(contentWithoutCode)) {
95332
- return false;
95333
- }
95334
- // Check for JSX elements (PascalCase components) in the original content
95335
- // This includes code blocks since JSX code examples should be detected
95336
- // Pattern matches:
95337
- // - Self-closing: <Component /> or <Component/>
95338
- // - With attributes: <Component prop="value" />
95339
- // - With children: <Component>...</Component>
95340
- // Use simpler, safer patterns to avoid ReDoS from backtracking
95341
- // Match self-closing tags with bounded attribute length to prevent excessive backtracking
95342
- const jsxSelfClosingPattern = /<[A-Z][a-zA-Z0-9]*(?:\s[^>]{0,50})?\/>/;
95343
- if (jsxSelfClosingPattern.test(content)) {
95344
- return false;
95345
- }
95346
- // For components with children, use a safer pattern that limits backtracking
95347
- // Match opening tag with bounded attributes, then look for closing tag with same name
95348
- const jsxWithChildrenPattern = /<([A-Z][a-zA-Z0-9]*)(?:\s[^>]{0,50})?>[\s\S]{0,50}<\/\1>/;
95349
- if (jsxWithChildrenPattern.test(content)) {
95350
- return false;
95351
- }
95352
- // Check for MDX expressions and HTML tags in the original content
95353
- // HTML/JSX/MDX in code blocks should be detected (as per test requirements)
95354
- // But exclude inline code that contains magic block patterns to avoid false positives
95355
- let contentForHtmlMdx = content;
95356
- // Find inline code blocks and check if they contain [block: pattern
95357
- // Exclude these from HTML/MDX detection to avoid false positives
95358
- const inlineCodePattern = /`[^`\n]+`/g;
95359
- let inlineCodeMatch;
95360
- inlineCodePattern.lastIndex = 0;
95361
- while ((inlineCodeMatch = inlineCodePattern.exec(content)) !== null) {
95362
- if (inlineCodeMatch[0].includes('[block:')) {
95363
- contentForHtmlMdx = contentForHtmlMdx.replace(inlineCodeMatch[0], '');
95364
- }
95365
- }
95366
- // Match simple MDX variable expressions like {variable}, {user.name}, {getValue()}, {}
95367
- // Use bounded quantifier to prevent ReDoS - limit to reasonable variable name length
95368
- // Allow empty braces {} to be detected as well
95369
- const jsxExpressionPattern = /\{[^}"]{0,50}\}/;
95370
- if (jsxExpressionPattern.test(contentForHtmlMdx)) {
95371
- return false;
95372
- }
95373
- // Match HTML tags with bounded attribute length to prevent ReDoS
95374
- const htmlTagPattern = /<[a-z][a-z0-9]*(?:\s[^>]{0,50})?(?:\/>|>)/i;
95375
- if (htmlTagPattern.test(contentForHtmlMdx)) {
95376
- return false;
95377
- }
95378
- return true;
95379
- }
95380
-
95381
95654
  ;// ./lib/index.ts
95382
95655
 
95383
95656
 
@@ -95396,7 +95669,6 @@ function isPlainText(content) {
95396
95669
 
95397
95670
 
95398
95671
 
95399
-
95400
95672
  ;// ./index.tsx
95401
95673
 
95402
95674