@readme/markdown 12.2.0 → 13.1.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.node.js CHANGED
@@ -24514,7 +24514,9 @@ const CreateHeading = (depth) => {
24514
24514
  ;// ./components/Image/index.tsx
24515
24515
 
24516
24516
  const Image = (Props) => {
24517
- const { align = '', alt = '', border = false, caption, className = '', height = 'auto', src, title = '', width = 'auto', lazy = true, children, } = Props;
24517
+ const { align = '', alt = '', border: borderProp = false, caption, className = '', height = 'auto', src, title = '', width = 'auto', lazy = true, children, } = Props;
24518
+ // Normalize border: MDXish passes {false} as the string "false", not a boolean
24519
+ const border = borderProp === true || borderProp === 'true';
24518
24520
  const [lightbox, setLightbox] = external_react_.useState(false);
24519
24521
  if (className === 'emoji') {
24520
24522
  return external_react_.createElement("img", { alt: alt, height: height, loading: lazy ? 'lazy' : 'eager', src: src, title: title, width: width });
@@ -26898,7 +26900,7 @@ function htmlExtension(all, extension) {
26898
26900
 
26899
26901
  ;// ./node_modules/micromark-util-character/index.js
26900
26902
  /**
26901
- * @typedef {import('micromark-util-types').Code} Code
26903
+ * @import {Code} from 'micromark-util-types'
26902
26904
  */
26903
26905
 
26904
26906
  /**
@@ -27124,7 +27126,9 @@ const unicodeWhitespace = regexCheck(/\s/);
27124
27126
  * Create a code check from a regex.
27125
27127
  *
27126
27128
  * @param {RegExp} regex
27129
+ * Expression.
27127
27130
  * @returns {(code: Code) => boolean}
27131
+ * Check.
27128
27132
  */
27129
27133
  function regexCheck(regex) {
27130
27134
  return check;
@@ -73302,6 +73306,59 @@ const isCalloutStructure = (node) => {
73302
73306
  const firstTextChild = firstChild.children?.[0];
73303
73307
  return firstTextChild?.type === 'text';
73304
73308
  };
73309
+ /**
73310
+ * Finds the first text node containing a newline in a paragraph's children.
73311
+ * Returns the index and the newline position within that text node.
73312
+ */
73313
+ const findNewlineInParagraph = (paragraph) => {
73314
+ for (let i = 0; i < paragraph.children.length; i += 1) {
73315
+ const child = paragraph.children[i];
73316
+ if (child.type === 'text' && typeof child.value === 'string') {
73317
+ const newlineIndex = child.value.indexOf('\n');
73318
+ if (newlineIndex !== -1) {
73319
+ return { index: i, newlineIndex };
73320
+ }
73321
+ }
73322
+ }
73323
+ return null;
73324
+ };
73325
+ /**
73326
+ * Splits a paragraph at the first newline, separating heading content (before \n)
73327
+ * from body content (after \n). Mutates the paragraph to contain only heading children.
73328
+ */
73329
+ const splitParagraphAtNewline = (paragraph) => {
73330
+ const splitPoint = findNewlineInParagraph(paragraph);
73331
+ if (!splitPoint)
73332
+ return null;
73333
+ const { index, newlineIndex } = splitPoint;
73334
+ const originalChildren = paragraph.children;
73335
+ const textNode = originalChildren[index];
73336
+ const beforeNewline = textNode.value.slice(0, newlineIndex);
73337
+ const afterNewline = textNode.value.slice(newlineIndex + 1);
73338
+ // Split paragraph: heading = children[0..index-1] + text before newline
73339
+ const headingChildren = originalChildren.slice(0, index);
73340
+ if (beforeNewline.length > 0 || headingChildren.length === 0) {
73341
+ headingChildren.push({ type: 'text', value: beforeNewline });
73342
+ }
73343
+ paragraph.children = headingChildren;
73344
+ // Body = text after newline + remaining children from original array
73345
+ const bodyChildren = [];
73346
+ if (afterNewline.length > 0) {
73347
+ bodyChildren.push({ type: 'text', value: afterNewline });
73348
+ }
73349
+ bodyChildren.push(...originalChildren.slice(index + 1));
73350
+ return bodyChildren.length > 0 ? bodyChildren : null;
73351
+ };
73352
+ /**
73353
+ * Removes the icon/match prefix from the first text node in a paragraph.
73354
+ * This is needed to clean up the raw AST after we've extracted the icon.
73355
+ */
73356
+ const removeIconPrefix = (paragraph, prefixLength) => {
73357
+ const firstTextNode = findFirst(paragraph);
73358
+ if (firstTextNode && 'value' in firstTextNode && typeof firstTextNode.value === 'string') {
73359
+ firstTextNode.value = firstTextNode.value.slice(prefixLength);
73360
+ }
73361
+ };
73305
73362
  const processBlockquote = (node, index, parent) => {
73306
73363
  if (!isCalloutStructure(node)) {
73307
73364
  // Only stringify empty blockquotes (no extractable text content)
@@ -73326,22 +73383,39 @@ const processBlockquote = (node, index, parent) => {
73326
73383
  const firstParagraph = node.children[0];
73327
73384
  const startText = lib_plain(firstParagraph).toString();
73328
73385
  const [match, icon] = startText.match(callouts_regex) || [];
73386
+ const firstParagraphOriginalEnd = firstParagraph.position.end;
73329
73387
  if (icon && match) {
73330
- const heading = startText.slice(match.length);
73331
- const empty = !heading.length && firstParagraph.children.length === 1;
73388
+ // Handle cases where heading and body are on the same line separated by a newline.
73389
+ // Example: "> ⚠️ **Bold heading**\nBody text here"
73390
+ const bodyChildren = splitParagraphAtNewline(firstParagraph);
73391
+ const didSplit = bodyChildren !== null;
73392
+ // Extract heading text after removing the icon prefix.
73393
+ // Use `plain()` to handle complex markdown structures (bold, inline code, etc.)
73394
+ const headingText = lib_plain(firstParagraph)
73395
+ .toString()
73396
+ .slice(match.length);
73397
+ // Clean up the raw AST by removing the icon prefix from the first text node
73398
+ removeIconPrefix(firstParagraph, match.length);
73399
+ const empty = !headingText.length && firstParagraph.children.length === 1;
73332
73400
  const theme = themes[icon] || 'default';
73333
- const firstChild = findFirst(node.children[0]);
73334
- if (firstChild && 'value' in firstChild && typeof firstChild.value === 'string') {
73335
- firstChild.value = firstChild.value.slice(match.length);
73336
- }
73337
- if (heading) {
73401
+ // Convert the first paragraph (first children of node) to a heading if it has content or was split
73402
+ if (headingText || didSplit) {
73338
73403
  node.children[0] = wrapHeading(node);
73339
- // @note: We add to the offset/column the length of the unicode
73340
- // character that was stripped off, so that the start position of the
73341
- // heading/text matches where it actually starts.
73404
+ // Adjust position to account for the stripped icon prefix
73342
73405
  node.children[0].position.start.offset += match.length;
73343
73406
  node.children[0].position.start.column += match.length;
73344
73407
  }
73408
+ // Insert body content as a separate paragraph after the heading
73409
+ if (bodyChildren) {
73410
+ node.children.splice(1, 0, {
73411
+ type: 'paragraph',
73412
+ children: bodyChildren,
73413
+ position: {
73414
+ start: node.children[0].position.end,
73415
+ end: firstParagraphOriginalEnd,
73416
+ },
73417
+ });
73418
+ }
73345
73419
  Object.assign(node, {
73346
73420
  type: NodeTypes.callout,
73347
73421
  data: {
@@ -90721,7 +90795,7 @@ const toAttributes = (object, keys = []) => {
90721
90795
  if (keys.length > 0 && !keys.includes(name))
90722
90796
  return;
90723
90797
  let value;
90724
- if (typeof v === 'undefined' || v === null || v === '') {
90798
+ if (typeof v === 'undefined' || v === null || v === '' || v === false) {
90725
90799
  return;
90726
90800
  }
90727
90801
  else if (typeof v === 'string') {
@@ -106756,7 +106830,7 @@ const CUSTOM_PROP_BOUNDARIES = [
106756
106830
  /**
106757
106831
  * Tags that should be passed through and handled at runtime (not by the mdxish plugin)
106758
106832
  */
106759
- const RUNTIME_COMPONENT_TAGS = new Set(['Variable', 'variable', 'rdme-pin']);
106833
+ const RUNTIME_COMPONENT_TAGS = new Set(['Variable', 'variable', 'html-block', 'rdme-pin']);
106760
106834
  /**
106761
106835
  * Standard HTML tags that should never be treated as custom components.
106762
106836
  * Uses the html-tags package, converted to a Set<string> for efficient lookups.
@@ -111869,6 +111943,10 @@ ${reformatHTML(html)}
111869
111943
  const plain_plain = (node) => node.value;
111870
111944
  /* harmony default export */ const compile_plain = (plain_plain);
111871
111945
 
111946
+ ;// ./processor/compile/variable.ts
111947
+ const variable = (node) => `{user.${node.data?.hProperties?.name || ''}}`;
111948
+ /* harmony default export */ const compile_variable = (variable);
111949
+
111872
111950
  ;// ./processor/compile/index.ts
111873
111951
 
111874
111952
 
@@ -111878,7 +111956,8 @@ const plain_plain = (node) => node.value;
111878
111956
 
111879
111957
 
111880
111958
 
111881
- function compilers() {
111959
+
111960
+ function compilers(mdxish = false) {
111882
111961
  const data = this.data();
111883
111962
  const toMarkdownExtensions = data.toMarkdownExtensions || (data.toMarkdownExtensions = []);
111884
111963
  const handlers = {
@@ -111889,6 +111968,7 @@ function compilers() {
111889
111968
  [NodeTypes.glossary]: compile_compatibility,
111890
111969
  [NodeTypes.htmlBlock]: html_block,
111891
111970
  [NodeTypes.reusableContent]: compile_compatibility,
111971
+ ...(mdxish && { [NodeTypes.variable]: compile_variable }),
111892
111972
  embed: compile_compatibility,
111893
111973
  escape: compile_compatibility,
111894
111974
  figure: compile_compatibility,
@@ -111899,6 +111979,9 @@ function compilers() {
111899
111979
  };
111900
111980
  toMarkdownExtensions.push({ extensions: [{ handlers }] });
111901
111981
  }
111982
+ function mdxishCompilers() {
111983
+ return compilers.call(this, true);
111984
+ }
111902
111985
  /* harmony default export */ const processor_compile = (compilers);
111903
111986
 
111904
111987
  ;// ./processor/transform/escape-pipes-in-tables.ts
@@ -113609,13 +113692,17 @@ const htmlBlockHandler = (_state, node) => {
113609
113692
  const embedHandler = (state, node) => {
113610
113693
  // Assert to get the minimum properties we need
113611
113694
  const { data } = node;
113695
+ // Magic block embeds (hName === 'embed-block') render as Embed component
113696
+ // which doesn't use children - it renders based on props only
113697
+ const isMagicBlockEmbed = data?.hName === NodeTypes.embedBlock;
113612
113698
  return {
113613
113699
  type: 'element',
113614
113700
  // To differentiate between regular embeds and magic block embeds,
113615
113701
  // magic block embeds have a certain hName
113616
- tagName: data?.hName === NodeTypes.embedBlock ? 'Embed' : 'embed',
113702
+ tagName: isMagicBlockEmbed ? 'Embed' : 'embed',
113617
113703
  properties: data?.hProperties,
113618
- children: state.all(node),
113704
+ // Don't include children for magic block embeds - Embed component renders based on props
113705
+ children: isMagicBlockEmbed ? [] : state.all(node),
113619
113706
  };
113620
113707
  };
113621
113708
  const mdxComponentHandlers = {
@@ -113628,7 +113715,102 @@ const mdxComponentHandlers = {
113628
113715
  [NodeTypes.htmlBlock]: htmlBlockHandler,
113629
113716
  };
113630
113717
 
113718
+ ;// ./lib/utils/mdxish/protect-code-blocks.ts
113719
+ /**
113720
+ * Replaces code blocks and inline code with placeholders to protect them from preprocessing.
113721
+ *
113722
+ * @param content - The markdown content to process
113723
+ * @returns Object containing protected content and arrays of original code blocks
113724
+ * @example
113725
+ * ```typescript
113726
+ * const input = 'Text with `inline code` and ```fenced block```';
113727
+ * protectCodeBlocks(input)
113728
+ * // Returns: {
113729
+ * // protectedCode: {
113730
+ * // codeBlocks: ['```fenced block```'],
113731
+ * // inlineCode: ['`inline code`']
113732
+ * // },
113733
+ * // protectedContent: 'Text with ___INLINE_CODE_0___ and ___CODE_BLOCK_0___'
113734
+ * // }
113735
+ * ```
113736
+ */
113737
+ function protectCodeBlocks(content) {
113738
+ const codeBlocks = [];
113739
+ const inlineCode = [];
113740
+ let protectedContent = '';
113741
+ let remaining = content;
113742
+ let codeBlockStart = remaining.indexOf('```');
113743
+ while (codeBlockStart !== -1) {
113744
+ protectedContent += remaining.slice(0, codeBlockStart);
113745
+ remaining = remaining.slice(codeBlockStart);
113746
+ const codeBlockEnd = remaining.indexOf('```', 3);
113747
+ if (codeBlockEnd === -1) {
113748
+ break;
113749
+ }
113750
+ const match = remaining.slice(0, codeBlockEnd + 3);
113751
+ const index = codeBlocks.length;
113752
+ codeBlocks.push(match);
113753
+ protectedContent += `___CODE_BLOCK_${index}___`;
113754
+ remaining = remaining.slice(codeBlockEnd + 3);
113755
+ codeBlockStart = remaining.indexOf('```');
113756
+ }
113757
+ protectedContent += remaining;
113758
+ protectedContent = protectedContent.replace(/`([^`\n]+)`/g, match => {
113759
+ const index = inlineCode.length;
113760
+ inlineCode.push(match);
113761
+ return `___INLINE_CODE_${index}___`;
113762
+ });
113763
+ return { protectedCode: { codeBlocks, inlineCode }, protectedContent };
113764
+ }
113765
+ /**
113766
+ * Restores inline code by replacing placeholders with original content.
113767
+ *
113768
+ * @param content - Content with inline code placeholders
113769
+ * @param protectedCode - The protected code arrays
113770
+ * @returns Content with inline code restored
113771
+ */
113772
+ function restoreInlineCode(content, protectedCode) {
113773
+ return content.replace(/___INLINE_CODE_(\d+)___/g, (_m, idx) => {
113774
+ return protectedCode.inlineCode[parseInt(idx, 10)];
113775
+ });
113776
+ }
113777
+ /**
113778
+ * Restores fenced code blocks by replacing placeholders with original content.
113779
+ *
113780
+ * @param content - Content with code block placeholders
113781
+ * @param protectedCode - The protected code arrays
113782
+ * @returns Content with code blocks restored
113783
+ */
113784
+ function restoreFencedCodeBlocks(content, protectedCode) {
113785
+ return content.replace(/___CODE_BLOCK_(\d+)___/g, (_m, idx) => {
113786
+ return protectedCode.codeBlocks[parseInt(idx, 10)];
113787
+ });
113788
+ }
113789
+ /**
113790
+ * Restores all code blocks and inline code by replacing placeholders with original content.
113791
+ *
113792
+ * @param content - Content with code placeholders
113793
+ * @param protectedCode - The protected code arrays
113794
+ * @returns Content with all code blocks and inline code restored
113795
+ * @example
113796
+ * ```typescript
113797
+ * const content = 'Text with ___INLINE_CODE_0___ and ___CODE_BLOCK_0___';
113798
+ * const protectedCode = {
113799
+ * codeBlocks: ['```js\ncode\n```'],
113800
+ * inlineCode: ['`inline`']
113801
+ * };
113802
+ * restoreCodeBlocks(content, protectedCode)
113803
+ * // Returns: 'Text with `inline` and ```js\ncode\n```'
113804
+ * ```
113805
+ */
113806
+ function restoreCodeBlocks(content, protectedCode) {
113807
+ let restored = restoreFencedCodeBlocks(content, protectedCode);
113808
+ restored = restoreInlineCode(restored, protectedCode);
113809
+ return restored;
113810
+ }
113811
+
113631
113812
  ;// ./processor/transform/mdxish/preprocess-jsx-expressions.ts
113813
+
113632
113814
  // Base64 encode (Node.js + browser compatible)
113633
113815
  function base64Encode(str) {
113634
113816
  if (typeof Buffer !== 'undefined') {
@@ -113695,52 +113877,6 @@ function protectHTMLBlockContent(content) {
113695
113877
  return `${openTag}${HTML_BLOCK_CONTENT_START}${encoded}${HTML_BLOCK_CONTENT_END}${closeTag}`;
113696
113878
  });
113697
113879
  }
113698
- /**
113699
- * Replaces code blocks and inline code with placeholders to protect them from JSX processing.
113700
- *
113701
- * @param content
113702
- * @returns Object containing protected content and arrays of original code blocks
113703
- * @example
113704
- * ```typescript
113705
- * const input = 'Text with `inline code` and ```fenced block```';
113706
- * protectCodeBlocks(input)
113707
- * // Returns: {
113708
- * // protectedCode: {
113709
- * // codeBlocks: ['```fenced block```'],
113710
- * // inlineCode: ['`inline code`']
113711
- * // },
113712
- * // protectedContent: 'Text with ___INLINE_CODE_0___ and ___CODE_BLOCK_0___'
113713
- * // }
113714
- * ```
113715
- */
113716
- function protectCodeBlocks(content) {
113717
- const codeBlocks = [];
113718
- const inlineCode = [];
113719
- let protectedContent = '';
113720
- let remaining = content;
113721
- let codeBlockStart = remaining.indexOf('```');
113722
- while (codeBlockStart !== -1) {
113723
- protectedContent += remaining.slice(0, codeBlockStart);
113724
- remaining = remaining.slice(codeBlockStart);
113725
- const codeBlockEnd = remaining.indexOf('```', 3);
113726
- if (codeBlockEnd === -1) {
113727
- break;
113728
- }
113729
- const match = remaining.slice(0, codeBlockEnd + 3);
113730
- const index = codeBlocks.length;
113731
- codeBlocks.push(match);
113732
- protectedContent += `___CODE_BLOCK_${index}___`;
113733
- remaining = remaining.slice(codeBlockEnd + 3);
113734
- codeBlockStart = remaining.indexOf('```');
113735
- }
113736
- protectedContent += remaining;
113737
- protectedContent = protectedContent.replace(/`[^`]+`/g, match => {
113738
- const index = inlineCode.length;
113739
- inlineCode.push(match);
113740
- return `___INLINE_CODE_${index}___`;
113741
- });
113742
- return { protectedCode: { codeBlocks, inlineCode }, protectedContent };
113743
- }
113744
113880
  /**
113745
113881
  * Removes JSX-style comments (e.g., { /* comment *\/ }) from content.
113746
113882
  *
@@ -113783,16 +113919,6 @@ function extractBalancedBraces(content, start) {
113783
113919
  return null;
113784
113920
  return { content: content.slice(start, pos - 1), end: pos };
113785
113921
  }
113786
- function restoreInlineCode(content, protectedCode) {
113787
- return content.replace(/___INLINE_CODE_(\d+)___/g, (_m, idx) => {
113788
- return protectedCode.inlineCode[parseInt(idx, 10)];
113789
- });
113790
- }
113791
- function restoreCodeBlocks(content, protectedCode) {
113792
- return content.replace(/___CODE_BLOCK_(\d+)___/g, (_m, idx) => {
113793
- return protectedCode.codeBlocks[parseInt(idx, 10)];
113794
- });
113795
- }
113796
113922
  /**
113797
113923
  * Escapes unbalanced braces in content to prevent MDX expression parsing errors.
113798
113924
  * Handles: already-escaped braces, string literals inside expressions, nested balanced braces.
@@ -113923,28 +114049,6 @@ function evaluateAttributeExpressions(content, context, protectedCode) {
113923
114049
  result += content.slice(lastEnd);
113924
114050
  return result;
113925
114051
  }
113926
- /**
113927
- * Restores code blocks and inline code by replacing placeholders with original content.
113928
- *
113929
- * @param content
113930
- * @param protectedCode
113931
- * @returns Content with all code blocks and inline code restored
113932
- * @example
113933
- * ```typescript
113934
- * const content = 'Text with ___INLINE_CODE_0___ and ___CODE_BLOCK_0___';
113935
- * const protectedCode = {
113936
- * codeBlocks: ['```js\ncode\n```'],
113937
- * inlineCode: ['`inline`']
113938
- * };
113939
- * restoreCodeBlocks(content, protectedCode)
113940
- * // Returns: 'Text with `inline` and ```js\ncode\n```'
113941
- * ```
113942
- */
113943
- function restoreProtectedCodes(content, protectedCode) {
113944
- let restored = restoreCodeBlocks(content, protectedCode);
113945
- restored = restoreInlineCode(restored, protectedCode);
113946
- return restored;
113947
- }
113948
114052
  /**
113949
114053
  * Preprocesses JSX-like expressions in markdown before parsing.
113950
114054
  * Inline expressions are handled separately; attribute expressions are processed here.
@@ -113967,7 +114071,7 @@ function preprocessJSXExpressions(content, context = {}) {
113967
114071
  // Step 4: Escape unbalanced braces to prevent MDX expression parsing errors
113968
114072
  processed = escapeUnbalancedBraces(processed);
113969
114073
  // Step 5: Restore protected code blocks
113970
- processed = restoreProtectedCodes(processed, protectedCode);
114074
+ processed = restoreCodeBlocks(processed, protectedCode);
113971
114075
  return processed;
113972
114076
  }
113973
114077
 
@@ -114022,360 +114126,407 @@ const evaluateExpressions = ({ context = {} } = {}) => tree => {
114022
114126
  };
114023
114127
  /* harmony default export */ const evaluate_expressions = (evaluateExpressions);
114024
114128
 
114025
- ;// ./processor/transform/mdxish/mdxish-html-blocks.ts
114026
-
114027
-
114028
-
114129
+ ;// ./processor/transform/mdxish/normalize-malformed-md-syntax.ts
114029
114130
 
114030
- /**
114031
- * Decodes HTMLBlock content that was protected during preprocessing.
114032
- * Content is wrapped in <!--RDMX_HTMLBLOCK:base64:RDMX_HTMLBLOCK-->
114033
- */
114034
- function decodeProtectedContent(content) {
114035
- // Escape special regex characters in the markers
114036
- const startEscaped = HTML_BLOCK_CONTENT_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
114037
- const endEscaped = HTML_BLOCK_CONTENT_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
114038
- const markerRegex = new RegExp(`${startEscaped}([A-Za-z0-9+/=]+)${endEscaped}`, 'g');
114039
- return content.replace(markerRegex, (_match, encoded) => {
114040
- try {
114041
- return base64Decode(encoded);
114131
+ // Marker patterns for multi-node emphasis detection
114132
+ const MARKER_PATTERNS = [
114133
+ { isBold: true, marker: '**' },
114134
+ { isBold: true, marker: '__' },
114135
+ { isBold: false, marker: '*' },
114136
+ { isBold: false, marker: '_' },
114137
+ ];
114138
+ // Patterns to detect for bold (** and __) and italic (* and _) syntax:
114139
+ // Bold: ** text**, **text **, word** text**, ** text **
114140
+ // Italic: * text*, *text *, word* text*, * text *
114141
+ // Same patterns for underscore variants
114142
+ // We use separate patterns for each marker type to allow this flexibility.
114143
+ // Pattern for ** bold **
114144
+ // Groups: 1=wordBefore, 2=marker, 3=contentWithSpaceAfter, 4=trailingSpace1, 5=contentWithSpaceBefore, 6=trailingSpace2, 7=afterChar
114145
+ // trailingSpace1 is for "** text **" pattern, trailingSpace2 is for "**text **" pattern
114146
+ const asteriskBoldRegex = /([^*\s]+)?\s*(\*\*)(?:\s+((?:[^*\n]|\*(?!\*))+?)(\s*)\2|((?:[^*\n]|\*(?!\*))+?)(\s+)\2)(\S|$)?/g;
114147
+ // Pattern for __ bold __
114148
+ const underscoreBoldRegex = /([^_\s]+)?\s*(__)(?:\s+((?:[^_\n]|_(?!_))+?)(\s*)\2|((?:[^_\n]|_(?!_))+?)(\s+)\2)(\S|$)?/g;
114149
+ // Pattern for * italic *
114150
+ const asteriskItalicRegex = /([^*\s]+)?\s*(\*)(?!\*)(?:\s+([^*\n]+?)(\s*)\2|([^*\n]+?)(\s+)\2)(\S|$)?/g;
114151
+ // Pattern for _ italic _
114152
+ const underscoreItalicRegex = /([^_\s]+)?\s*(_)(?!_)(?:\s+([^_\n]+?)(\s*)\2|([^_\n]+?)(\s+)\2)(\S|$)?/g;
114153
+ // CommonMark ignores intraword underscores or asteriks, but we want to italicize/bold the inner part
114154
+ // Pattern for intraword _word_ in words like hello_world_
114155
+ const intrawordUnderscoreItalicRegex = /(\w)_(?!_)([a-zA-Z0-9]+)_(?![\w_])/g;
114156
+ // Pattern for intraword __word__ in words like hello__world__
114157
+ const intrawordUnderscoreBoldRegex = /(\w)__([a-zA-Z0-9]+)__(?![\w_])/g;
114158
+ // Pattern for intraword *word* in words like hello*world*
114159
+ const intrawordAsteriskItalicRegex = /(\w)\*(?!\*)([a-zA-Z0-9]+)\*(?![\w*])/g;
114160
+ // Pattern for intraword **word** in words like hello**world**
114161
+ const intrawordAsteriskBoldRegex = /(\w)\*\*([a-zA-Z0-9]+)\*\*(?![\w*])/g;
114162
+ /**
114163
+ * Finds opening emphasis marker in a text value.
114164
+ * Returns marker info if found, null otherwise.
114165
+ */
114166
+ function findOpeningMarker(text) {
114167
+ const results = MARKER_PATTERNS.map(({ isBold, marker }) => {
114168
+ if (marker === '*' && text.startsWith('**'))
114169
+ return null;
114170
+ if (marker === '_' && text.startsWith('__'))
114171
+ return null;
114172
+ if (text.startsWith(marker) && text.length > marker.length) {
114173
+ return { isBold, marker, textAfter: text.slice(marker.length), textBefore: '' };
114042
114174
  }
114043
- catch {
114044
- return encoded;
114175
+ const idx = text.indexOf(marker);
114176
+ if (idx > 0 && !/\s/.test(text[idx - 1])) {
114177
+ if (marker === '*' && text.slice(idx).startsWith('**'))
114178
+ return null;
114179
+ if (marker === '_' && text.slice(idx).startsWith('__'))
114180
+ return null;
114181
+ const after = text.slice(idx + marker.length);
114182
+ if (after.length > 0) {
114183
+ return { isBold, marker, textAfter: after, textBefore: text.slice(0, idx) };
114184
+ }
114045
114185
  }
114186
+ return null;
114046
114187
  });
114188
+ return results.find(r => r !== null) ?? null;
114047
114189
  }
114048
114190
  /**
114049
- * Collects text content from a node and its children recursively
114191
+ * Finds the end/closing marker in a text node for multi-node emphasis.
114050
114192
  */
114051
- function collectTextContent(node) {
114052
- const parts = [];
114053
- if (node.type === 'text' && node.value) {
114054
- parts.push(node.value);
114055
- }
114056
- else if (node.type === 'html' && node.value) {
114057
- parts.push(node.value);
114058
- }
114059
- else if (node.type === 'inlineCode' && node.value) {
114060
- parts.push(node.value);
114061
- }
114062
- else if (node.type === 'code' && node.value) {
114063
- // Reconstruct code fence syntax (markdown parser consumes opening ```)
114064
- const lang = node.lang || '';
114065
- const fence = `\`\`\`${lang ? `${lang}\n` : ''}`;
114066
- parts.push(fence);
114067
- parts.push(node.value);
114068
- // Add newline before closing fence if missing
114069
- const closingFence = node.value.endsWith('\n') ? '```' : '\n```';
114070
- parts.push(closingFence);
114193
+ function findEndMarker(text, marker) {
114194
+ const spacePattern = ` ${marker}`;
114195
+ const spaceIdx = text.indexOf(spacePattern);
114196
+ if (spaceIdx >= 0) {
114197
+ if (marker === '*' && text.slice(spaceIdx + 1).startsWith('**'))
114198
+ return null;
114199
+ if (marker === '_' && text.slice(spaceIdx + 1).startsWith('__'))
114200
+ return null;
114201
+ return {
114202
+ textAfter: text.slice(spaceIdx + spacePattern.length),
114203
+ textBefore: text.slice(0, spaceIdx),
114204
+ };
114071
114205
  }
114072
- else if (node.children && Array.isArray(node.children)) {
114073
- node.children.forEach(child => {
114074
- if (typeof child === 'object' && child !== null) {
114075
- parts.push(collectTextContent(child));
114076
- }
114077
- });
114206
+ if (text.startsWith(marker)) {
114207
+ if (marker === '*' && text.startsWith('**'))
114208
+ return null;
114209
+ if (marker === '_' && text.startsWith('__'))
114210
+ return null;
114211
+ return {
114212
+ textAfter: text.slice(marker.length),
114213
+ textBefore: '',
114214
+ };
114078
114215
  }
114079
- return parts.join('');
114216
+ return null;
114080
114217
  }
114081
114218
  /**
114082
- * Extracts boolean attribute from HTML tag. Handles JSX (safeMode={true}) and string (safeMode="true") syntax.
114083
- * Returns "true"/"false" string to survive rehypeRaw serialization.
114219
+ * Scan children for an opening emphasis marker in a text node.
114084
114220
  */
114085
- function extractBooleanAttr(attrs, name) {
114086
- // Try JSX syntax: name={true|false}
114087
- const jsxMatch = attrs.match(new RegExp(`${name}=\\{(true|false)\\}`));
114088
- if (jsxMatch) {
114089
- return jsxMatch[1];
114090
- }
114091
- // Try string syntax: name="true"|true
114092
- const stringMatch = attrs.match(new RegExp(`${name}="?(true|false)"?`));
114093
- if (stringMatch) {
114094
- return stringMatch[1];
114095
- }
114096
- return undefined;
114221
+ function findOpeningInChildren(children) {
114222
+ let result = null;
114223
+ children.some((child, idx) => {
114224
+ if (child.type !== 'text')
114225
+ return false;
114226
+ const found = findOpeningMarker(child.value);
114227
+ if (found) {
114228
+ result = { idx, opening: found };
114229
+ return true;
114230
+ }
114231
+ return false;
114232
+ });
114233
+ return result;
114097
114234
  }
114098
114235
  /**
114099
- * Extracts runScripts attribute from HTML tag. Returns boolean for "true"/"false", string for other values, or undefined if not found.
114236
+ * Scan children (after openingIdx) for a closing emphasis marker.
114100
114237
  */
114101
- function extractRunScriptsAttr(attrs) {
114102
- const runScriptsMatch = attrs.match(/runScripts="?([^">\s]+)"?/);
114103
- if (!runScriptsMatch) {
114104
- return undefined;
114105
- }
114106
- const value = runScriptsMatch[1];
114107
- if (value === 'true') {
114108
- return true;
114109
- }
114110
- if (value === 'false') {
114238
+ function findClosingInChildren(children, openingIdx, marker) {
114239
+ let result = null;
114240
+ children.slice(openingIdx + 1).some((child, relativeIdx) => {
114241
+ if (child.type !== 'text')
114242
+ return false;
114243
+ const found = findEndMarker(child.value, marker);
114244
+ if (found) {
114245
+ result = { closingIdx: openingIdx + 1 + relativeIdx, closing: found };
114246
+ return true;
114247
+ }
114111
114248
  return false;
114112
- }
114113
- return value;
114249
+ });
114250
+ return result;
114114
114251
  }
114115
114252
  /**
114116
- * Creates an HTMLBlock node from HTML string and optional attributes
114253
+ * Build the replacement nodes for a matched emphasis pair.
114117
114254
  */
114118
- function createHTMLBlockNode(htmlString, position, runScripts, safeMode) {
114119
- return {
114120
- position,
114121
- children: [{ type: 'text', value: htmlString }],
114122
- type: NodeTypes.htmlBlock,
114123
- data: {
114124
- hName: 'html-block',
114125
- hProperties: {
114126
- html: htmlString,
114127
- ...(runScripts !== undefined && { runScripts }),
114128
- ...(safeMode !== undefined && { safeMode }),
114129
- },
114130
- },
114131
- };
114255
+ function buildReplacementNodes(container, { opening, openingIdx, closing, closingIdx }) {
114256
+ const newNodes = [];
114257
+ if (opening.textBefore) {
114258
+ newNodes.push({ type: 'text', value: `${opening.textBefore} ` });
114259
+ }
114260
+ const emphasisChildren = [];
114261
+ const openingText = opening.textAfter.replace(/^\s+/, '');
114262
+ if (openingText) {
114263
+ emphasisChildren.push({ type: 'text', value: openingText });
114264
+ }
114265
+ container.children.slice(openingIdx + 1, closingIdx).forEach(child => {
114266
+ emphasisChildren.push(child);
114267
+ });
114268
+ const closingText = closing.textBefore.replace(/\s+$/, '');
114269
+ if (closingText) {
114270
+ emphasisChildren.push({ type: 'text', value: closingText });
114271
+ }
114272
+ if (emphasisChildren.length > 0) {
114273
+ const emphasisNode = opening.isBold
114274
+ ? { type: 'strong', children: emphasisChildren }
114275
+ : { type: 'emphasis', children: emphasisChildren };
114276
+ newNodes.push(emphasisNode);
114277
+ }
114278
+ if (closing.textAfter) {
114279
+ newNodes.push({ type: 'text', value: closing.textAfter });
114280
+ }
114281
+ return newNodes;
114132
114282
  }
114133
114283
  /**
114134
- * Checks for opening tag only (for split detection)
114284
+ * Find and transform one multi-node emphasis pair in the container.
114285
+ * Returns true if a pair was found and transformed, false otherwise.
114135
114286
  */
114136
- function hasOpeningTagOnly(node) {
114137
- let hasOpening = false;
114138
- let hasClosed = false;
114139
- let attrs = '';
114140
- const check = (n) => {
114141
- if (n.type === 'html' && n.value) {
114142
- if (n.value === '<HTMLBlock>') {
114143
- hasOpening = true;
114144
- }
114145
- else {
114146
- const match = n.value.match(/^<HTMLBlock(\s[^>]*)?>$/);
114147
- if (match) {
114148
- hasOpening = true;
114149
- attrs = match[1] || '';
114150
- }
114151
- }
114152
- if (n.value === '</HTMLBlock>' || n.value.includes('</HTMLBlock>')) {
114153
- hasClosed = true;
114154
- }
114155
- }
114156
- if (n.children && Array.isArray(n.children)) {
114157
- n.children.forEach(child => {
114158
- check(child);
114159
- });
114160
- }
114161
- };
114162
- check(node);
114163
- // Return true only if opening without closing (split case)
114164
- return { attrs, found: hasOpening && !hasClosed };
114287
+ function processOneEmphasisPair(container) {
114288
+ const openingResult = findOpeningInChildren(container.children);
114289
+ if (!openingResult)
114290
+ return false;
114291
+ const { idx: openingIdx, opening } = openingResult;
114292
+ const closingResult = findClosingInChildren(container.children, openingIdx, opening.marker);
114293
+ if (!closingResult)
114294
+ return false;
114295
+ const { closingIdx, closing } = closingResult;
114296
+ const newNodes = buildReplacementNodes(container, { opening, openingIdx, closing, closingIdx });
114297
+ const deleteCount = closingIdx - openingIdx + 1;
114298
+ container.children.splice(openingIdx, deleteCount, ...newNodes);
114299
+ return true;
114165
114300
  }
114166
114301
  /**
114167
- * Checks if a node contains an HTMLBlock closing tag
114302
+ * Handle malformed emphasis that spans multiple AST nodes.
114303
+ * E.g., "**bold [link](url)**" where markers are in different text nodes.
114168
114304
  */
114169
- function hasClosingTag(node) {
114170
- if (node.type === 'html' && node.value) {
114171
- if (node.value === '</HTMLBlock>' || node.value.includes('</HTMLBlock>'))
114172
- return true;
114173
- }
114174
- if (node.children && Array.isArray(node.children)) {
114175
- return node.children.some(child => hasClosingTag(child));
114176
- }
114177
- return false;
114305
+ function visitMultiNodeEmphasis(tree) {
114306
+ const containerTypes = ['paragraph', 'heading', 'tableCell', 'listItem', 'blockquote'];
114307
+ visit(tree, node => {
114308
+ if (!containerTypes.includes(node.type))
114309
+ return;
114310
+ if (!('children' in node) || !Array.isArray(node.children))
114311
+ return;
114312
+ const container = node;
114313
+ let foundPair = true;
114314
+ while (foundPair) {
114315
+ foundPair = processOneEmphasisPair(container);
114316
+ }
114317
+ });
114178
114318
  }
114179
114319
  /**
114180
- * Transforms HTMLBlock MDX JSX to html-block nodes. Handles <HTMLBlock>{`...`}</HTMLBlock> syntax.
114320
+ * A remark plugin that normalizes malformed bold and italic markers in text nodes.
114321
+ * Detects patterns like `** bold**`, `Hello** Wrong Bold**`, `__ bold__`, `Hello__ Wrong Bold__`,
114322
+ * `* italic*`, `Hello* Wrong Italic*`, `_ italic_`, or `Hello_ Wrong Italic_`
114323
+ * and converts them to proper strong/emphasis nodes, matching the behavior of the legacy rdmd engine.
114324
+ *
114325
+ * Supports both asterisk (`**bold**`, `*italic*`) and underscore (`__bold__`, `_italic_`) syntax.
114326
+ * Also supports snake_case content like `** some_snake_case**`.
114327
+ *
114328
+ * This runs after remark-parse, which (in v11+) is strict and doesn't parse
114329
+ * malformed emphasis syntax. This plugin post-processes the AST to handle these cases.
114181
114330
  */
114182
- const mdxishHtmlBlocks = () => tree => {
114183
- // Handle HTMLBlock split across root children (caused by newlines)
114184
- visit(tree, 'root', (root) => {
114185
- const children = root.children;
114186
- let i = 0;
114187
- while (i < children.length) {
114188
- const child = children[i];
114189
- const { attrs, found: hasOpening } = hasOpeningTagOnly(child);
114190
- if (hasOpening) {
114191
- // Find closing tag in subsequent siblings
114192
- let closingIdx = -1;
114193
- for (let j = i + 1; j < children.length; j += 1) {
114194
- if (hasClosingTag(children[j])) {
114195
- closingIdx = j;
114196
- break;
114197
- }
114331
+ const normalizeEmphasisAST = () => (tree) => {
114332
+ visit(tree, 'text', function visitor(node, index, parent) {
114333
+ if (index === undefined || !parent)
114334
+ return undefined;
114335
+ // Skip if inside code blocks or inline code
114336
+ if (parent.type === 'inlineCode' || parent.type === 'code') {
114337
+ return undefined;
114338
+ }
114339
+ const text = node.value;
114340
+ const allMatches = [];
114341
+ [...text.matchAll(asteriskBoldRegex)].forEach(match => {
114342
+ allMatches.push({ isBold: true, marker: '**', match });
114343
+ });
114344
+ [...text.matchAll(underscoreBoldRegex)].forEach(match => {
114345
+ allMatches.push({ isBold: true, marker: '__', match });
114346
+ });
114347
+ [...text.matchAll(asteriskItalicRegex)].forEach(match => {
114348
+ allMatches.push({ isBold: false, marker: '*', match });
114349
+ });
114350
+ [...text.matchAll(underscoreItalicRegex)].forEach(match => {
114351
+ allMatches.push({ isBold: false, marker: '_', match });
114352
+ });
114353
+ [...text.matchAll(intrawordUnderscoreItalicRegex)].forEach(match => {
114354
+ allMatches.push({ isBold: false, isIntraword: true, marker: '_', match });
114355
+ });
114356
+ [...text.matchAll(intrawordUnderscoreBoldRegex)].forEach(match => {
114357
+ allMatches.push({ isBold: true, isIntraword: true, marker: '__', match });
114358
+ });
114359
+ [...text.matchAll(intrawordAsteriskItalicRegex)].forEach(match => {
114360
+ allMatches.push({ isBold: false, isIntraword: true, marker: '*', match });
114361
+ });
114362
+ [...text.matchAll(intrawordAsteriskBoldRegex)].forEach(match => {
114363
+ allMatches.push({ isBold: true, isIntraword: true, marker: '**', match });
114364
+ });
114365
+ if (allMatches.length === 0)
114366
+ return undefined;
114367
+ allMatches.sort((a, b) => (a.match.index ?? 0) - (b.match.index ?? 0));
114368
+ const filteredMatches = [];
114369
+ let lastEnd = 0;
114370
+ allMatches.forEach(info => {
114371
+ const start = info.match.index ?? 0;
114372
+ const end = start + info.match[0].length;
114373
+ if (start >= lastEnd) {
114374
+ filteredMatches.push(info);
114375
+ lastEnd = end;
114376
+ }
114377
+ });
114378
+ if (filteredMatches.length === 0)
114379
+ return undefined;
114380
+ const parts = [];
114381
+ let lastIndex = 0;
114382
+ filteredMatches.forEach(({ isBold, isIntraword, marker, match }) => {
114383
+ const matchIndex = match.index ?? 0;
114384
+ const fullMatch = match[0];
114385
+ if (isIntraword) {
114386
+ // handles cases like hello_world_ where we only want to italicize 'world'
114387
+ const charBefore = match[1] || ''; // e.g., "l" in "hello_world_"
114388
+ const content = match[2]; // e.g., "world"
114389
+ const combinedBefore = text.slice(lastIndex, matchIndex) + charBefore;
114390
+ if (combinedBefore) {
114391
+ parts.push({ type: 'text', value: combinedBefore });
114198
114392
  }
114199
- if (closingIdx !== -1) {
114200
- // Collect inner content between tags
114201
- const contentParts = [];
114202
- for (let j = i; j <= closingIdx; j += 1) {
114203
- const node = children[j];
114204
- contentParts.push(collectTextContent(node));
114205
- }
114206
- // Remove the opening/closing tags and template literal syntax from content
114207
- let content = contentParts.join('');
114208
- content = content.replace(/^<HTMLBlock[^>]*>\s*\{?\s*`?/, '').replace(/`?\s*\}?\s*<\/HTMLBlock>$/, '');
114209
- // Decode protected content that was base64 encoded during preprocessing
114210
- content = decodeProtectedContent(content);
114211
- const htmlString = formatHtmlForMdxish(content);
114212
- const runScripts = extractRunScriptsAttr(attrs);
114213
- const safeMode = extractBooleanAttr(attrs, 'safeMode');
114214
- // Replace range with single HTMLBlock node
114215
- const mdNode = createHTMLBlockNode(htmlString, children[i].position, runScripts, safeMode);
114216
- root.children.splice(i, closingIdx - i + 1, mdNode);
114393
+ if (isBold) {
114394
+ parts.push({
114395
+ type: 'strong',
114396
+ children: [{ type: 'text', value: content }],
114397
+ });
114217
114398
  }
114399
+ else {
114400
+ parts.push({
114401
+ type: 'emphasis',
114402
+ children: [{ type: 'text', value: content }],
114403
+ });
114404
+ }
114405
+ lastIndex = matchIndex + fullMatch.length;
114406
+ return;
114218
114407
  }
114219
- i += 1;
114220
- }
114221
- });
114222
- // Handle HTMLBlock parsed as HTML elements (when template literal contains block-level HTML tags)
114223
- visit(tree, 'html', (node, index, parent) => {
114224
- if (!parent || index === undefined)
114225
- return;
114226
- const value = node.value;
114227
- if (!value)
114228
- return;
114229
- // Case 1: Full HTMLBlock in single node
114230
- const fullMatch = value.match(/^<HTMLBlock(\s[^>]*)?>([\s\S]*)<\/HTMLBlock>$/);
114231
- if (fullMatch) {
114232
- const attrs = fullMatch[1] || '';
114233
- let content = fullMatch[2] || '';
114234
- // Remove template literal syntax if present: {`...`}
114235
- content = content.replace(/^\s*\{\s*`/, '').replace(/`\s*\}\s*$/, '');
114236
- // Decode protected content that was base64 encoded during preprocessing
114237
- content = decodeProtectedContent(content);
114238
- const htmlString = formatHtmlForMdxish(content);
114239
- const runScripts = extractRunScriptsAttr(attrs);
114240
- const safeMode = extractBooleanAttr(attrs, 'safeMode');
114241
- parent.children[index] = createHTMLBlockNode(htmlString, node.position, runScripts, safeMode);
114242
- return;
114243
- }
114244
- // Case 2: Opening tag only (split by blank lines)
114245
- if (value === '<HTMLBlock>' || value.match(/^<HTMLBlock\s[^>]*>$/)) {
114246
- const siblings = parent.children;
114247
- let closingIdx = -1;
114248
- // Find closing tag in siblings
114249
- for (let i = index + 1; i < siblings.length; i += 1) {
114250
- const sibling = siblings[i];
114251
- if (sibling.type === 'html') {
114252
- const sibVal = sibling.value;
114253
- if (sibVal === '</HTMLBlock>' || sibVal?.includes('</HTMLBlock>')) {
114254
- closingIdx = i;
114255
- break;
114256
- }
114408
+ if (matchIndex > lastIndex) {
114409
+ const beforeText = text.slice(lastIndex, matchIndex);
114410
+ if (beforeText) {
114411
+ parts.push({ type: 'text', value: beforeText });
114257
114412
  }
114258
114413
  }
114259
- if (closingIdx === -1)
114260
- return;
114261
- // Collect content between tags, skipping template literal delimiters
114262
- const contentParts = [];
114263
- for (let i = index + 1; i < closingIdx; i += 1) {
114264
- const sibling = siblings[i];
114265
- // Skip template literal delimiters
114266
- if (sibling.type === 'text') {
114267
- const textVal = sibling.value;
114268
- if (textVal === '{' || textVal === '}' || textVal === '{`' || textVal === '`}') {
114269
- // eslint-disable-next-line no-continue
114270
- continue;
114271
- }
114272
- }
114273
- contentParts.push(collectTextContent(sibling));
114414
+ const wordBefore = match[1]; // e.g., "Hello" in "Hello** Wrong Bold**"
114415
+ const contentWithSpaceAfter = match[3]; // Content when there's a space after opening markers
114416
+ const trailingSpace1 = match[4] || ''; // Space before closing markers (for "** text **" pattern)
114417
+ const contentWithSpaceBefore = match[5]; // Content when there's only a space before closing markers
114418
+ const trailingSpace2 = match[6] || ''; // Space before closing markers (for "**text **" pattern)
114419
+ const trailingSpace = trailingSpace1 || trailingSpace2; // Combined trailing space
114420
+ const content = (contentWithSpaceAfter || contentWithSpaceBefore || '').trim();
114421
+ const afterChar = match[7]; // Character after closing markers (if any)
114422
+ const markerPos = fullMatch.indexOf(marker);
114423
+ const spacesBeforeMarkers = wordBefore
114424
+ ? fullMatch.slice(wordBefore.length, markerPos)
114425
+ : fullMatch.slice(0, markerPos);
114426
+ const shouldAddSpace = !!contentWithSpaceAfter && !!wordBefore && !spacesBeforeMarkers;
114427
+ if (wordBefore) {
114428
+ const spacing = spacesBeforeMarkers + (shouldAddSpace ? ' ' : '');
114429
+ parts.push({ type: 'text', value: wordBefore + spacing });
114274
114430
  }
114275
- // Decode protected content that was base64 encoded during preprocessing
114276
- const decodedContent = decodeProtectedContent(contentParts.join(''));
114277
- const htmlString = formatHtmlForMdxish(decodedContent);
114278
- const runScripts = extractRunScriptsAttr(value);
114279
- const safeMode = extractBooleanAttr(value, 'safeMode');
114280
- // Replace opening tag with HTMLBlock node, remove consumed siblings
114281
- parent.children[index] = createHTMLBlockNode(htmlString, node.position, runScripts, safeMode);
114282
- parent.children.splice(index + 1, closingIdx - index);
114283
- }
114284
- });
114285
- // Handle HTMLBlock inside paragraphs (parsed as inline elements)
114286
- visit(tree, 'paragraph', (node, index, parent) => {
114287
- if (!parent || index === undefined)
114288
- return;
114289
- const children = node.children || [];
114290
- let htmlBlockStartIdx = -1;
114291
- let htmlBlockEndIdx = -1;
114292
- let templateLiteralStartIdx = -1;
114293
- let templateLiteralEndIdx = -1;
114294
- for (let i = 0; i < children.length; i += 1) {
114295
- const child = children[i];
114296
- if (child.type === 'html' && typeof child.value === 'string') {
114297
- const value = child.value;
114298
- if (value === '<HTMLBlock>' || value.match(/^<HTMLBlock\s[^>]*>$/)) {
114299
- htmlBlockStartIdx = i;
114300
- }
114301
- else if (value === '</HTMLBlock>') {
114302
- htmlBlockEndIdx = i;
114303
- }
114431
+ else if (spacesBeforeMarkers) {
114432
+ parts.push({ type: 'text', value: spacesBeforeMarkers });
114304
114433
  }
114305
- // Find opening brace after HTMLBlock start
114306
- if (htmlBlockStartIdx !== -1 && templateLiteralStartIdx === -1 && child.type === 'text') {
114307
- const value = child.value;
114308
- if (value === '{') {
114309
- templateLiteralStartIdx = i;
114434
+ if (content) {
114435
+ if (isBold) {
114436
+ parts.push({
114437
+ type: 'strong',
114438
+ children: [{ type: 'text', value: content }],
114439
+ });
114310
114440
  }
114311
- }
114312
- // Find closing brace before HTMLBlock end
114313
- if (htmlBlockStartIdx !== -1 && htmlBlockEndIdx === -1 && child.type === 'text') {
114314
- const value = child.value;
114315
- if (value === '}') {
114316
- templateLiteralEndIdx = i;
114441
+ else {
114442
+ parts.push({
114443
+ type: 'emphasis',
114444
+ children: [{ type: 'text', value: content }],
114445
+ });
114317
114446
  }
114318
114447
  }
114319
- }
114320
- if (htmlBlockStartIdx !== -1 &&
114321
- htmlBlockEndIdx !== -1 &&
114322
- templateLiteralStartIdx !== -1 &&
114323
- templateLiteralEndIdx !== -1 &&
114324
- templateLiteralStartIdx < templateLiteralEndIdx) {
114325
- const openingTag = children[htmlBlockStartIdx];
114326
- // Collect content between braces (handles code blocks)
114327
- const templateContent = [];
114328
- for (let i = templateLiteralStartIdx + 1; i < templateLiteralEndIdx; i += 1) {
114329
- const child = children[i];
114330
- templateContent.push(collectTextContent(child));
114448
+ if (afterChar) {
114449
+ const prefix = trailingSpace ? ' ' : '';
114450
+ parts.push({ type: 'text', value: prefix + afterChar });
114451
+ }
114452
+ lastIndex = matchIndex + fullMatch.length;
114453
+ });
114454
+ if (lastIndex < text.length) {
114455
+ const remainingText = text.slice(lastIndex);
114456
+ if (remainingText) {
114457
+ parts.push({ type: 'text', value: remainingText });
114331
114458
  }
114332
- // Decode protected content that was base64 encoded during preprocessing
114333
- const decodedContent = decodeProtectedContent(templateContent.join(''));
114334
- const htmlString = formatHtmlForMdxish(decodedContent);
114335
- const runScripts = openingTag.value ? extractRunScriptsAttr(openingTag.value) : undefined;
114336
- const safeMode = openingTag.value ? extractBooleanAttr(openingTag.value, 'safeMode') : undefined;
114337
- const mdNode = createHTMLBlockNode(htmlString, node.position, runScripts, safeMode);
114338
- parent.children[index] = mdNode;
114339
114459
  }
114340
- });
114341
- // Ensure html-block nodes have HTML in children as text node
114342
- visit(tree, 'html-block', (node) => {
114343
- const html = node.data?.hProperties?.html;
114344
- if (html &&
114345
- (!node.children ||
114346
- node.children.length === 0 ||
114347
- (node.children.length === 1 && node.children[0].type === 'text' && node.children[0].value !== html))) {
114348
- node.children = [
114349
- {
114350
- type: 'text',
114351
- value: html,
114352
- },
114353
- ];
114460
+ if (parts.length > 0) {
114461
+ parent.children.splice(index, 1, ...parts);
114462
+ return [SKIP, index + parts.length];
114354
114463
  }
114464
+ return undefined;
114355
114465
  });
114466
+ // Handle malformed emphasis spanning multiple nodes (e.g., **text [link](url) **)
114467
+ visitMultiNodeEmphasis(tree);
114356
114468
  return tree;
114357
114469
  };
114358
- /* harmony default export */ const mdxish_html_blocks = (mdxishHtmlBlocks);
114470
+ /* harmony default export */ const normalize_malformed_md_syntax = (normalizeEmphasisAST);
114471
+
114472
+ ;// ./processor/transform/mdxish/magic-blocks/placeholder.ts
114473
+ const EMPTY_IMAGE_PLACEHOLDER = {
114474
+ type: 'image',
114475
+ url: '',
114476
+ alt: '',
114477
+ title: '',
114478
+ data: { hProperties: {} },
114479
+ };
114480
+ const EMPTY_EMBED_PLACEHOLDER = {
114481
+ type: 'embed',
114482
+ children: [{ type: 'link', url: '', title: '', children: [{ type: 'text', value: '' }] }],
114483
+ data: { hName: 'embed-block', hProperties: { url: '', href: '', title: '' } },
114484
+ };
114485
+ const EMPTY_RECIPE_PLACEHOLDER = {
114486
+ type: 'mdxJsxFlowElement',
114487
+ name: 'Recipe',
114488
+ attributes: [],
114489
+ children: [],
114490
+ };
114491
+ const EMPTY_CALLOUT_PLACEHOLDER = {
114492
+ type: 'mdxJsxFlowElement',
114493
+ name: 'Callout',
114494
+ attributes: [
114495
+ { type: 'mdxJsxAttribute', name: 'icon', value: '📘' },
114496
+ { type: 'mdxJsxAttribute', name: 'theme', value: 'info' },
114497
+ { type: 'mdxJsxAttribute', name: 'type', value: 'info' },
114498
+ { type: 'mdxJsxAttribute', name: 'empty', value: 'true' },
114499
+ ],
114500
+ children: [{ type: 'heading', depth: 3, children: [{ type: 'text', value: '' }] }],
114501
+ };
114502
+ const EMPTY_TABLE_PLACEHOLDER = {
114503
+ type: 'table',
114504
+ align: ['left', 'left'],
114505
+ children: [
114506
+ { type: 'tableRow', children: [{ type: 'tableCell', children: [{ type: 'text', value: '' }] }] },
114507
+ { type: 'tableRow', children: [{ type: 'tableCell', children: [{ type: 'text', value: '' }] }] },
114508
+ ],
114509
+ };
114510
+ const EMPTY_CODE_PLACEHOLDER = {
114511
+ type: 'code',
114512
+ value: '',
114513
+ lang: null,
114514
+ meta: null,
114515
+ };
114516
+
114517
+ ;// ./processor/transform/mdxish/magic-blocks/magic-block-transformer.ts
114518
+
114359
114519
 
114360
- ;// ./processor/transform/mdxish/mdxish-magic-blocks.ts
114361
114520
 
114362
114521
 
114363
114522
 
114364
114523
 
114365
114524
 
114366
114525
  /**
114367
- * Matches legacy magic block syntax: [block:TYPE]...JSON...[/block]
114368
- * Group 1: block type (e.g., "image", "code", "callout")
114369
- * Group 2: JSON content between the tags
114370
- * Taken from the v6 branch
114371
- */
114372
- const RGXP = /^\s*\[block:([^\]]*)\]([^]+?)\[\/block\]/;
114373
- /**
114374
- * Wraps a node in a "pinned" container if sidebar: true is set in the JSON.
114375
- * Pinned blocks are displayed in a sidebar/floating position in the UI.
114526
+ * Wraps a node in a "pinned" container if sidebar: true is set.
114376
114527
  */
114377
- const wrapPinnedBlocks = (node, json) => {
114378
- if (!json.sidebar)
114528
+ const wrapPinnedBlocks = (node, data) => {
114529
+ if (!data.sidebar)
114379
114530
  return node;
114380
114531
  return {
114381
114532
  children: [node],
@@ -114391,34 +114542,24 @@ const imgSizeValues = {
114391
114542
  original: 'auto',
114392
114543
  };
114393
114544
  /**
114394
- * Proxy that resolves image sizing values:
114395
- * - "full" → "100%", "original" → "auto" (from imgSizeValues)
114396
- * - Pure numbers like "50" → "50%" (percentage)
114397
- * - Anything else passes through as-is (e.g., "200px")
114545
+ * Proxy that resolves image sizing values.
114398
114546
  */
114399
114547
  const imgWidthBySize = new Proxy(imgSizeValues, {
114400
114548
  get: (widths, size) => (size?.match(/^\d+$/) ? `${size}%` : size in widths ? widths[size] : size),
114401
114549
  });
114402
- // Simple text to inline nodes (just returns text node - no markdown parsing)
114403
114550
  const textToInline = (text) => [{ type: 'text', value: text }];
114404
- // Simple text to block nodes (wraps in paragraph)
114405
114551
  const textToBlock = (text) => [{ children: textToInline(text), type: 'paragraph' }];
114406
114552
  /** Parses markdown and html to markdown nodes */
114407
- const contentParser = unified().use(remarkParse).use(remarkGfm);
114408
- // Table cells may contain html or markdown content, so we need to parse it accordingly instead of keeping it as raw text
114553
+ const contentParser = unified().use(remarkParse).use(remarkGfm).use(normalize_malformed_md_syntax);
114409
114554
  const parseTableCell = (text) => {
114410
114555
  if (!text.trim())
114411
114556
  return [{ type: 'text', value: '' }];
114412
114557
  const tree = contentParser.runSync(contentParser.parse(text));
114413
- // If there are multiple block-level nodes, keep them as-is to preserve the document structure and spacing
114414
114558
  if (tree.children.length > 1) {
114415
114559
  return tree.children;
114416
114560
  }
114417
- return tree.children.flatMap(n =>
114418
- // This unwraps the extra p node that might appear & wrapping the content
114419
- n.type === 'paragraph' && 'children' in n ? n.children : [n]);
114561
+ return tree.children.flatMap(n => n.type === 'paragraph' && 'children' in n ? n.children : [n]);
114420
114562
  };
114421
- // Parse markdown/HTML into block-level nodes (preserves paragraphs, headings, lists, etc.)
114422
114563
  const parseBlock = (text) => {
114423
114564
  if (!text.trim())
114424
114565
  return [{ type: 'paragraph', children: [{ type: 'text', value: '' }] }];
@@ -114426,44 +114567,43 @@ const parseBlock = (text) => {
114426
114567
  return tree.children;
114427
114568
  };
114428
114569
  /**
114429
- * Parse a magic block string and return MDAST nodes.
114430
- *
114431
- * @param raw - The raw magic block string including [block:TYPE] and [/block] tags
114432
- * @param options - Parsing options for compatibility and error handling
114433
- * @returns Array of MDAST nodes representing the parsed block
114434
- */
114435
- function parseMagicBlock(raw, options = {}) {
114436
- const { alwaysThrow = false, compatibilityMode = false, safeMode = false } = options;
114437
- const matchResult = RGXP.exec(raw);
114438
- if (!matchResult)
114439
- return [];
114440
- const [, rawType, jsonStr] = matchResult;
114441
- const type = rawType?.trim();
114442
- if (!type)
114443
- return [];
114444
- let json;
114445
- try {
114446
- json = JSON.parse(jsonStr);
114570
+ * Transform a magicBlock node into final MDAST nodes.
114571
+ */
114572
+ function transformMagicBlock(blockType, data, rawValue, options = {}) {
114573
+ const { compatibilityMode = false, safeMode = false } = options;
114574
+ // Handle empty data by returning placeholder nodes for known block types
114575
+ // This allows the editor to show appropriate placeholder UI instead of nothing
114576
+ if (Object.keys(data).length < 1) {
114577
+ switch (blockType) {
114578
+ case 'image':
114579
+ return [EMPTY_IMAGE_PLACEHOLDER];
114580
+ case 'embed':
114581
+ return [EMPTY_EMBED_PLACEHOLDER];
114582
+ case 'code':
114583
+ return [EMPTY_CODE_PLACEHOLDER];
114584
+ case 'callout':
114585
+ return [EMPTY_CALLOUT_PLACEHOLDER];
114586
+ case 'parameters':
114587
+ case 'table':
114588
+ return [EMPTY_TABLE_PLACEHOLDER];
114589
+ case 'recipe':
114590
+ case 'tutorial-tile':
114591
+ return [EMPTY_RECIPE_PLACEHOLDER];
114592
+ default:
114593
+ return [{ type: 'paragraph', children: [{ type: 'text', value: rawValue }] }];
114594
+ }
114447
114595
  }
114448
- catch (err) {
114449
- // eslint-disable-next-line no-console
114450
- console.error('Invalid Magic Block JSON:', err);
114451
- if (alwaysThrow)
114452
- throw new Error('Invalid Magic Block JSON');
114453
- return [];
114454
- }
114455
- if (Object.keys(json).length < 1)
114456
- return [];
114457
- // Each case handles a different magic block type and returns appropriate MDAST nodes
114458
- switch (type) {
114459
- // Code blocks: single code block or tabbed code blocks (multiple languages)
114596
+ switch (blockType) {
114460
114597
  case 'code': {
114461
- const codeJson = json;
114598
+ const codeJson = data;
114599
+ if (!codeJson.codes || !Array.isArray(codeJson.codes)) {
114600
+ return [wrapPinnedBlocks(EMPTY_CODE_PLACEHOLDER, data)];
114601
+ }
114462
114602
  const children = codeJson.codes.map(obj => ({
114463
114603
  className: 'tab-panel',
114464
114604
  data: { hName: 'code', hProperties: { lang: obj.language, meta: obj.name || null } },
114465
114605
  lang: obj.language,
114466
- meta: obj.name || null, // Tab name shown in the UI
114606
+ meta: obj.name || null,
114467
114607
  type: 'code',
114468
114608
  value: obj.code.trim(),
114469
114609
  }));
@@ -114473,31 +114613,31 @@ function parseMagicBlock(raw, options = {}) {
114473
114613
  if (!children[0].value)
114474
114614
  return [];
114475
114615
  if (!(children[0].meta || children[0].lang))
114476
- return [wrapPinnedBlocks(children[0], json)];
114616
+ return [wrapPinnedBlocks(children[0], data)];
114477
114617
  }
114478
114618
  // Multiple code blocks or a single code block with a tab name (meta or language) renders as a code tabs block
114479
- return [wrapPinnedBlocks({ children, className: 'tabs', data: { hName: 'CodeTabs' }, type: 'code-tabs' }, json)];
114619
+ return [wrapPinnedBlocks({ children, className: 'tabs', data: { hName: 'CodeTabs' }, type: 'code-tabs' }, data)];
114480
114620
  }
114481
- // API header: renders as a heading element (h1-h6)
114482
114621
  case 'api-header': {
114483
- const headerJson = json;
114484
- // In compatibility mode, default to h1; otherwise h2
114622
+ const headerJson = data;
114485
114623
  const depth = headerJson.level || (compatibilityMode ? 1 : 2);
114486
114624
  return [
114487
114625
  wrapPinnedBlocks({
114488
114626
  children: 'title' in headerJson ? textToInline(headerJson.title || '') : [],
114489
114627
  depth,
114490
114628
  type: 'heading',
114491
- }, json),
114629
+ }, data),
114492
114630
  ];
114493
114631
  }
114494
- // Image block: renders as <img> or <figure> with caption
114495
114632
  case 'image': {
114496
- const imageJson = json;
114633
+ const imageJson = data;
114634
+ if (!imageJson.images || !Array.isArray(imageJson.images)) {
114635
+ return [wrapPinnedBlocks(EMPTY_IMAGE_PLACEHOLDER, data)];
114636
+ }
114497
114637
  const imgData = imageJson.images.find(i => i.image);
114498
- if (!imgData?.image)
114499
- return [];
114500
- // Image array format: [url, title?, alt?]
114638
+ if (!imgData?.image) {
114639
+ return [wrapPinnedBlocks(EMPTY_IMAGE_PLACEHOLDER, data)];
114640
+ }
114501
114641
  const [url, title, alt] = imgData.image;
114502
114642
  const block = {
114503
114643
  alt: alt || imgData.caption || '',
@@ -114512,57 +114652,66 @@ function parseMagicBlock(raw, options = {}) {
114512
114652
  type: 'image',
114513
114653
  url,
114514
114654
  };
114515
- // Wrap in <figure> if caption is present
114516
114655
  const img = imgData.caption
114517
114656
  ? {
114518
114657
  children: [
114519
114658
  block,
114520
- { children: textToBlock(imgData.caption), data: { hName: 'figcaption' }, type: 'figcaption' },
114659
+ { children: parseBlock(imgData.caption), data: { hName: 'figcaption' }, type: 'figcaption' },
114521
114660
  ],
114522
114661
  data: { hName: 'figure' },
114523
114662
  type: 'figure',
114524
114663
  url,
114525
114664
  }
114526
114665
  : block;
114527
- return [wrapPinnedBlocks(img, json)];
114666
+ return [wrapPinnedBlocks(img, data)];
114528
114667
  }
114529
- // Callout: info/warning/error boxes with icon and theme
114530
114668
  case 'callout': {
114531
- const calloutJson = json;
114532
- // Preset callout types map to [icon, theme] tuples
114669
+ const calloutJson = data;
114533
114670
  const types = {
114534
114671
  danger: ['❗️', 'error'],
114535
114672
  info: ['📘', 'info'],
114536
114673
  success: ['👍', 'okay'],
114537
114674
  warning: ['🚧', 'warn'],
114538
114675
  };
114539
- // Resolve type to [icon, theme] - use preset if available, otherwise custom
114540
114676
  const resolvedType = typeof calloutJson.type === 'string' && calloutJson.type in types
114541
114677
  ? types[calloutJson.type]
114542
114678
  : [calloutJson.icon || '👍', typeof calloutJson.type === 'string' ? calloutJson.type : 'default'];
114543
114679
  const [icon, theme] = Array.isArray(resolvedType) ? resolvedType : ['👍', 'default'];
114544
114680
  if (!(calloutJson.title || calloutJson.body))
114545
114681
  return [];
114546
- // Parses html & markdown content
114547
- const titleBlocks = parseBlock(calloutJson.title || '');
114548
- const bodyBlocks = parseBlock(calloutJson.body || '');
114682
+ const hasTitle = !!calloutJson.title?.trim();
114683
+ const hasBody = !!calloutJson.body?.trim();
114684
+ const empty = !hasTitle;
114549
114685
  const children = [];
114550
- if (titleBlocks.length > 0 && titleBlocks[0].type === 'paragraph') {
114551
- const firstTitle = titleBlocks[0];
114552
- const heading = {
114686
+ if (hasTitle) {
114687
+ const titleBlocks = parseBlock(calloutJson.title || '');
114688
+ if (titleBlocks.length > 0 && titleBlocks[0].type === 'paragraph') {
114689
+ const firstTitle = titleBlocks[0];
114690
+ const heading = {
114691
+ type: 'heading',
114692
+ depth: 3,
114693
+ children: (firstTitle.children || []),
114694
+ };
114695
+ children.push(heading);
114696
+ children.push(...titleBlocks.slice(1));
114697
+ }
114698
+ else {
114699
+ children.push(...titleBlocks);
114700
+ }
114701
+ }
114702
+ else {
114703
+ // Add empty heading placeholder so body goes to children.slice(1)
114704
+ // The Callout component expects children[0] to be the heading
114705
+ children.push({
114553
114706
  type: 'heading',
114554
114707
  depth: 3,
114555
- children: (firstTitle.children || []),
114556
- };
114557
- children.push(heading);
114558
- children.push(...titleBlocks.slice(1), ...bodyBlocks);
114708
+ children: [{ type: 'text', value: '' }],
114709
+ });
114559
114710
  }
114560
- else {
114561
- children.push(...titleBlocks, ...bodyBlocks);
114711
+ if (hasBody) {
114712
+ const bodyBlocks = parseBlock(calloutJson.body || '');
114713
+ children.push(...bodyBlocks);
114562
114714
  }
114563
- // If there is no title or title is empty
114564
- const empty = !titleBlocks.length || !titleBlocks[0].children[0]?.value;
114565
- // Create mdxJsxFlowElement directly for mdxish
114566
114715
  const calloutElement = {
114567
114716
  type: 'mdxJsxFlowElement',
114568
114717
  name: 'Callout',
@@ -114574,23 +114723,17 @@ function parseMagicBlock(raw, options = {}) {
114574
114723
  ]),
114575
114724
  children: children,
114576
114725
  };
114577
- return [wrapPinnedBlocks(calloutElement, json)];
114726
+ return [wrapPinnedBlocks(calloutElement, data)];
114578
114727
  }
114579
- // Parameters: renders as a table (used for API parameters, etc.)
114580
114728
  case 'parameters': {
114581
- const paramsJson = json;
114582
- const { cols, data, rows } = paramsJson;
114583
- if (!Object.keys(data).length)
114729
+ const paramsJson = data;
114730
+ const { cols, data: tableData, rows } = paramsJson;
114731
+ if (!tableData || !Object.keys(tableData).length)
114584
114732
  return [];
114585
- /**
114586
- * Convert sparse key-value data to 2D array.
114587
- * Keys are formatted as "ROW-COL" where ROW is "h" for header or a number.
114588
- * Example: { "h-0": "Name", "h-1": "Type", "0-0": "id", "0-1": "string" }
114589
- * Becomes: [["Name", "Type"], ["id", "string"]]
114590
- */
114591
- const sparseData = Object.entries(data).reduce((mapped, [key, v]) => {
114733
+ if (typeof cols !== 'number' || typeof rows !== 'number' || cols < 1 || rows < 0)
114734
+ return [];
114735
+ const sparseData = Object.entries(tableData).reduce((mapped, [key, v]) => {
114592
114736
  const [row, col] = key.split('-');
114593
- // Header row ("h") becomes index 0, data rows are offset by 1
114594
114737
  const rowIndex = row === 'h' ? 0 : parseInt(row, 10) + 1;
114595
114738
  const colIndex = parseInt(col, 10);
114596
114739
  if (!mapped[rowIndex])
@@ -114598,9 +114741,8 @@ function parseMagicBlock(raw, options = {}) {
114598
114741
  mapped[rowIndex][colIndex] = v;
114599
114742
  return mapped;
114600
114743
  }, []);
114601
- // In compatibility mode, wrap cell content in paragraphs; otherwise inline text
114602
114744
  const tokenizeCell = compatibilityMode ? textToBlock : parseTableCell;
114603
- const children = Array.from({ length: rows + 1 }, (_, y) => ({
114745
+ const tableChildren = Array.from({ length: rows + 1 }, (_, y) => ({
114604
114746
  children: Array.from({ length: cols }, (__, x) => ({
114605
114747
  children: sparseData[y]?.[x] ? tokenizeCell(sparseData[y][x]) : [{ type: 'text', value: '' }],
114606
114748
  type: y === 0 ? 'tableHead' : 'tableCell',
@@ -114608,14 +114750,15 @@ function parseMagicBlock(raw, options = {}) {
114608
114750
  type: 'tableRow',
114609
114751
  }));
114610
114752
  return [
114611
- wrapPinnedBlocks({ align: paramsJson.align ?? new Array(cols).fill('left'), children, type: 'table' }, json),
114753
+ wrapPinnedBlocks({ align: paramsJson.align ?? new Array(cols).fill('left'), children: tableChildren, type: 'table' }, data),
114612
114754
  ];
114613
114755
  }
114614
- // Embed: external content (YouTube, etc.) with provider detection
114615
114756
  case 'embed': {
114616
- const embedJson = json;
114757
+ const embedJson = data;
114758
+ if (!embedJson.url) {
114759
+ return [wrapPinnedBlocks(EMPTY_EMBED_PLACEHOLDER, data)];
114760
+ }
114617
114761
  const { html, title, url } = embedJson;
114618
- // Extract provider name from URL hostname (e.g., "youtube.com" → "youtube.com")
114619
114762
  try {
114620
114763
  embedJson.provider = new URL(url).hostname
114621
114764
  .split(/(?:www)?\./)
@@ -114632,12 +114775,13 @@ function parseMagicBlock(raw, options = {}) {
114632
114775
  ],
114633
114776
  data: { hName: 'embed-block', hProperties: { ...embedJson, href: url, html, title, url } },
114634
114777
  type: 'embed',
114635
- }, json),
114778
+ }, data),
114636
114779
  ];
114637
114780
  }
114638
- // HTML block: raw HTML content (scripts enabled only in compatibility mode)
114639
114781
  case 'html': {
114640
- const htmlJson = json;
114782
+ const htmlJson = data;
114783
+ if (typeof htmlJson.html !== 'string')
114784
+ return [];
114641
114785
  return [
114642
114786
  wrapPinnedBlocks({
114643
114787
  data: {
@@ -114645,39 +114789,33 @@ function parseMagicBlock(raw, options = {}) {
114645
114789
  hProperties: { html: htmlJson.html, runScripts: compatibilityMode, safeMode },
114646
114790
  },
114647
114791
  type: 'html-block',
114648
- }, json),
114792
+ }, data),
114649
114793
  ];
114650
114794
  }
114651
- // Recipe/TutorialTile: renders as Recipe component
114652
114795
  case 'recipe':
114653
114796
  case 'tutorial-tile': {
114654
- const recipeJson = json;
114797
+ const recipeJson = data;
114655
114798
  if (!recipeJson.slug || !recipeJson.title)
114656
114799
  return [];
114657
- // Create mdxJsxFlowElement directly for mdxish flow
114658
- // Note: Don't wrap in pinned blocks for mdxish - rehypeMdxishComponents handles component resolution
114659
- // The node structure matches what mdxishComponentBlocks creates for JSX tags
114660
114800
  const recipeNode = {
114661
114801
  type: 'mdxJsxFlowElement',
114662
114802
  name: 'Recipe',
114663
114803
  attributes: toAttributes(recipeJson, ['slug', 'title']),
114664
114804
  children: [],
114665
- // Position is optional but helps with debugging
114666
114805
  position: undefined,
114667
114806
  };
114668
114807
  return [recipeNode];
114669
114808
  }
114670
- // Unknown block types: render as generic div with JSON properties
114671
114809
  default: {
114672
- const text = json.text || json.html || '';
114810
+ const text = data.text || data.html || '';
114673
114811
  return [
114674
- wrapPinnedBlocks({ children: textToBlock(text), data: { hName: type || 'div', hProperties: json, ...json }, type: 'div' }, json),
114812
+ wrapPinnedBlocks({ children: textToBlock(text), data: { hName: blockType || 'div', hProperties: data, ...data }, type: 'div' }, data),
114675
114813
  ];
114676
114814
  }
114677
114815
  }
114678
114816
  }
114679
114817
  /**
114680
- * Block-level node types that cannot be nested inside paragraphs.
114818
+ * Check if a child node is a flow element that needs unwrapping.
114681
114819
  */
114682
114820
  const blockTypes = [
114683
114821
  'heading',
@@ -114703,29 +114841,19 @@ const blockTypes = [
114703
114841
  */
114704
114842
  const isBlockNode = (node) => blockTypes.includes(node.type);
114705
114843
  /**
114706
- * Unified plugin that restores magic blocks from placeholder tokens.
114707
- *
114708
- * During preprocessing, extractMagicBlocks replaces [block:TYPE]...[/block]
114709
- * with inline code tokens like `__MAGIC_BLOCK_0__`. This plugin finds those
114710
- * tokens in the parsed MDAST and replaces them with the parsed block content.
114844
+ * Unified plugin that transforms magicBlock nodes into final MDAST nodes.
114711
114845
  */
114712
- const magicBlockRestorer = ({ blocks }) => tree => {
114713
- if (!blocks.length)
114714
- return;
114715
- // Map: key → original raw magic block content
114716
- const magicBlockKeys = new Map(blocks.map(({ key, raw }) => [key, raw]));
114717
- // Collect replacements to apply (we need to visit in reverse to maintain indices)
114846
+ const magicBlockTransformer = (options = {}) => tree => {
114718
114847
  const replacements = [];
114719
- // First pass: collect all replacements
114720
- visit(tree, 'inlineCode', (node, index, parent) => {
114721
- if (!parent || index == null)
114722
- return undefined;
114723
- const raw = magicBlockKeys.get(node.value);
114724
- if (!raw)
114725
- return undefined;
114726
- const children = parseMagicBlock(raw);
114727
- if (!children.length)
114848
+ visit(tree, 'magicBlock', (node, index, parent) => {
114849
+ if (!parent || index === undefined)
114728
114850
  return undefined;
114851
+ const children = transformMagicBlock(node.blockType, node.data, node.value, options);
114852
+ if (!children.length) {
114853
+ // Remove the node if transformation returns nothing
114854
+ parent.children.splice(index, 1);
114855
+ return [SKIP, index];
114856
+ }
114729
114857
  // If parent is a paragraph and we're inserting block nodes (which must not be in paragraphs), lift them out
114730
114858
  if (parent.type === 'paragraph' && children.some(child => isBlockNode(child))) {
114731
114859
  const blockNodes = [];
@@ -114794,7 +114922,557 @@ const magicBlockRestorer = ({ blocks }) => tree => {
114794
114922
  }
114795
114923
  }
114796
114924
  };
114797
- /* harmony default export */ const mdxish_magic_blocks = (magicBlockRestorer);
114925
+ /* harmony default export */ const magic_block_transformer = (magicBlockTransformer);
114926
+
114927
+ ;// ./processor/transform/mdxish/mdxish-html-blocks.ts
114928
+
114929
+
114930
+
114931
+
114932
+ /**
114933
+ * Decodes HTMLBlock content that was protected during preprocessing.
114934
+ * Content is wrapped in <!--RDMX_HTMLBLOCK:base64:RDMX_HTMLBLOCK-->
114935
+ */
114936
+ function decodeProtectedContent(content) {
114937
+ // Escape special regex characters in the markers
114938
+ const startEscaped = HTML_BLOCK_CONTENT_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
114939
+ const endEscaped = HTML_BLOCK_CONTENT_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
114940
+ const markerRegex = new RegExp(`${startEscaped}([A-Za-z0-9+/=]+)${endEscaped}`, 'g');
114941
+ return content.replace(markerRegex, (_match, encoded) => {
114942
+ try {
114943
+ return base64Decode(encoded);
114944
+ }
114945
+ catch {
114946
+ return encoded;
114947
+ }
114948
+ });
114949
+ }
114950
+ /**
114951
+ * Collects text content from a node and its children recursively
114952
+ */
114953
+ function collectTextContent(node) {
114954
+ const parts = [];
114955
+ if (node.type === 'text' && node.value) {
114956
+ parts.push(node.value);
114957
+ }
114958
+ else if (node.type === 'html' && node.value) {
114959
+ parts.push(node.value);
114960
+ }
114961
+ else if (node.type === 'inlineCode' && node.value) {
114962
+ parts.push(node.value);
114963
+ }
114964
+ else if (node.type === 'code' && node.value) {
114965
+ // Reconstruct code fence syntax (markdown parser consumes opening ```)
114966
+ const lang = node.lang || '';
114967
+ const fence = `\`\`\`${lang ? `${lang}\n` : ''}`;
114968
+ parts.push(fence);
114969
+ parts.push(node.value);
114970
+ // Add newline before closing fence if missing
114971
+ const closingFence = node.value.endsWith('\n') ? '```' : '\n```';
114972
+ parts.push(closingFence);
114973
+ }
114974
+ else if (node.children && Array.isArray(node.children)) {
114975
+ node.children.forEach(child => {
114976
+ if (typeof child === 'object' && child !== null) {
114977
+ parts.push(collectTextContent(child));
114978
+ }
114979
+ });
114980
+ }
114981
+ return parts.join('');
114982
+ }
114983
+ /**
114984
+ * Extracts boolean attribute from HTML tag. Handles JSX (safeMode={true}) and string (safeMode="true") syntax.
114985
+ * Returns "true"/"false" string to survive rehypeRaw serialization.
114986
+ */
114987
+ function extractBooleanAttr(attrs, name) {
114988
+ // Try JSX syntax: name={true|false}
114989
+ const jsxMatch = attrs.match(new RegExp(`${name}=\\{(true|false)\\}`));
114990
+ if (jsxMatch) {
114991
+ return jsxMatch[1];
114992
+ }
114993
+ // Try string syntax: name="true"|true
114994
+ const stringMatch = attrs.match(new RegExp(`${name}="?(true|false)"?`));
114995
+ if (stringMatch) {
114996
+ return stringMatch[1];
114997
+ }
114998
+ return undefined;
114999
+ }
115000
+ /**
115001
+ * Extracts runScripts attribute from HTML tag. Returns boolean for "true"/"false", string for other values, or undefined if not found.
115002
+ */
115003
+ function extractRunScriptsAttr(attrs) {
115004
+ const runScriptsMatch = attrs.match(/runScripts="?([^">\s]+)"?/);
115005
+ if (!runScriptsMatch) {
115006
+ return undefined;
115007
+ }
115008
+ const value = runScriptsMatch[1];
115009
+ if (value === 'true') {
115010
+ return true;
115011
+ }
115012
+ if (value === 'false') {
115013
+ return false;
115014
+ }
115015
+ return value;
115016
+ }
115017
+ /**
115018
+ * Creates an HTMLBlock node from HTML string and optional attributes
115019
+ */
115020
+ function createHTMLBlockNode(htmlString, position, runScripts, safeMode) {
115021
+ return {
115022
+ position,
115023
+ children: [{ type: 'text', value: htmlString }],
115024
+ type: NodeTypes.htmlBlock,
115025
+ data: {
115026
+ hName: 'html-block',
115027
+ hProperties: {
115028
+ html: htmlString,
115029
+ ...(runScripts !== undefined && { runScripts }),
115030
+ ...(safeMode !== undefined && { safeMode }),
115031
+ },
115032
+ },
115033
+ };
115034
+ }
115035
+ /**
115036
+ * Checks for opening tag only (for split detection)
115037
+ */
115038
+ function hasOpeningTagOnly(node) {
115039
+ let hasOpening = false;
115040
+ let hasClosed = false;
115041
+ let attrs = '';
115042
+ const check = (n) => {
115043
+ if (n.type === 'html' && n.value) {
115044
+ if (n.value === '<HTMLBlock>') {
115045
+ hasOpening = true;
115046
+ }
115047
+ else {
115048
+ const match = n.value.match(/^<HTMLBlock(\s[^>]*)?>$/);
115049
+ if (match) {
115050
+ hasOpening = true;
115051
+ attrs = match[1] || '';
115052
+ }
115053
+ }
115054
+ if (n.value === '</HTMLBlock>' || n.value.includes('</HTMLBlock>')) {
115055
+ hasClosed = true;
115056
+ }
115057
+ }
115058
+ if (n.children && Array.isArray(n.children)) {
115059
+ n.children.forEach(child => {
115060
+ check(child);
115061
+ });
115062
+ }
115063
+ };
115064
+ check(node);
115065
+ // Return true only if opening without closing (split case)
115066
+ return { attrs, found: hasOpening && !hasClosed };
115067
+ }
115068
+ /**
115069
+ * Checks if a node contains an HTMLBlock closing tag
115070
+ */
115071
+ function hasClosingTag(node) {
115072
+ if (node.type === 'html' && node.value) {
115073
+ if (node.value === '</HTMLBlock>' || node.value.includes('</HTMLBlock>'))
115074
+ return true;
115075
+ }
115076
+ if (node.children && Array.isArray(node.children)) {
115077
+ return node.children.some(child => hasClosingTag(child));
115078
+ }
115079
+ return false;
115080
+ }
115081
+ /**
115082
+ * Transforms HTMLBlock MDX JSX to html-block nodes. Handles <HTMLBlock>{`...`}</HTMLBlock> syntax.
115083
+ */
115084
+ const mdxishHtmlBlocks = () => tree => {
115085
+ // Handle HTMLBlock split across root children (caused by newlines)
115086
+ visit(tree, 'root', (root) => {
115087
+ const children = root.children;
115088
+ let i = 0;
115089
+ while (i < children.length) {
115090
+ const child = children[i];
115091
+ const { attrs, found: hasOpening } = hasOpeningTagOnly(child);
115092
+ if (hasOpening) {
115093
+ // Find closing tag in subsequent siblings
115094
+ let closingIdx = -1;
115095
+ for (let j = i + 1; j < children.length; j += 1) {
115096
+ if (hasClosingTag(children[j])) {
115097
+ closingIdx = j;
115098
+ break;
115099
+ }
115100
+ }
115101
+ if (closingIdx !== -1) {
115102
+ // Collect inner content between tags
115103
+ const contentParts = [];
115104
+ for (let j = i; j <= closingIdx; j += 1) {
115105
+ const node = children[j];
115106
+ contentParts.push(collectTextContent(node));
115107
+ }
115108
+ // Remove the opening/closing tags and template literal syntax from content
115109
+ let content = contentParts.join('');
115110
+ content = content.replace(/^<HTMLBlock[^>]*>\s*\{?\s*`?/, '').replace(/`?\s*\}?\s*<\/HTMLBlock>$/, '');
115111
+ // Decode protected content that was base64 encoded during preprocessing
115112
+ content = decodeProtectedContent(content);
115113
+ const htmlString = formatHtmlForMdxish(content);
115114
+ const runScripts = extractRunScriptsAttr(attrs);
115115
+ const safeMode = extractBooleanAttr(attrs, 'safeMode');
115116
+ // Replace range with single HTMLBlock node
115117
+ const mdNode = createHTMLBlockNode(htmlString, children[i].position, runScripts, safeMode);
115118
+ root.children.splice(i, closingIdx - i + 1, mdNode);
115119
+ }
115120
+ }
115121
+ i += 1;
115122
+ }
115123
+ });
115124
+ // Handle HTMLBlock parsed as HTML elements (when template literal contains block-level HTML tags)
115125
+ visit(tree, 'html', (node, index, parent) => {
115126
+ if (!parent || index === undefined)
115127
+ return;
115128
+ const value = node.value;
115129
+ if (!value)
115130
+ return;
115131
+ // Case 1: Full HTMLBlock in single node
115132
+ const fullMatch = value.match(/^<HTMLBlock(\s[^>]*)?>([\s\S]*)<\/HTMLBlock>$/);
115133
+ if (fullMatch) {
115134
+ const attrs = fullMatch[1] || '';
115135
+ let content = fullMatch[2] || '';
115136
+ // Remove template literal syntax if present: {`...`}
115137
+ content = content.replace(/^\s*\{\s*`/, '').replace(/`\s*\}\s*$/, '');
115138
+ // Decode protected content that was base64 encoded during preprocessing
115139
+ content = decodeProtectedContent(content);
115140
+ const htmlString = formatHtmlForMdxish(content);
115141
+ const runScripts = extractRunScriptsAttr(attrs);
115142
+ const safeMode = extractBooleanAttr(attrs, 'safeMode');
115143
+ parent.children[index] = createHTMLBlockNode(htmlString, node.position, runScripts, safeMode);
115144
+ return;
115145
+ }
115146
+ // Case 2: Opening tag only (split by blank lines)
115147
+ if (value === '<HTMLBlock>' || value.match(/^<HTMLBlock\s[^>]*>$/)) {
115148
+ const siblings = parent.children;
115149
+ let closingIdx = -1;
115150
+ // Find closing tag in siblings
115151
+ for (let i = index + 1; i < siblings.length; i += 1) {
115152
+ const sibling = siblings[i];
115153
+ if (sibling.type === 'html') {
115154
+ const sibVal = sibling.value;
115155
+ if (sibVal === '</HTMLBlock>' || sibVal?.includes('</HTMLBlock>')) {
115156
+ closingIdx = i;
115157
+ break;
115158
+ }
115159
+ }
115160
+ }
115161
+ if (closingIdx === -1)
115162
+ return;
115163
+ // Collect content between tags, skipping template literal delimiters
115164
+ const contentParts = [];
115165
+ for (let i = index + 1; i < closingIdx; i += 1) {
115166
+ const sibling = siblings[i];
115167
+ // Skip template literal delimiters
115168
+ if (sibling.type === 'text') {
115169
+ const textVal = sibling.value;
115170
+ if (textVal === '{' || textVal === '}' || textVal === '{`' || textVal === '`}') {
115171
+ // eslint-disable-next-line no-continue
115172
+ continue;
115173
+ }
115174
+ }
115175
+ contentParts.push(collectTextContent(sibling));
115176
+ }
115177
+ // Decode protected content that was base64 encoded during preprocessing
115178
+ const decodedContent = decodeProtectedContent(contentParts.join(''));
115179
+ const htmlString = formatHtmlForMdxish(decodedContent);
115180
+ const runScripts = extractRunScriptsAttr(value);
115181
+ const safeMode = extractBooleanAttr(value, 'safeMode');
115182
+ // Replace opening tag with HTMLBlock node, remove consumed siblings
115183
+ parent.children[index] = createHTMLBlockNode(htmlString, node.position, runScripts, safeMode);
115184
+ parent.children.splice(index + 1, closingIdx - index);
115185
+ }
115186
+ });
115187
+ // Handle HTMLBlock inside paragraphs (parsed as inline elements)
115188
+ visit(tree, 'paragraph', (node, index, parent) => {
115189
+ if (!parent || index === undefined)
115190
+ return;
115191
+ const children = node.children || [];
115192
+ let htmlBlockStartIdx = -1;
115193
+ let htmlBlockEndIdx = -1;
115194
+ let templateLiteralStartIdx = -1;
115195
+ let templateLiteralEndIdx = -1;
115196
+ for (let i = 0; i < children.length; i += 1) {
115197
+ const child = children[i];
115198
+ if (child.type === 'html' && typeof child.value === 'string') {
115199
+ const value = child.value;
115200
+ if (value === '<HTMLBlock>' || value.match(/^<HTMLBlock\s[^>]*>$/)) {
115201
+ htmlBlockStartIdx = i;
115202
+ }
115203
+ else if (value === '</HTMLBlock>') {
115204
+ htmlBlockEndIdx = i;
115205
+ }
115206
+ }
115207
+ // Find opening brace after HTMLBlock start
115208
+ if (htmlBlockStartIdx !== -1 && templateLiteralStartIdx === -1 && child.type === 'text') {
115209
+ const value = child.value;
115210
+ if (value === '{') {
115211
+ templateLiteralStartIdx = i;
115212
+ }
115213
+ }
115214
+ // Find closing brace before HTMLBlock end
115215
+ if (htmlBlockStartIdx !== -1 && htmlBlockEndIdx === -1 && child.type === 'text') {
115216
+ const value = child.value;
115217
+ if (value === '}') {
115218
+ templateLiteralEndIdx = i;
115219
+ }
115220
+ }
115221
+ }
115222
+ if (htmlBlockStartIdx !== -1 &&
115223
+ htmlBlockEndIdx !== -1 &&
115224
+ templateLiteralStartIdx !== -1 &&
115225
+ templateLiteralEndIdx !== -1 &&
115226
+ templateLiteralStartIdx < templateLiteralEndIdx) {
115227
+ const openingTag = children[htmlBlockStartIdx];
115228
+ // Collect content between braces (handles code blocks)
115229
+ const templateContent = [];
115230
+ for (let i = templateLiteralStartIdx + 1; i < templateLiteralEndIdx; i += 1) {
115231
+ const child = children[i];
115232
+ templateContent.push(collectTextContent(child));
115233
+ }
115234
+ // Decode protected content that was base64 encoded during preprocessing
115235
+ const decodedContent = decodeProtectedContent(templateContent.join(''));
115236
+ const htmlString = formatHtmlForMdxish(decodedContent);
115237
+ const runScripts = openingTag.value ? extractRunScriptsAttr(openingTag.value) : undefined;
115238
+ const safeMode = openingTag.value ? extractBooleanAttr(openingTag.value, 'safeMode') : undefined;
115239
+ const mdNode = createHTMLBlockNode(htmlString, node.position, runScripts, safeMode);
115240
+ parent.children[index] = mdNode;
115241
+ }
115242
+ });
115243
+ // Ensure html-block nodes have HTML in children as text node
115244
+ visit(tree, 'html-block', (node) => {
115245
+ const html = node.data?.hProperties?.html;
115246
+ if (html &&
115247
+ (!node.children ||
115248
+ node.children.length === 0 ||
115249
+ (node.children.length === 1 && node.children[0].type === 'text' && node.children[0].value !== html))) {
115250
+ node.children = [
115251
+ {
115252
+ type: 'text',
115253
+ value: html,
115254
+ },
115255
+ ];
115256
+ }
115257
+ });
115258
+ return tree;
115259
+ };
115260
+ /* harmony default export */ const mdxish_html_blocks = (mdxishHtmlBlocks);
115261
+
115262
+ ;// ./processor/transform/mdxish/mdxish-jsx-to-mdast.ts
115263
+
115264
+
115265
+
115266
+ const transformImage = (jsx) => {
115267
+ const attrs = getAttrs(jsx);
115268
+ const { align, alt = '', border, caption, className, height, lazy, src = '', title = '', width } = attrs;
115269
+ const hProperties = {
115270
+ alt,
115271
+ src,
115272
+ title,
115273
+ ...(align && { align }),
115274
+ ...(border !== undefined && { border: String(border) }),
115275
+ ...(caption && { caption }),
115276
+ ...(className && { className }),
115277
+ ...(height !== undefined && { height: String(height) }),
115278
+ ...(lazy !== undefined && { lazy }),
115279
+ ...(width !== undefined && { width: String(width) }),
115280
+ };
115281
+ return {
115282
+ type: NodeTypes.imageBlock,
115283
+ align,
115284
+ alt,
115285
+ border: border !== undefined ? String(border) : undefined,
115286
+ caption,
115287
+ className,
115288
+ height: height !== undefined ? String(height) : undefined,
115289
+ lazy,
115290
+ src,
115291
+ title,
115292
+ width: width !== undefined ? String(width) : undefined,
115293
+ data: {
115294
+ hName: 'img',
115295
+ hProperties,
115296
+ },
115297
+ position: jsx.position,
115298
+ };
115299
+ };
115300
+ const transformCallout = (jsx) => {
115301
+ const attrs = getAttrs(jsx);
115302
+ const { empty = false, icon = '', theme = '' } = attrs;
115303
+ return {
115304
+ type: NodeTypes.callout,
115305
+ children: jsx.children,
115306
+ data: {
115307
+ hName: 'Callout',
115308
+ hProperties: {
115309
+ empty,
115310
+ icon,
115311
+ theme,
115312
+ },
115313
+ },
115314
+ position: jsx.position,
115315
+ };
115316
+ };
115317
+ const transformEmbed = (jsx) => {
115318
+ const attrs = getAttrs(jsx);
115319
+ const { favicon, html, iframe, image, providerName, providerUrl, title = '', url = '' } = attrs;
115320
+ return {
115321
+ type: NodeTypes.embedBlock,
115322
+ title,
115323
+ url,
115324
+ data: {
115325
+ hName: 'embed',
115326
+ hProperties: {
115327
+ title,
115328
+ url,
115329
+ ...(favicon && { favicon }),
115330
+ ...(html && { html }),
115331
+ ...(iframe !== undefined && { iframe }),
115332
+ ...(image && { image }),
115333
+ ...(providerName && { providerName }),
115334
+ ...(providerUrl && { providerUrl }),
115335
+ },
115336
+ },
115337
+ position: jsx.position,
115338
+ };
115339
+ };
115340
+ const transformRecipe = (jsx) => {
115341
+ const attrs = getAttrs(jsx);
115342
+ const { backgroundColor = '', emoji = '', id = '', link = '', slug = '', title = '' } = attrs;
115343
+ return {
115344
+ type: NodeTypes.recipe,
115345
+ backgroundColor,
115346
+ emoji,
115347
+ id,
115348
+ link,
115349
+ slug,
115350
+ title,
115351
+ position: jsx.position,
115352
+ };
115353
+ };
115354
+ /**
115355
+ * Transform a magic block image node into an ImageBlock.
115356
+ * Magic block images have structure: { type: 'image', url, title, alt, data.hProperties }
115357
+ */
115358
+ const transformMagicBlockImage = (node) => {
115359
+ const { alt = '', data, position, title = '', url = '' } = node;
115360
+ const hProps = data?.hProperties || {};
115361
+ const { align, border, width } = hProps;
115362
+ const hProperties = {
115363
+ alt,
115364
+ src: url,
115365
+ title,
115366
+ ...(align && { align }),
115367
+ ...(border && { border }),
115368
+ ...(width && { width }),
115369
+ };
115370
+ return {
115371
+ type: NodeTypes.imageBlock,
115372
+ align,
115373
+ alt,
115374
+ border,
115375
+ src: url,
115376
+ title,
115377
+ width,
115378
+ data: {
115379
+ hName: 'img',
115380
+ hProperties,
115381
+ },
115382
+ position,
115383
+ };
115384
+ };
115385
+ /**
115386
+ * Transform a magic block embed node into an EmbedBlock.
115387
+ * Magic block embeds have structure: { type: 'embed', children, data.hProperties }
115388
+ */
115389
+ const transformMagicBlockEmbed = (node) => {
115390
+ const { data, position } = node;
115391
+ const hProps = data?.hProperties || {};
115392
+ const { favicon, html, image, providerName, providerUrl, title = '', url = '' } = hProps;
115393
+ return {
115394
+ type: NodeTypes.embedBlock,
115395
+ title,
115396
+ url,
115397
+ data: {
115398
+ hName: 'embed',
115399
+ hProperties: {
115400
+ title,
115401
+ url,
115402
+ ...(favicon && { favicon }),
115403
+ ...(html && { html }),
115404
+ ...(image && { image }),
115405
+ ...(providerName && { providerName }),
115406
+ ...(providerUrl && { providerUrl }),
115407
+ },
115408
+ },
115409
+ position,
115410
+ };
115411
+ };
115412
+ const COMPONENT_MAP = {
115413
+ Callout: transformCallout,
115414
+ Embed: transformEmbed,
115415
+ Image: transformImage,
115416
+ Recipe: transformRecipe,
115417
+ };
115418
+ /**
115419
+ * Transform mdxJsxFlowElement nodes and magic block nodes into proper MDAST node types.
115420
+ *
115421
+ * This transformer runs after mdxishComponentBlocks and converts:
115422
+ * - JSX component elements (Image, Callout, Embed, Recipe) into their corresponding MDAST types
115423
+ * - Magic block image nodes (type: 'image') into image-block
115424
+ * - Magic block embed nodes (type: 'embed') into embed-block
115425
+ * - Figure nodes containing images (from magic blocks with captions) - transforms the inner image
115426
+ *
115427
+ * This is controlled by the `newEditorTypes` flag to maintain backwards compatibility.
115428
+ */
115429
+ const mdxishJsxToMdast = () => tree => {
115430
+ // Transform JSX components (Image, Callout, Embed, Recipe)
115431
+ visit(tree, 'mdxJsxFlowElement', (node, index, parent) => {
115432
+ if (!parent || index === undefined || !node.name)
115433
+ return;
115434
+ const transformer = COMPONENT_MAP[node.name];
115435
+ if (!transformer)
115436
+ return;
115437
+ const newNode = transformer(node);
115438
+ // Replace the JSX node with the MDAST node
115439
+ parent.children[index] = newNode;
115440
+ });
115441
+ // Transform magic block images (type: 'image') to image-block
115442
+ // Note: Standard markdown images are wrapped in paragraphs and handled by imageTransformer
115443
+ // Magic block images are direct children of root, so we handle them here
115444
+ visit(tree, 'image', (node, index, parent) => {
115445
+ if (!parent || index === undefined)
115446
+ return SKIP;
115447
+ // Skip images inside paragraphs (those are standard markdown images handled by imageTransformer)
115448
+ if (parent.type === 'paragraph')
115449
+ return SKIP;
115450
+ const newNode = transformMagicBlockImage(node);
115451
+ parent.children[index] = newNode;
115452
+ return SKIP;
115453
+ });
115454
+ // Transform magic block embeds (type: 'embed') to embed-block
115455
+ visit(tree, 'embed', (node, index, parent) => {
115456
+ if (!parent || index === undefined)
115457
+ return SKIP;
115458
+ const newNode = transformMagicBlockEmbed(node);
115459
+ parent.children[index] = newNode;
115460
+ return SKIP;
115461
+ });
115462
+ // Transform images inside figure nodes (magic blocks with captions)
115463
+ const isFigure = (node) => node.type === 'figure';
115464
+ visit(tree, isFigure, node => {
115465
+ // Find and transform the image child
115466
+ node.children = node.children.map(child => {
115467
+ if (child.type === 'image') {
115468
+ return transformMagicBlockImage(child);
115469
+ }
115470
+ return child;
115471
+ });
115472
+ });
115473
+ return tree;
115474
+ };
115475
+ /* harmony default export */ const mdxish_jsx_to_mdast = (mdxishJsxToMdast);
114798
115476
 
114799
115477
  ;// ./processor/transform/mdxish/mdxish-mermaid.ts
114800
115478
 
@@ -114836,25 +115514,50 @@ const componentTagPattern = /<(\/?[A-Z][A-Za-z0-9_]*)([^>]*?)(\/?)>/g;
114836
115514
 
114837
115515
  ;// ./processor/transform/mdxish/mdxish-snake-case-components.ts
114838
115516
 
115517
+
114839
115518
  /**
114840
115519
  * Replaces snake_case component names with valid HTML placeholders.
114841
115520
  * Required because remark-parse rejects tags with underscores.
114842
115521
  * Example: `<Snake_case />` → `<MDXishSnakeCase0 />`
115522
+ *
115523
+ * Code blocks and inline code are protected and will not be transformed.
115524
+ *
115525
+ * @param content - The markdown content to process
115526
+ * @param options - Options including knownComponents to filter by
114843
115527
  */
114844
- function processSnakeCaseComponent(content) {
115528
+ function processSnakeCaseComponent(content, options = {}) {
115529
+ const { knownComponents } = options;
114845
115530
  // Early exit if no potential snake_case components
114846
115531
  if (!/[A-Z][A-Za-z0-9]*_[A-Za-z0-9_]*/.test(content)) {
114847
115532
  return { content, mapping: {} };
114848
115533
  }
115534
+ // Step 1: Extract code blocks to protect them from transformation
115535
+ const { protectedCode, protectedContent } = protectCodeBlocks(content);
115536
+ // Find the highest existing placeholder number to avoid collisions
115537
+ // e.g., if content has <MDXishSnakeCase0 />, start counter from 1
115538
+ const placeholderPattern = /MDXishSnakeCase(\d+)/g;
115539
+ let startCounter = 0;
115540
+ let placeholderMatch;
115541
+ while ((placeholderMatch = placeholderPattern.exec(content)) !== null) {
115542
+ const num = parseInt(placeholderMatch[1], 10);
115543
+ if (num >= startCounter) {
115544
+ startCounter = num + 1;
115545
+ }
115546
+ }
114849
115547
  const mapping = {};
114850
115548
  const reverseMap = new Map();
114851
- let counter = 0;
114852
- const processedContent = content.replace(componentTagPattern, (match, tagName, attrs, selfClosing) => {
115549
+ let counter = startCounter;
115550
+ // Step 2: Transform snake_case components in non-code content
115551
+ const processedContent = protectedContent.replace(componentTagPattern, (match, tagName, attrs, selfClosing) => {
114853
115552
  if (!tagName.includes('_')) {
114854
115553
  return match;
114855
115554
  }
114856
115555
  const isClosing = tagName.startsWith('/');
114857
115556
  const cleanTagName = isClosing ? tagName.slice(1) : tagName;
115557
+ // Only transform if it's a known component (or if no filter is provided)
115558
+ if (knownComponents && !knownComponents.has(cleanTagName)) {
115559
+ return match;
115560
+ }
114858
115561
  let placeholder = reverseMap.get(cleanTagName);
114859
115562
  if (!placeholder) {
114860
115563
  // eslint-disable-next-line no-plusplus
@@ -114865,8 +115568,10 @@ function processSnakeCaseComponent(content) {
114865
115568
  const processedTagName = isClosing ? `/${placeholder}` : placeholder;
114866
115569
  return `<${processedTagName}${attrs}${selfClosing}>`;
114867
115570
  });
115571
+ // Step 3: Restore code blocks (untouched)
115572
+ const finalContent = restoreCodeBlocks(processedContent, protectedCode);
114868
115573
  return {
114869
- content: processedContent,
115574
+ content: finalContent,
114870
115575
  mapping,
114871
115576
  };
114872
115577
  }
@@ -114883,139 +115588,6 @@ function restoreSnakeCase(placeholderName, mapping) {
114883
115588
  return matchingKey ? mapping[matchingKey] : placeholderName;
114884
115589
  }
114885
115590
 
114886
- ;// ./processor/transform/mdxish/normalize-malformed-md-syntax.ts
114887
-
114888
- // Patterns to detect for bold (** and __) and italic (* and _) syntax:
114889
- // Bold: ** text**, **text **, word** text**, ** text **
114890
- // Italic: * text*, *text *, word* text*, * text *
114891
- // Same patterns for underscore variants
114892
- // We use separate patterns for each marker type to allow this flexibility.
114893
- // Pattern for ** bold **
114894
- // Groups: 1=wordBefore, 2=marker, 3=contentWithSpaceAfter, 4=trailingSpace1, 5=contentWithSpaceBefore, 6=trailingSpace2, 7=afterChar
114895
- // trailingSpace1 is for "** text **" pattern, trailingSpace2 is for "**text **" pattern
114896
- const asteriskBoldRegex = /([^*\s]+)?\s*(\*\*)(?:\s+((?:[^*\n]|\*(?!\*))+?)(\s*)\2|((?:[^*\n]|\*(?!\*))+?)(\s+)\2)(\S|$)?/g;
114897
- // Pattern for __ bold __
114898
- const underscoreBoldRegex = /([^_\s]+)?\s*(__)(?:\s+((?:[^_\n]|_(?!_))+?)(\s*)\2|((?:[^_\n]|_(?!_))+?)(\s+)\2)(\S|$)?/g;
114899
- // Pattern for * italic *
114900
- const asteriskItalicRegex = /([^*\s]+)?\s*(\*)(?!\*)(?:\s+([^*\n]+?)(\s*)\2|([^*\n]+?)(\s+)\2)(\S|$)?/g;
114901
- // Pattern for _ italic _
114902
- const underscoreItalicRegex = /([^_\s]+)?\s*(_)(?!_)(?:\s+([^_\n]+?)(\s*)\2|([^_\n]+?)(\s+)\2)(\S|$)?/g;
114903
- /**
114904
- * A remark plugin that normalizes malformed bold and italic markers in text nodes.
114905
- * Detects patterns like `** bold**`, `Hello** Wrong Bold**`, `__ bold__`, `Hello__ Wrong Bold__`,
114906
- * `* italic*`, `Hello* Wrong Italic*`, `_ italic_`, or `Hello_ Wrong Italic_`
114907
- * and converts them to proper strong/emphasis nodes, matching the behavior of the legacy rdmd engine.
114908
- *
114909
- * Supports both asterisk (`**bold**`, `*italic*`) and underscore (`__bold__`, `_italic_`) syntax.
114910
- * Also supports snake_case content like `** some_snake_case**`.
114911
- *
114912
- * This runs after remark-parse, which (in v11+) is strict and doesn't parse
114913
- * malformed emphasis syntax. This plugin post-processes the AST to handle these cases.
114914
- */
114915
- const normalizeEmphasisAST = () => (tree) => {
114916
- visit(tree, 'text', function visitor(node, index, parent) {
114917
- if (index === undefined || !parent)
114918
- return undefined;
114919
- // Skip if inside code blocks or inline code
114920
- if (parent.type === 'inlineCode' || parent.type === 'code') {
114921
- return undefined;
114922
- }
114923
- const text = node.value;
114924
- const allMatches = [];
114925
- [...text.matchAll(asteriskBoldRegex)].forEach(match => {
114926
- allMatches.push({ isBold: true, marker: '**', match });
114927
- });
114928
- [...text.matchAll(underscoreBoldRegex)].forEach(match => {
114929
- allMatches.push({ isBold: true, marker: '__', match });
114930
- });
114931
- [...text.matchAll(asteriskItalicRegex)].forEach(match => {
114932
- allMatches.push({ isBold: false, marker: '*', match });
114933
- });
114934
- [...text.matchAll(underscoreItalicRegex)].forEach(match => {
114935
- allMatches.push({ isBold: false, marker: '_', match });
114936
- });
114937
- if (allMatches.length === 0)
114938
- return undefined;
114939
- allMatches.sort((a, b) => (a.match.index ?? 0) - (b.match.index ?? 0));
114940
- const filteredMatches = [];
114941
- let lastEnd = 0;
114942
- allMatches.forEach(info => {
114943
- const start = info.match.index ?? 0;
114944
- const end = start + info.match[0].length;
114945
- if (start >= lastEnd) {
114946
- filteredMatches.push(info);
114947
- lastEnd = end;
114948
- }
114949
- });
114950
- if (filteredMatches.length === 0)
114951
- return undefined;
114952
- const parts = [];
114953
- let lastIndex = 0;
114954
- filteredMatches.forEach(({ match, marker, isBold }) => {
114955
- const matchIndex = match.index ?? 0;
114956
- const fullMatch = match[0];
114957
- if (matchIndex > lastIndex) {
114958
- const beforeText = text.slice(lastIndex, matchIndex);
114959
- if (beforeText) {
114960
- parts.push({ type: 'text', value: beforeText });
114961
- }
114962
- }
114963
- const wordBefore = match[1]; // e.g., "Hello" in "Hello** Wrong Bold**"
114964
- const contentWithSpaceAfter = match[3]; // Content when there's a space after opening markers
114965
- const trailingSpace1 = match[4] || ''; // Space before closing markers (for "** text **" pattern)
114966
- const contentWithSpaceBefore = match[5]; // Content when there's only a space before closing markers
114967
- const trailingSpace2 = match[6] || ''; // Space before closing markers (for "**text **" pattern)
114968
- const trailingSpace = trailingSpace1 || trailingSpace2; // Combined trailing space
114969
- const content = (contentWithSpaceAfter || contentWithSpaceBefore || '').trim();
114970
- const afterChar = match[7]; // Character after closing markers (if any)
114971
- const markerPos = fullMatch.indexOf(marker);
114972
- const spacesBeforeMarkers = wordBefore
114973
- ? fullMatch.slice(wordBefore.length, markerPos)
114974
- : fullMatch.slice(0, markerPos);
114975
- const shouldAddSpace = !!contentWithSpaceAfter && !!wordBefore && !spacesBeforeMarkers;
114976
- if (wordBefore) {
114977
- const spacing = spacesBeforeMarkers + (shouldAddSpace ? ' ' : '');
114978
- parts.push({ type: 'text', value: wordBefore + spacing });
114979
- }
114980
- else if (spacesBeforeMarkers) {
114981
- parts.push({ type: 'text', value: spacesBeforeMarkers });
114982
- }
114983
- if (content) {
114984
- if (isBold) {
114985
- parts.push({
114986
- type: 'strong',
114987
- children: [{ type: 'text', value: content }],
114988
- });
114989
- }
114990
- else {
114991
- parts.push({
114992
- type: 'emphasis',
114993
- children: [{ type: 'text', value: content }],
114994
- });
114995
- }
114996
- }
114997
- if (afterChar) {
114998
- const prefix = trailingSpace ? ' ' : '';
114999
- parts.push({ type: 'text', value: prefix + afterChar });
115000
- }
115001
- lastIndex = matchIndex + fullMatch.length;
115002
- });
115003
- if (lastIndex < text.length) {
115004
- const remainingText = text.slice(lastIndex);
115005
- if (remainingText) {
115006
- parts.push({ type: 'text', value: remainingText });
115007
- }
115008
- }
115009
- if (parts.length > 0) {
115010
- parent.children.splice(index, 1, ...parts);
115011
- return [SKIP, index + parts.length];
115012
- }
115013
- return undefined;
115014
- });
115015
- return tree;
115016
- };
115017
- /* harmony default export */ const normalize_malformed_md_syntax = (normalizeEmphasisAST);
115018
-
115019
115591
  ;// ./processor/transform/mdxish/normalize-table-separator.ts
115020
115592
  /**
115021
115593
  * Preprocessor to normalize malformed GFM table separator syntax.
@@ -115248,57 +115820,1172 @@ const variablesTextTransformer = () => tree => {
115248
115820
  };
115249
115821
  /* harmony default export */ const variables_text = (variablesTextTransformer);
115250
115822
 
115251
- ;// ./lib/utils/extractMagicBlocks.ts
115823
+ ;// ./lib/mdast-util/magic-block/index.ts
115824
+ const contextMap = new WeakMap();
115252
115825
  /**
115253
- * The content matching in this regex captures everything between `[block:TYPE]`
115254
- * and `[/block]`, including new lines. Negative lookahead for the closing
115255
- * `[/block]` tag is required to prevent greedy matching to ensure it stops at
115256
- * the first closing tag it encounters preventing vulnerability to polynomial
115257
- * backtracking issues.
115826
+ * Find the magicBlock token in the token ancestry.
115258
115827
  */
115259
- const MAGIC_BLOCK_REGEX = /\[block:[^\]]{1,100}\](?:(?!\[block:)(?!\[\/block\])[\s\S])*\[\/block\]/g;
115828
+ function findMagicBlockToken() {
115829
+ // Walk up the token stack to find the magicBlock token
115830
+ const events = this.tokenStack;
115831
+ for (let i = events.length - 1; i >= 0; i -= 1) {
115832
+ const token = events[i][0];
115833
+ if (token.type === 'magicBlock') {
115834
+ return token;
115835
+ }
115836
+ }
115837
+ return undefined;
115838
+ }
115260
115839
  /**
115261
- * Extract legacy magic block syntax from a markdown string.
115262
- * Returns the modified markdown and an array of extracted blocks.
115840
+ * Enter handler: Create a new magicBlock node.
115263
115841
  */
115264
- function extractMagicBlocks(markdown) {
115265
- const blocks = [];
115266
- let index = 0;
115267
- const replaced = markdown.replace(MAGIC_BLOCK_REGEX, match => {
115268
- /**
115269
- * Key is the unique identifier for the magic block
115270
- */
115271
- const key = `__MAGIC_BLOCK_${index}__`;
115272
- /**
115273
- * Token is a wrapper around the `key` to serialize & influence how the
115274
- * magic block is parsed in the remark pipeline.
115275
- * - Use backticks so it becomes a code span, preventing `remarkParse` from
115276
- * parsing special characters in the token as markdown syntax
115277
- * - Prepend a newline to ensure it is parsed as a block level node
115278
- * - Append a newline to ensure it is separated from following content
115279
- */
115280
- const token = `\n\`${key}\`\n`;
115281
- blocks.push({ key, raw: match, token });
115282
- index += 1;
115283
- return token;
115284
- });
115285
- return { replaced, blocks };
115842
+ function enterMagicBlock(token) {
115843
+ // Initialize context for this magic block
115844
+ contextMap.set(token, { blockType: '', dataChunks: [] });
115845
+ this.enter({
115846
+ type: 'magicBlock',
115847
+ blockType: '',
115848
+ data: {},
115849
+ value: '',
115850
+ }, token);
115286
115851
  }
115287
115852
  /**
115288
- * Restore extracted magic blocks back into a markdown string.
115853
+ * Exit handler for block type: Extract the block type from the token.
115289
115854
  */
115290
- function restoreMagicBlocks(replaced, blocks) {
115291
- // If a magic block is at the start or end of the document, the extraction
115292
- // token's newlines will have been trimmed during processing. We need to
115293
- // account for that here to ensure the token is found and replaced correctly.
115294
- // These extra newlines will be removed again when the final string is trimmed.
115295
- const content = `\n${replaced}\n`;
115296
- const restoredContent = blocks.reduce((acc, { token, raw }) => {
115297
- // Ensure each magic block is separated by newlines when restored.
115298
- return acc.split(token).join(`\n${raw}\n`);
115299
- }, content);
115300
- return restoredContent.trim();
115855
+ function exitMagicBlockType(token) {
115856
+ const blockToken = findMagicBlockToken.call(this);
115857
+ if (!blockToken)
115858
+ return;
115859
+ const context = contextMap.get(blockToken);
115860
+ if (context) {
115861
+ context.blockType = this.sliceSerialize(token);
115862
+ }
115863
+ }
115864
+ /**
115865
+ * Exit handler for block data: Accumulate JSON content chunks.
115866
+ */
115867
+ function exitMagicBlockData(token) {
115868
+ const blockToken = findMagicBlockToken.call(this);
115869
+ if (!blockToken)
115870
+ return;
115871
+ const context = contextMap.get(blockToken);
115872
+ if (context) {
115873
+ context.dataChunks.push(this.sliceSerialize(token));
115874
+ }
115875
+ }
115876
+ /**
115877
+ * Exit handler for line endings: Preserve newlines in multiline blocks.
115878
+ */
115879
+ function exitMagicBlockLineEnding(token) {
115880
+ const blockToken = findMagicBlockToken.call(this);
115881
+ if (!blockToken)
115882
+ return;
115883
+ const context = contextMap.get(blockToken);
115884
+ if (context) {
115885
+ context.dataChunks.push(this.sliceSerialize(token));
115886
+ }
115301
115887
  }
115888
+ /**
115889
+ * Exit handler for end marker: If this is a failed end marker check (not the final marker),
115890
+ * add its content to the data chunks so we don't lose characters like '['.
115891
+ */
115892
+ function exitMagicBlockMarkerEnd(token) {
115893
+ const blockToken = findMagicBlockToken.call(this);
115894
+ if (!blockToken)
115895
+ return;
115896
+ // Get the content of the marker
115897
+ const markerContent = this.sliceSerialize(token);
115898
+ // If this marker doesn't end with ']', it's a failed check and content belongs to data
115899
+ // The successful end marker would be "[/block]"
115900
+ if (!markerContent.endsWith(']') || markerContent !== '[/block]') {
115901
+ const context = contextMap.get(blockToken);
115902
+ if (context) {
115903
+ context.dataChunks.push(markerContent);
115904
+ }
115905
+ }
115906
+ }
115907
+ /**
115908
+ * Exit handler: Finalize the magicBlock node with parsed JSON data.
115909
+ */
115910
+ function exitMagicBlock(token) {
115911
+ const context = contextMap.get(token);
115912
+ const node = this.stack[this.stack.length - 1];
115913
+ if (context) {
115914
+ const rawJson = context.dataChunks.join('');
115915
+ node.blockType = context.blockType;
115916
+ node.value = `[block:${context.blockType}]${rawJson}[/block]`;
115917
+ // Parse JSON data
115918
+ try {
115919
+ node.data = JSON.parse(rawJson.trim());
115920
+ }
115921
+ catch {
115922
+ // Invalid JSON - store empty object but keep the raw value
115923
+ node.data = {};
115924
+ }
115925
+ // Clean up context
115926
+ contextMap.delete(token);
115927
+ }
115928
+ this.exit(token);
115929
+ }
115930
+ /**
115931
+ * Handler to serialize magicBlock nodes back to markdown.
115932
+ */
115933
+ const handleMagicBlock = (node) => {
115934
+ const magicNode = node;
115935
+ // If we have the original raw value, use it
115936
+ if (magicNode.value) {
115937
+ return magicNode.value;
115938
+ }
115939
+ // Otherwise reconstruct from parsed data
115940
+ const json = JSON.stringify(magicNode.data, null, 2);
115941
+ return `[block:${magicNode.blockType}]\n${json}\n[/block]`;
115942
+ };
115943
+ /**
115944
+ * Create an extension for `mdast-util-from-markdown` to enable magic blocks.
115945
+ *
115946
+ * Converts micromark magic block tokens into `magicBlock` MDAST nodes.
115947
+ *
115948
+ * @returns Extension for `mdast-util-from-markdown`
115949
+ */
115950
+ function magicBlockFromMarkdown() {
115951
+ return {
115952
+ enter: {
115953
+ magicBlock: enterMagicBlock,
115954
+ },
115955
+ exit: {
115956
+ magicBlockType: exitMagicBlockType,
115957
+ magicBlockData: exitMagicBlockData,
115958
+ magicBlockLineEnding: exitMagicBlockLineEnding,
115959
+ magicBlockMarkerEnd: exitMagicBlockMarkerEnd,
115960
+ magicBlock: exitMagicBlock,
115961
+ },
115962
+ };
115963
+ }
115964
+ /**
115965
+ * Create an extension for `mdast-util-to-markdown` to serialize magic blocks.
115966
+ *
115967
+ * Converts `magicBlock` MDAST nodes back to `[block:TYPE]JSON[/block]` syntax.
115968
+ *
115969
+ * @returns Extension for `mdast-util-to-markdown`
115970
+ */
115971
+ function magicBlockToMarkdown() {
115972
+ return {
115973
+ handlers: {
115974
+ magicBlock: handleMagicBlock,
115975
+ },
115976
+ };
115977
+ }
115978
+
115979
+ ;// ./node_modules/micromark-util-symbol/lib/codes.js
115980
+ /**
115981
+ * Character codes.
115982
+ *
115983
+ * This module is compiled away!
115984
+ *
115985
+ * micromark works based on character codes.
115986
+ * This module contains constants for the ASCII block and the replacement
115987
+ * character.
115988
+ * A couple of them are handled in a special way, such as the line endings
115989
+ * (CR, LF, and CR+LF, commonly known as end-of-line: EOLs), the tab (horizontal
115990
+ * tab) and its expansion based on what column it’s at (virtual space),
115991
+ * and the end-of-file (eof) character.
115992
+ * As values are preprocessed before handling them, the actual characters LF,
115993
+ * CR, HT, and NUL (which is present as the replacement character), are
115994
+ * guaranteed to not exist.
115995
+ *
115996
+ * Unicode basic latin block.
115997
+ */
115998
+ const codes = /** @type {const} */ ({
115999
+ carriageReturn: -5,
116000
+ lineFeed: -4,
116001
+ carriageReturnLineFeed: -3,
116002
+ horizontalTab: -2,
116003
+ virtualSpace: -1,
116004
+ eof: null,
116005
+ nul: 0,
116006
+ soh: 1,
116007
+ stx: 2,
116008
+ etx: 3,
116009
+ eot: 4,
116010
+ enq: 5,
116011
+ ack: 6,
116012
+ bel: 7,
116013
+ bs: 8,
116014
+ ht: 9, // `\t`
116015
+ lf: 10, // `\n`
116016
+ vt: 11, // `\v`
116017
+ ff: 12, // `\f`
116018
+ cr: 13, // `\r`
116019
+ so: 14,
116020
+ si: 15,
116021
+ dle: 16,
116022
+ dc1: 17,
116023
+ dc2: 18,
116024
+ dc3: 19,
116025
+ dc4: 20,
116026
+ nak: 21,
116027
+ syn: 22,
116028
+ etb: 23,
116029
+ can: 24,
116030
+ em: 25,
116031
+ sub: 26,
116032
+ esc: 27,
116033
+ fs: 28,
116034
+ gs: 29,
116035
+ rs: 30,
116036
+ us: 31,
116037
+ space: 32,
116038
+ exclamationMark: 33, // `!`
116039
+ quotationMark: 34, // `"`
116040
+ numberSign: 35, // `#`
116041
+ dollarSign: 36, // `$`
116042
+ percentSign: 37, // `%`
116043
+ ampersand: 38, // `&`
116044
+ apostrophe: 39, // `'`
116045
+ leftParenthesis: 40, // `(`
116046
+ rightParenthesis: 41, // `)`
116047
+ asterisk: 42, // `*`
116048
+ plusSign: 43, // `+`
116049
+ comma: 44, // `,`
116050
+ dash: 45, // `-`
116051
+ dot: 46, // `.`
116052
+ slash: 47, // `/`
116053
+ digit0: 48, // `0`
116054
+ digit1: 49, // `1`
116055
+ digit2: 50, // `2`
116056
+ digit3: 51, // `3`
116057
+ digit4: 52, // `4`
116058
+ digit5: 53, // `5`
116059
+ digit6: 54, // `6`
116060
+ digit7: 55, // `7`
116061
+ digit8: 56, // `8`
116062
+ digit9: 57, // `9`
116063
+ colon: 58, // `:`
116064
+ semicolon: 59, // `;`
116065
+ lessThan: 60, // `<`
116066
+ equalsTo: 61, // `=`
116067
+ greaterThan: 62, // `>`
116068
+ questionMark: 63, // `?`
116069
+ atSign: 64, // `@`
116070
+ uppercaseA: 65, // `A`
116071
+ uppercaseB: 66, // `B`
116072
+ uppercaseC: 67, // `C`
116073
+ uppercaseD: 68, // `D`
116074
+ uppercaseE: 69, // `E`
116075
+ uppercaseF: 70, // `F`
116076
+ uppercaseG: 71, // `G`
116077
+ uppercaseH: 72, // `H`
116078
+ uppercaseI: 73, // `I`
116079
+ uppercaseJ: 74, // `J`
116080
+ uppercaseK: 75, // `K`
116081
+ uppercaseL: 76, // `L`
116082
+ uppercaseM: 77, // `M`
116083
+ uppercaseN: 78, // `N`
116084
+ uppercaseO: 79, // `O`
116085
+ uppercaseP: 80, // `P`
116086
+ uppercaseQ: 81, // `Q`
116087
+ uppercaseR: 82, // `R`
116088
+ uppercaseS: 83, // `S`
116089
+ uppercaseT: 84, // `T`
116090
+ uppercaseU: 85, // `U`
116091
+ uppercaseV: 86, // `V`
116092
+ uppercaseW: 87, // `W`
116093
+ uppercaseX: 88, // `X`
116094
+ uppercaseY: 89, // `Y`
116095
+ uppercaseZ: 90, // `Z`
116096
+ leftSquareBracket: 91, // `[`
116097
+ backslash: 92, // `\`
116098
+ rightSquareBracket: 93, // `]`
116099
+ caret: 94, // `^`
116100
+ underscore: 95, // `_`
116101
+ graveAccent: 96, // `` ` ``
116102
+ lowercaseA: 97, // `a`
116103
+ lowercaseB: 98, // `b`
116104
+ lowercaseC: 99, // `c`
116105
+ lowercaseD: 100, // `d`
116106
+ lowercaseE: 101, // `e`
116107
+ lowercaseF: 102, // `f`
116108
+ lowercaseG: 103, // `g`
116109
+ lowercaseH: 104, // `h`
116110
+ lowercaseI: 105, // `i`
116111
+ lowercaseJ: 106, // `j`
116112
+ lowercaseK: 107, // `k`
116113
+ lowercaseL: 108, // `l`
116114
+ lowercaseM: 109, // `m`
116115
+ lowercaseN: 110, // `n`
116116
+ lowercaseO: 111, // `o`
116117
+ lowercaseP: 112, // `p`
116118
+ lowercaseQ: 113, // `q`
116119
+ lowercaseR: 114, // `r`
116120
+ lowercaseS: 115, // `s`
116121
+ lowercaseT: 116, // `t`
116122
+ lowercaseU: 117, // `u`
116123
+ lowercaseV: 118, // `v`
116124
+ lowercaseW: 119, // `w`
116125
+ lowercaseX: 120, // `x`
116126
+ lowercaseY: 121, // `y`
116127
+ lowercaseZ: 122, // `z`
116128
+ leftCurlyBrace: 123, // `{`
116129
+ verticalBar: 124, // `|`
116130
+ rightCurlyBrace: 125, // `}`
116131
+ tilde: 126, // `~`
116132
+ del: 127,
116133
+ // Unicode Specials block.
116134
+ byteOrderMarker: 65_279,
116135
+ // Unicode Specials block.
116136
+ replacementCharacter: 65_533 // `�`
116137
+ })
116138
+
116139
+ ;// ./lib/micromark/magic-block/syntax.ts
116140
+
116141
+
116142
+ /**
116143
+ * Known magic block types that the tokenizer will recognize.
116144
+ * Unknown types will not be tokenized as magic blocks.
116145
+ */
116146
+ const KNOWN_BLOCK_TYPES = new Set([
116147
+ 'code',
116148
+ 'api-header',
116149
+ 'image',
116150
+ 'callout',
116151
+ 'parameters',
116152
+ 'table',
116153
+ 'embed',
116154
+ 'html',
116155
+ 'recipe',
116156
+ 'tutorial-tile',
116157
+ ]);
116158
+ /**
116159
+ * Check if a character is valid for a magic block type identifier.
116160
+ * Types can contain alphanumeric characters and hyphens (e.g., "api-header", "tutorial-tile")
116161
+ */
116162
+ function isTypeChar(code) {
116163
+ return asciiAlphanumeric(code) || code === codes.dash;
116164
+ }
116165
+ /**
116166
+ * Creates the opening marker state machine: [block:
116167
+ * Returns the first state function to start parsing.
116168
+ */
116169
+ function createOpeningMarkerParser(effects, nok, onComplete) {
116170
+ const expectB = (code) => {
116171
+ if (code !== codes.lowercaseB)
116172
+ return nok(code);
116173
+ effects.consume(code);
116174
+ return expectL;
116175
+ };
116176
+ const expectL = (code) => {
116177
+ if (code !== codes.lowercaseL)
116178
+ return nok(code);
116179
+ effects.consume(code);
116180
+ return expectO;
116181
+ };
116182
+ const expectO = (code) => {
116183
+ if (code !== codes.lowercaseO)
116184
+ return nok(code);
116185
+ effects.consume(code);
116186
+ return expectC;
116187
+ };
116188
+ const expectC = (code) => {
116189
+ if (code !== codes.lowercaseC)
116190
+ return nok(code);
116191
+ effects.consume(code);
116192
+ return expectK;
116193
+ };
116194
+ const expectK = (code) => {
116195
+ if (code !== codes.lowercaseK)
116196
+ return nok(code);
116197
+ effects.consume(code);
116198
+ return expectColon;
116199
+ };
116200
+ const expectColon = (code) => {
116201
+ if (code !== codes.colon)
116202
+ return nok(code);
116203
+ effects.consume(code);
116204
+ effects.exit('magicBlockMarkerStart');
116205
+ effects.enter('magicBlockType');
116206
+ return onComplete;
116207
+ };
116208
+ return expectB;
116209
+ }
116210
+ /**
116211
+ * Creates the type capture state machine.
116212
+ * Captures type characters until ] and validates against known types.
116213
+ */
116214
+ function createTypeCaptureParser(effects, nok, onComplete, blockTypeRef) {
116215
+ const captureTypeFirst = (code) => {
116216
+ // Reject empty type name [block:]
116217
+ if (code === codes.rightSquareBracket) {
116218
+ return nok(code);
116219
+ }
116220
+ if (isTypeChar(code)) {
116221
+ blockTypeRef.value += String.fromCharCode(code);
116222
+ effects.consume(code);
116223
+ return captureType;
116224
+ }
116225
+ return nok(code);
116226
+ };
116227
+ const captureType = (code) => {
116228
+ if (code === codes.rightSquareBracket) {
116229
+ if (!KNOWN_BLOCK_TYPES.has(blockTypeRef.value)) {
116230
+ return nok(code);
116231
+ }
116232
+ effects.exit('magicBlockType');
116233
+ effects.enter('magicBlockMarkerTypeEnd');
116234
+ effects.consume(code);
116235
+ effects.exit('magicBlockMarkerTypeEnd');
116236
+ return onComplete;
116237
+ }
116238
+ if (isTypeChar(code)) {
116239
+ blockTypeRef.value += String.fromCharCode(code);
116240
+ effects.consume(code);
116241
+ return captureType;
116242
+ }
116243
+ return nok(code);
116244
+ };
116245
+ return { first: captureTypeFirst, remaining: captureType };
116246
+ }
116247
+ /**
116248
+ * Creates the closing marker state machine: /block]
116249
+ * Handles partial matches by calling onMismatch to fall back to data capture.
116250
+ */
116251
+ function createClosingMarkerParser(effects, onSuccess, onMismatch, onEof, jsonState) {
116252
+ const handleMismatch = (code) => {
116253
+ if (jsonState && code === codes.quotationMark) {
116254
+ jsonState.inString = true;
116255
+ }
116256
+ return onMismatch(code);
116257
+ };
116258
+ const expectSlash = (code) => {
116259
+ if (code === null)
116260
+ return onEof(code);
116261
+ if (code !== codes.slash)
116262
+ return handleMismatch(code);
116263
+ effects.consume(code);
116264
+ return expectB;
116265
+ };
116266
+ const expectB = (code) => {
116267
+ if (code === null)
116268
+ return onEof(code);
116269
+ if (code !== codes.lowercaseB)
116270
+ return handleMismatch(code);
116271
+ effects.consume(code);
116272
+ return expectL;
116273
+ };
116274
+ const expectL = (code) => {
116275
+ if (code === null)
116276
+ return onEof(code);
116277
+ if (code !== codes.lowercaseL)
116278
+ return handleMismatch(code);
116279
+ effects.consume(code);
116280
+ return expectO;
116281
+ };
116282
+ const expectO = (code) => {
116283
+ if (code === null)
116284
+ return onEof(code);
116285
+ if (code !== codes.lowercaseO)
116286
+ return handleMismatch(code);
116287
+ effects.consume(code);
116288
+ return expectC;
116289
+ };
116290
+ const expectC = (code) => {
116291
+ if (code === null)
116292
+ return onEof(code);
116293
+ if (code !== codes.lowercaseC)
116294
+ return handleMismatch(code);
116295
+ effects.consume(code);
116296
+ return expectK;
116297
+ };
116298
+ const expectK = (code) => {
116299
+ if (code === null)
116300
+ return onEof(code);
116301
+ if (code !== codes.lowercaseK)
116302
+ return handleMismatch(code);
116303
+ effects.consume(code);
116304
+ return expectBracket;
116305
+ };
116306
+ const expectBracket = (code) => {
116307
+ if (code === null)
116308
+ return onEof(code);
116309
+ if (code !== codes.rightSquareBracket)
116310
+ return handleMismatch(code);
116311
+ effects.consume(code);
116312
+ return onSuccess;
116313
+ };
116314
+ return { expectSlash };
116315
+ }
116316
+ /**
116317
+ * Partial construct for checking non-lazy continuation.
116318
+ * This is used by the flow tokenizer to check if we can continue
116319
+ * parsing on the next line.
116320
+ */
116321
+ const syntax_nonLazyContinuation = {
116322
+ partial: true,
116323
+ tokenize: syntax_tokenizeNonLazyContinuation,
116324
+ };
116325
+ /**
116326
+ * Tokenizer for non-lazy continuation checking.
116327
+ * Returns ok if the next line is non-lazy (can continue), nok if lazy.
116328
+ */
116329
+ function syntax_tokenizeNonLazyContinuation(effects, ok, nok) {
116330
+ const lineStart = (code) => {
116331
+ // `this` here refers to the micromark parser
116332
+ // since we are just passing functions as references and not actually calling them
116333
+ // micromarks's internal parser will call this automatically with appropriate arguments
116334
+ return this.parser.lazy[this.now().line] ? nok(code) : ok(code);
116335
+ };
116336
+ const start = (code) => {
116337
+ if (code === null) {
116338
+ return nok(code);
116339
+ }
116340
+ if (!markdownLineEnding(code)) {
116341
+ return nok(code);
116342
+ }
116343
+ effects.enter('magicBlockLineEnding');
116344
+ effects.consume(code);
116345
+ effects.exit('magicBlockLineEnding');
116346
+ return lineStart;
116347
+ };
116348
+ return start;
116349
+ }
116350
+ /**
116351
+ * Create a micromark extension for magic block syntax.
116352
+ *
116353
+ * This extension handles both single-line and multiline magic blocks:
116354
+ * - Flow construct (concrete): Handles block-level multiline magic blocks at document level
116355
+ * - Text construct: Handles inline magic blocks in lists, paragraphs, etc.
116356
+ *
116357
+ * The flow construct is marked as "concrete" which prevents it from being
116358
+ * interrupted by container markers (like `>` for blockquotes or `-` for lists).
116359
+ */
116360
+ function magicBlock() {
116361
+ return {
116362
+ // Flow construct - handles block-level magic blocks at document root
116363
+ // Marked as concrete to prevent interruption by containers
116364
+ flow: {
116365
+ [codes.leftSquareBracket]: {
116366
+ name: 'magicBlock',
116367
+ concrete: true,
116368
+ tokenize: tokenizeMagicBlockFlow,
116369
+ },
116370
+ },
116371
+ // Text construct - handles magic blocks in inline contexts (lists, paragraphs)
116372
+ text: {
116373
+ [codes.leftSquareBracket]: {
116374
+ name: 'magicBlock',
116375
+ tokenize: tokenizeMagicBlockText,
116376
+ },
116377
+ },
116378
+ };
116379
+ }
116380
+ /**
116381
+ * Flow tokenizer for block-level magic blocks (multiline).
116382
+ * Uses the continuation checking pattern from code fences.
116383
+ */
116384
+ function tokenizeMagicBlockFlow(effects, ok, nok) {
116385
+ // State for tracking JSON content
116386
+ const jsonState = { escapeNext: false, inString: false };
116387
+ const blockTypeRef = { value: '' };
116388
+ let seenOpenBrace = false;
116389
+ // Create shared parsers for opening marker and type capture
116390
+ const typeParser = createTypeCaptureParser(effects, nok, beforeData, blockTypeRef);
116391
+ const openingMarkerParser = createOpeningMarkerParser(effects, nok, typeParser.first);
116392
+ return start;
116393
+ function start(code) {
116394
+ if (code !== codes.leftSquareBracket)
116395
+ return nok(code);
116396
+ effects.enter('magicBlock');
116397
+ effects.enter('magicBlockMarkerStart');
116398
+ effects.consume(code);
116399
+ return openingMarkerParser;
116400
+ }
116401
+ /**
116402
+ * State before data content - handles line endings or starts data capture.
116403
+ */
116404
+ function beforeData(code) {
116405
+ // EOF - magic block must be closed
116406
+ if (code === null) {
116407
+ effects.exit('magicBlock');
116408
+ return nok(code);
116409
+ }
116410
+ // Line ending before any data - check continuation
116411
+ if (markdownLineEnding(code)) {
116412
+ return effects.check(syntax_nonLazyContinuation, continuationOkBeforeData, after)(code);
116413
+ }
116414
+ // Check for closing marker directly (without entering data)
116415
+ // This handles cases like [block:type]\n{}\n\n[/block] where there are
116416
+ // newlines after the data object
116417
+ if (code === codes.leftSquareBracket) {
116418
+ effects.enter('magicBlockMarkerEnd');
116419
+ effects.consume(code);
116420
+ return expectSlashFromContinuation;
116421
+ }
116422
+ // If we've already seen the opening brace, just continue capturing data
116423
+ if (seenOpenBrace) {
116424
+ effects.enter('magicBlockData');
116425
+ return captureData(code);
116426
+ }
116427
+ // Skip whitespace (spaces/tabs) before the data - stay in beforeData
116428
+ // Don't enter magicBlockData token yet until we confirm there's a '{'
116429
+ if (code === codes.space || code === codes.horizontalTab) {
116430
+ return beforeDataWhitespace(code);
116431
+ }
116432
+ // Data must start with '{' for valid JSON
116433
+ if (code !== codes.leftCurlyBrace) {
116434
+ effects.exit('magicBlock');
116435
+ return nok(code);
116436
+ }
116437
+ // We have '{' - enter data token and start capturing
116438
+ seenOpenBrace = true;
116439
+ effects.enter('magicBlockData');
116440
+ return captureData(code);
116441
+ }
116442
+ /**
116443
+ * Consume whitespace before the data without creating a token.
116444
+ * Uses a temporary token to satisfy micromark's requirement.
116445
+ */
116446
+ function beforeDataWhitespace(code) {
116447
+ if (code === null) {
116448
+ effects.exit('magicBlock');
116449
+ return nok(code);
116450
+ }
116451
+ if (markdownLineEnding(code)) {
116452
+ return effects.check(syntax_nonLazyContinuation, continuationOkBeforeData, after)(code);
116453
+ }
116454
+ if (code === codes.space || code === codes.horizontalTab) {
116455
+ // We need to consume this but can't without a token - use magicBlockData
116456
+ // and track that we haven't seen '{' yet
116457
+ effects.enter('magicBlockData');
116458
+ effects.consume(code);
116459
+ return beforeDataWhitespaceContinue;
116460
+ }
116461
+ if (code === codes.leftCurlyBrace) {
116462
+ seenOpenBrace = true;
116463
+ effects.enter('magicBlockData');
116464
+ return captureData(code);
116465
+ }
116466
+ effects.exit('magicBlock');
116467
+ return nok(code);
116468
+ }
116469
+ /**
116470
+ * Continue consuming whitespace or validate the opening brace.
116471
+ */
116472
+ function beforeDataWhitespaceContinue(code) {
116473
+ if (code === null) {
116474
+ effects.exit('magicBlockData');
116475
+ effects.exit('magicBlock');
116476
+ return nok(code);
116477
+ }
116478
+ if (markdownLineEnding(code)) {
116479
+ effects.exit('magicBlockData');
116480
+ return effects.check(syntax_nonLazyContinuation, continuationOk, after)(code);
116481
+ }
116482
+ if (code === codes.space || code === codes.horizontalTab) {
116483
+ effects.consume(code);
116484
+ return beforeDataWhitespaceContinue;
116485
+ }
116486
+ if (code === codes.leftCurlyBrace) {
116487
+ seenOpenBrace = true;
116488
+ return captureData(code);
116489
+ }
116490
+ effects.exit('magicBlockData');
116491
+ effects.exit('magicBlock');
116492
+ return nok(code);
116493
+ }
116494
+ /**
116495
+ * Continuation OK before we've entered data token.
116496
+ */
116497
+ function continuationOkBeforeData(code) {
116498
+ effects.enter('magicBlockLineEnding');
116499
+ effects.consume(code);
116500
+ effects.exit('magicBlockLineEnding');
116501
+ return beforeData; // Stay in beforeData state - don't enter magicBlockData yet
116502
+ }
116503
+ function captureData(code) {
116504
+ // EOF - magic block must be closed
116505
+ if (code === null) {
116506
+ effects.exit('magicBlockData');
116507
+ effects.exit('magicBlock');
116508
+ return nok(code);
116509
+ }
116510
+ // At line ending, check if we can continue on the next line
116511
+ if (markdownLineEnding(code)) {
116512
+ effects.exit('magicBlockData');
116513
+ return effects.check(syntax_nonLazyContinuation, continuationOk, after)(code);
116514
+ }
116515
+ if (jsonState.escapeNext) {
116516
+ jsonState.escapeNext = false;
116517
+ effects.consume(code);
116518
+ return captureData;
116519
+ }
116520
+ if (jsonState.inString) {
116521
+ if (code === codes.backslash) {
116522
+ jsonState.escapeNext = true;
116523
+ effects.consume(code);
116524
+ return captureData;
116525
+ }
116526
+ if (code === codes.quotationMark) {
116527
+ jsonState.inString = false;
116528
+ }
116529
+ effects.consume(code);
116530
+ return captureData;
116531
+ }
116532
+ if (code === codes.quotationMark) {
116533
+ jsonState.inString = true;
116534
+ effects.consume(code);
116535
+ return captureData;
116536
+ }
116537
+ if (code === codes.leftSquareBracket) {
116538
+ effects.exit('magicBlockData');
116539
+ effects.enter('magicBlockMarkerEnd');
116540
+ effects.consume(code);
116541
+ return expectSlash;
116542
+ }
116543
+ effects.consume(code);
116544
+ return captureData;
116545
+ }
116546
+ /**
116547
+ * Called when non-lazy continuation check passes - we can continue parsing.
116548
+ */
116549
+ function continuationOk(code) {
116550
+ // Consume the line ending
116551
+ effects.enter('magicBlockLineEnding');
116552
+ effects.consume(code);
116553
+ effects.exit('magicBlockLineEnding');
116554
+ return continuationStart;
116555
+ }
116556
+ /**
116557
+ * Start of continuation line - check for more line endings, closing marker, or start capturing data.
116558
+ */
116559
+ function continuationStart(code) {
116560
+ // Handle consecutive line endings
116561
+ if (code === null) {
116562
+ effects.exit('magicBlock');
116563
+ return nok(code);
116564
+ }
116565
+ if (markdownLineEnding(code)) {
116566
+ return effects.check(syntax_nonLazyContinuation, continuationOkBeforeData, after)(code);
116567
+ }
116568
+ // Check if this is the start of the closing marker [/block]
116569
+ // If so, handle it directly without entering magicBlockData
116570
+ if (code === codes.leftSquareBracket) {
116571
+ effects.enter('magicBlockMarkerEnd');
116572
+ effects.consume(code);
116573
+ return expectSlashFromContinuation;
116574
+ }
116575
+ effects.enter('magicBlockData');
116576
+ return captureData(code);
116577
+ }
116578
+ /**
116579
+ * Check for closing marker slash when coming from continuation.
116580
+ * If not a closing marker, create an empty data token and continue.
116581
+ */
116582
+ function expectSlashFromContinuation(code) {
116583
+ if (code === null) {
116584
+ effects.exit('magicBlockMarkerEnd');
116585
+ effects.exit('magicBlock');
116586
+ return nok(code);
116587
+ }
116588
+ if (markdownLineEnding(code)) {
116589
+ effects.exit('magicBlockMarkerEnd');
116590
+ return effects.check(syntax_nonLazyContinuation, continuationOkBeforeData, after)(code);
116591
+ }
116592
+ if (code === codes.slash) {
116593
+ effects.consume(code);
116594
+ return expectClosingB;
116595
+ }
116596
+ // Not a closing marker - this is data content
116597
+ // Exit marker and enter data
116598
+ effects.exit('magicBlockMarkerEnd');
116599
+ effects.enter('magicBlockData');
116600
+ // The [ was consumed by the marker, so we need to conceptually "have it" in our data
116601
+ // But since we already consumed it into the marker, we need a different approach
116602
+ // Re-classify: consume this character as data and continue
116603
+ if (code === codes.quotationMark)
116604
+ jsonState.inString = true;
116605
+ effects.consume(code);
116606
+ return captureData;
116607
+ }
116608
+ function expectSlash(code) {
116609
+ if (code === null) {
116610
+ effects.exit('magicBlockMarkerEnd');
116611
+ effects.exit('magicBlock');
116612
+ return nok(code);
116613
+ }
116614
+ // At line ending during marker parsing
116615
+ if (markdownLineEnding(code)) {
116616
+ effects.exit('magicBlockMarkerEnd');
116617
+ return effects.check(syntax_nonLazyContinuation, continuationOk, after)(code);
116618
+ }
116619
+ if (code !== codes.slash) {
116620
+ effects.exit('magicBlockMarkerEnd');
116621
+ effects.enter('magicBlockData');
116622
+ if (code === codes.quotationMark)
116623
+ jsonState.inString = true;
116624
+ effects.consume(code);
116625
+ return captureData;
116626
+ }
116627
+ effects.consume(code);
116628
+ return expectClosingB;
116629
+ }
116630
+ function expectClosingB(code) {
116631
+ if (code === null) {
116632
+ effects.exit('magicBlockMarkerEnd');
116633
+ effects.exit('magicBlock');
116634
+ return nok(code);
116635
+ }
116636
+ if (code !== codes.lowercaseB) {
116637
+ effects.exit('magicBlockMarkerEnd');
116638
+ effects.enter('magicBlockData');
116639
+ if (code === codes.quotationMark)
116640
+ jsonState.inString = true;
116641
+ effects.consume(code);
116642
+ return captureData;
116643
+ }
116644
+ effects.consume(code);
116645
+ return expectClosingL;
116646
+ }
116647
+ function expectClosingL(code) {
116648
+ if (code === null) {
116649
+ effects.exit('magicBlockMarkerEnd');
116650
+ effects.exit('magicBlock');
116651
+ return nok(code);
116652
+ }
116653
+ if (code !== codes.lowercaseL) {
116654
+ effects.exit('magicBlockMarkerEnd');
116655
+ effects.enter('magicBlockData');
116656
+ if (code === codes.quotationMark)
116657
+ jsonState.inString = true;
116658
+ effects.consume(code);
116659
+ return captureData;
116660
+ }
116661
+ effects.consume(code);
116662
+ return expectClosingO;
116663
+ }
116664
+ function expectClosingO(code) {
116665
+ if (code === null) {
116666
+ effects.exit('magicBlockMarkerEnd');
116667
+ effects.exit('magicBlock');
116668
+ return nok(code);
116669
+ }
116670
+ if (code !== codes.lowercaseO) {
116671
+ effects.exit('magicBlockMarkerEnd');
116672
+ effects.enter('magicBlockData');
116673
+ if (code === codes.quotationMark)
116674
+ jsonState.inString = true;
116675
+ effects.consume(code);
116676
+ return captureData;
116677
+ }
116678
+ effects.consume(code);
116679
+ return expectClosingC;
116680
+ }
116681
+ function expectClosingC(code) {
116682
+ if (code === null) {
116683
+ effects.exit('magicBlockMarkerEnd');
116684
+ effects.exit('magicBlock');
116685
+ return nok(code);
116686
+ }
116687
+ if (code !== codes.lowercaseC) {
116688
+ effects.exit('magicBlockMarkerEnd');
116689
+ effects.enter('magicBlockData');
116690
+ if (code === codes.quotationMark)
116691
+ jsonState.inString = true;
116692
+ effects.consume(code);
116693
+ return captureData;
116694
+ }
116695
+ effects.consume(code);
116696
+ return expectClosingK;
116697
+ }
116698
+ function expectClosingK(code) {
116699
+ if (code === null) {
116700
+ effects.exit('magicBlockMarkerEnd');
116701
+ effects.exit('magicBlock');
116702
+ return nok(code);
116703
+ }
116704
+ if (code !== codes.lowercaseK) {
116705
+ effects.exit('magicBlockMarkerEnd');
116706
+ effects.enter('magicBlockData');
116707
+ if (code === codes.quotationMark)
116708
+ jsonState.inString = true;
116709
+ effects.consume(code);
116710
+ return captureData;
116711
+ }
116712
+ effects.consume(code);
116713
+ return expectClosingBracket;
116714
+ }
116715
+ function expectClosingBracket(code) {
116716
+ if (code === null) {
116717
+ effects.exit('magicBlockMarkerEnd');
116718
+ effects.exit('magicBlock');
116719
+ return nok(code);
116720
+ }
116721
+ if (code !== codes.rightSquareBracket) {
116722
+ effects.exit('magicBlockMarkerEnd');
116723
+ effects.enter('magicBlockData');
116724
+ if (code === codes.quotationMark)
116725
+ jsonState.inString = true;
116726
+ effects.consume(code);
116727
+ return captureData;
116728
+ }
116729
+ effects.consume(code);
116730
+ effects.exit('magicBlockMarkerEnd');
116731
+ // Check for trailing whitespace on the same line
116732
+ return consumeTrailing;
116733
+ }
116734
+ /**
116735
+ * Consume trailing whitespace (spaces/tabs) on the same line after [/block].
116736
+ * For concrete flow constructs, we must end at eol/eof or fail.
116737
+ * Trailing whitespace is consumed; other content causes nok (text tokenizer handles it).
116738
+ */
116739
+ function consumeTrailing(code) {
116740
+ // End of file - done
116741
+ if (code === null) {
116742
+ effects.exit('magicBlock');
116743
+ return ok(code);
116744
+ }
116745
+ // Line ending - done
116746
+ if (markdownLineEnding(code)) {
116747
+ effects.exit('magicBlock');
116748
+ return ok(code);
116749
+ }
116750
+ // Space or tab - consume as trailing whitespace
116751
+ if (code === codes.space || code === codes.horizontalTab) {
116752
+ effects.enter('magicBlockTrailing');
116753
+ effects.consume(code);
116754
+ return consumeTrailingContinue;
116755
+ }
116756
+ // Any other character - fail flow tokenizer, let text tokenizer handle it
116757
+ effects.exit('magicBlock');
116758
+ return nok(code);
116759
+ }
116760
+ /**
116761
+ * Continue consuming trailing whitespace.
116762
+ */
116763
+ function consumeTrailingContinue(code) {
116764
+ // End of file - done
116765
+ if (code === null) {
116766
+ effects.exit('magicBlockTrailing');
116767
+ effects.exit('magicBlock');
116768
+ return ok(code);
116769
+ }
116770
+ // Line ending - done
116771
+ if (markdownLineEnding(code)) {
116772
+ effects.exit('magicBlockTrailing');
116773
+ effects.exit('magicBlock');
116774
+ return ok(code);
116775
+ }
116776
+ // More space or tab - keep consuming
116777
+ if (code === codes.space || code === codes.horizontalTab) {
116778
+ effects.consume(code);
116779
+ return consumeTrailingContinue;
116780
+ }
116781
+ // Non-whitespace after whitespace - fail flow tokenizer
116782
+ effects.exit('magicBlockTrailing');
116783
+ effects.exit('magicBlock');
116784
+ return nok(code);
116785
+ }
116786
+ /**
116787
+ * Called when we can't continue (lazy line or EOF) - exit the construct.
116788
+ */
116789
+ function after(code) {
116790
+ effects.exit('magicBlock');
116791
+ return nok(code);
116792
+ }
116793
+ }
116794
+ /**
116795
+ * Text tokenizer for single-line magic blocks only.
116796
+ * Used in inline contexts like list items and paragraphs.
116797
+ * Multiline blocks are handled by the flow tokenizer.
116798
+ */
116799
+ function tokenizeMagicBlockText(effects, ok, nok) {
116800
+ // State for tracking JSON content
116801
+ const jsonState = { escapeNext: false, inString: false };
116802
+ const blockTypeRef = { value: '' };
116803
+ let seenOpenBrace = false;
116804
+ // Create shared parsers for opening marker and type capture
116805
+ const typeParser = createTypeCaptureParser(effects, nok, beforeData, blockTypeRef);
116806
+ const openingMarkerParser = createOpeningMarkerParser(effects, nok, typeParser.first);
116807
+ // Success handler for closing marker - exits tokens and returns ok
116808
+ const closingSuccess = (code) => {
116809
+ effects.exit('magicBlockMarkerEnd');
116810
+ effects.exit('magicBlock');
116811
+ return ok(code);
116812
+ };
116813
+ // Mismatch handler - falls back to data capture
116814
+ const closingMismatch = (code) => {
116815
+ effects.exit('magicBlockMarkerEnd');
116816
+ effects.enter('magicBlockData');
116817
+ if (code === codes.quotationMark)
116818
+ jsonState.inString = true;
116819
+ effects.consume(code);
116820
+ return captureData;
116821
+ };
116822
+ // EOF handler
116823
+ const closingEof = (_code) => nok(_code);
116824
+ // Create closing marker parsers
116825
+ const closingFromBeforeData = createClosingMarkerParser(effects, closingSuccess, closingMismatch, closingEof, jsonState);
116826
+ const closingFromData = createClosingMarkerParser(effects, closingSuccess, closingMismatch, closingEof, jsonState);
116827
+ return start;
116828
+ function start(code) {
116829
+ if (code !== codes.leftSquareBracket)
116830
+ return nok(code);
116831
+ effects.enter('magicBlock');
116832
+ effects.enter('magicBlockMarkerStart');
116833
+ effects.consume(code);
116834
+ return openingMarkerParser;
116835
+ }
116836
+ /**
116837
+ * State before data content - handles line endings before entering data token.
116838
+ * Whitespace before '{' is allowed.
116839
+ */
116840
+ function beforeData(code) {
116841
+ // Fail on EOF - magic block must be closed
116842
+ if (code === null) {
116843
+ return nok(code);
116844
+ }
116845
+ // Handle line endings before any data - consume them without entering data token
116846
+ if (markdownLineEnding(code)) {
116847
+ effects.enter('magicBlockLineEnding');
116848
+ effects.consume(code);
116849
+ effects.exit('magicBlockLineEnding');
116850
+ return beforeData;
116851
+ }
116852
+ // Check for closing marker directly (without entering data)
116853
+ if (code === codes.leftSquareBracket) {
116854
+ effects.enter('magicBlockMarkerEnd');
116855
+ effects.consume(code);
116856
+ return closingFromBeforeData.expectSlash;
116857
+ }
116858
+ // If we've already seen the opening brace, just continue capturing data
116859
+ if (seenOpenBrace) {
116860
+ effects.enter('magicBlockData');
116861
+ return captureData(code);
116862
+ }
116863
+ // Skip whitespace (spaces/tabs) before the data
116864
+ if (code === codes.space || code === codes.horizontalTab) {
116865
+ return beforeDataWhitespace(code);
116866
+ }
116867
+ // Data must start with '{' for valid JSON
116868
+ if (code !== codes.leftCurlyBrace) {
116869
+ return nok(code);
116870
+ }
116871
+ // We have '{' - enter data token and start capturing
116872
+ seenOpenBrace = true;
116873
+ effects.enter('magicBlockData');
116874
+ return captureData(code);
116875
+ }
116876
+ /**
116877
+ * Consume whitespace before the data.
116878
+ */
116879
+ function beforeDataWhitespace(code) {
116880
+ if (code === null) {
116881
+ return nok(code);
116882
+ }
116883
+ if (markdownLineEnding(code)) {
116884
+ effects.enter('magicBlockLineEnding');
116885
+ effects.consume(code);
116886
+ effects.exit('magicBlockLineEnding');
116887
+ return beforeData;
116888
+ }
116889
+ if (code === codes.space || code === codes.horizontalTab) {
116890
+ effects.enter('magicBlockData');
116891
+ effects.consume(code);
116892
+ return beforeDataWhitespaceContinue;
116893
+ }
116894
+ if (code === codes.leftCurlyBrace) {
116895
+ seenOpenBrace = true;
116896
+ effects.enter('magicBlockData');
116897
+ return captureData(code);
116898
+ }
116899
+ return nok(code);
116900
+ }
116901
+ /**
116902
+ * Continue consuming whitespace or validate the opening brace.
116903
+ */
116904
+ function beforeDataWhitespaceContinue(code) {
116905
+ if (code === null) {
116906
+ effects.exit('magicBlockData');
116907
+ return nok(code);
116908
+ }
116909
+ if (markdownLineEnding(code)) {
116910
+ effects.exit('magicBlockData');
116911
+ effects.enter('magicBlockLineEnding');
116912
+ effects.consume(code);
116913
+ effects.exit('magicBlockLineEnding');
116914
+ return beforeData;
116915
+ }
116916
+ if (code === codes.space || code === codes.horizontalTab) {
116917
+ effects.consume(code);
116918
+ return beforeDataWhitespaceContinue;
116919
+ }
116920
+ if (code === codes.leftCurlyBrace) {
116921
+ seenOpenBrace = true;
116922
+ return captureData(code);
116923
+ }
116924
+ effects.exit('magicBlockData');
116925
+ return nok(code);
116926
+ }
116927
+ function captureData(code) {
116928
+ // Fail on EOF - magic block must be closed
116929
+ if (code === null) {
116930
+ effects.exit('magicBlockData');
116931
+ return nok(code);
116932
+ }
116933
+ // Handle multiline magic blocks within text/paragraphs
116934
+ // Exit data, consume line ending with proper token, then re-enter data
116935
+ if (markdownLineEnding(code)) {
116936
+ effects.exit('magicBlockData');
116937
+ effects.enter('magicBlockLineEnding');
116938
+ effects.consume(code);
116939
+ effects.exit('magicBlockLineEnding');
116940
+ return beforeData; // Go back to beforeData to handle potential empty lines
116941
+ }
116942
+ if (jsonState.escapeNext) {
116943
+ jsonState.escapeNext = false;
116944
+ effects.consume(code);
116945
+ return captureData;
116946
+ }
116947
+ if (jsonState.inString) {
116948
+ if (code === codes.backslash) {
116949
+ jsonState.escapeNext = true;
116950
+ effects.consume(code);
116951
+ return captureData;
116952
+ }
116953
+ if (code === codes.quotationMark) {
116954
+ jsonState.inString = false;
116955
+ }
116956
+ effects.consume(code);
116957
+ return captureData;
116958
+ }
116959
+ if (code === codes.quotationMark) {
116960
+ jsonState.inString = true;
116961
+ effects.consume(code);
116962
+ return captureData;
116963
+ }
116964
+ if (code === codes.leftSquareBracket) {
116965
+ effects.exit('magicBlockData');
116966
+ effects.enter('magicBlockMarkerEnd');
116967
+ effects.consume(code);
116968
+ return closingFromData.expectSlash;
116969
+ }
116970
+ effects.consume(code);
116971
+ return captureData;
116972
+ }
116973
+ }
116974
+ /* harmony default export */ const syntax = ((/* unused pure expression or super */ null && (magicBlock)));
116975
+
116976
+ ;// ./lib/micromark/magic-block/index.ts
116977
+ /**
116978
+ * Micromark extension for magic block syntax.
116979
+ *
116980
+ * Usage:
116981
+ * ```ts
116982
+ * import { magicBlock } from './lib/micromark/magic-block';
116983
+ *
116984
+ * const processor = unified()
116985
+ * .data('micromarkExtensions', [magicBlock()])
116986
+ * ```
116987
+ */
116988
+
115302
116989
 
115303
116990
  ;// ./lib/utils/mdxish/mdxish-load-components.ts
115304
116991
 
@@ -115365,46 +117052,56 @@ function loadComponents() {
115365
117052
 
115366
117053
 
115367
117054
 
117055
+
117056
+
115368
117057
 
115369
117058
 
115370
117059
 
115371
117060
  const defaultTransformers = [callouts, code_tabs, gemoji_, transform_embeds];
115372
117061
  function mdxishAstProcessor(mdContent, opts = {}) {
115373
- const { components: userComponents = {}, jsxContext = {}, useTailwind } = opts;
117062
+ const { components: userComponents = {}, jsxContext = {}, newEditorTypes = false, safeMode = false, useTailwind, } = opts;
115374
117063
  const components = {
115375
117064
  ...loadComponents(),
115376
117065
  ...userComponents,
115377
117066
  };
117067
+ // Build set of known component names for snake_case filtering
117068
+ const knownComponents = new Set(Object.keys(components));
115378
117069
  // Preprocessing pipeline: Transform content to be parser-ready
115379
- // Step 1: Extract legacy magic blocks
115380
- const { replaced: contentAfterMagicBlocks, blocks } = extractMagicBlocks(mdContent);
115381
- // Step 2: Normalize malformed table separator syntax (e.g., `|: ---` → `| :---`)
115382
- const contentAfterTableNormalization = normalizeTableSeparator(contentAfterMagicBlocks);
115383
- // Step 3: Evaluate JSX expressions in attributes
115384
- const contentAfterJSXEvaluation = preprocessJSXExpressions(contentAfterTableNormalization, jsxContext);
115385
- // Step 4: Replace snake_case component names with parser-safe placeholders
115386
- // (e.g., <Snake_case /> <MDXishSnakeCase0 /> which will be restored after parsing)
115387
- const { content: parserReadyContent, mapping: snakeCaseMapping } = processSnakeCaseComponent(contentAfterJSXEvaluation);
117070
+ // Step 1: Normalize malformed table separator syntax (e.g., `|: ---` → `| :---`)
117071
+ const contentAfterTableNormalization = normalizeTableSeparator(mdContent);
117072
+ // Step 2: Evaluate JSX expressions in attributes
117073
+ const contentAfterJSXEvaluation = safeMode
117074
+ ? contentAfterTableNormalization
117075
+ : preprocessJSXExpressions(contentAfterTableNormalization, jsxContext);
117076
+ // Step 3: Replace snake_case component names with parser-safe placeholders
117077
+ const { content: parserReadyContent, mapping: snakeCaseMapping } = processSnakeCaseComponent(contentAfterJSXEvaluation, { knownComponents });
115388
117078
  // Create string map for tailwind transformer
115389
117079
  const tempComponentsMap = Object.entries(components).reduce((acc, [key, value]) => {
115390
117080
  acc[key] = String(value);
115391
117081
  return acc;
115392
117082
  }, {});
117083
+ // Get mdxExpression extension and remove its flow construct to prevent
117084
+ // `{...}` from interrupting paragraphs (which breaks multiline magic blocks)
117085
+ const mdxExprExt = mdxExpression({ allowEmpty: true });
117086
+ const mdxExprTextOnly = {
117087
+ text: mdxExprExt.text,
117088
+ };
115393
117089
  const processor = unified()
115394
- .data('micromarkExtensions', [mdxExpression({ allowEmpty: true })]) // Parse inline JSX expressions as AST nodes for later evaluation
115395
- .data('fromMarkdownExtensions', [mdxExpressionFromMarkdown()])
117090
+ .data('micromarkExtensions', safeMode ? [magicBlock()] : [magicBlock(), mdxExprTextOnly])
117091
+ .data('fromMarkdownExtensions', safeMode ? [magicBlockFromMarkdown()] : [magicBlockFromMarkdown(), mdxExpressionFromMarkdown()])
115396
117092
  .use(remarkParse)
115397
117093
  .use(remarkFrontmatter)
115398
117094
  .use(normalize_malformed_md_syntax)
115399
- .use(mdxish_magic_blocks, { blocks })
117095
+ .use(magic_block_transformer)
115400
117096
  .use(transform_images, { isMdxish: true })
115401
117097
  .use(defaultTransformers)
115402
117098
  .use(mdxish_component_blocks)
115403
117099
  .use(restore_snake_case_component_name, { mapping: snakeCaseMapping })
115404
117100
  .use(mdxish_tables)
115405
117101
  .use(mdxish_html_blocks)
115406
- .use(evaluate_expressions, { context: jsxContext }) // Evaluate MDX expressions using jsxContext
115407
- .use(variables_text) // Parse {user.*} patterns from text (can't rely on remarkMdx)
117102
+ .use(newEditorTypes ? mdxish_jsx_to_mdast : undefined) // Convert JSX elements to MDAST types
117103
+ .use(safeMode ? undefined : evaluate_expressions, { context: jsxContext }) // Evaluate MDX expressions using jsxContext
117104
+ .use(variables_text) // Parse {user.*} patterns from text
115408
117105
  .use(useTailwind ? transform_tailwind : undefined, { components: tempComponentsMap })
115409
117106
  .use(remarkGfm);
115410
117107
  return {
@@ -115421,10 +117118,14 @@ function mdxishAstProcessor(mdContent, opts = {}) {
115421
117118
  * Converts an Mdast to a Markdown string.
115422
117119
  */
115423
117120
  function mdxishMdastToMd(mdast) {
115424
- const md = unified().use(remarkGfm).use(processor_compile).use(remarkStringify, {
117121
+ const md = unified()
117122
+ .use(remarkGfm)
117123
+ .use(mdxishCompilers)
117124
+ .use(remarkStringify, {
115425
117125
  bullet: '-',
115426
117126
  emphasis: '_',
115427
- }).stringify(mdast);
117127
+ })
117128
+ .stringify(mdast);
115428
117129
  return md;
115429
117130
  }
115430
117131
  /**
@@ -115920,11 +117621,14 @@ const tags = (doc) => {
115920
117621
 
115921
117622
 
115922
117623
 
117624
+
115923
117625
  const mdxishTags_tags = (doc) => {
115924
- const { replaced: sanitizedDoc } = extractMagicBlocks(doc);
115925
117626
  const set = new Set();
115926
- const processor = remark().use(mdxish_component_blocks);
115927
- const tree = processor.parse(sanitizedDoc);
117627
+ const processor = remark()
117628
+ .data('micromarkExtensions', [magicBlock()])
117629
+ .data('fromMarkdownExtensions', [magicBlockFromMarkdown()])
117630
+ .use(mdxish_component_blocks);
117631
+ const tree = processor.parse(doc);
115928
117632
  visit(processor.runSync(tree), isMDXElement, (node) => {
115929
117633
  if (node.name?.match(/^[A-Z]/)) {
115930
117634
  set.add(node.name);
@@ -115945,12 +117649,15 @@ const mdxishTags_tags = (doc) => {
115945
117649
 
115946
117650
 
115947
117651
 
117652
+
115948
117653
  /**
115949
117654
  * Removes Markdown and MDX comments.
115950
117655
  */
115951
117656
  async function stripComments(doc, { mdx, mdxish } = {}) {
115952
- const { replaced, blocks } = extractMagicBlocks(doc);
115953
- const processor = unified();
117657
+ const processor = unified()
117658
+ .data('micromarkExtensions', [magicBlock()])
117659
+ .data('fromMarkdownExtensions', [magicBlockFromMarkdown()])
117660
+ .data('toMarkdownExtensions', [magicBlockToMarkdown()]);
115954
117661
  // we still require these two extensions because:
115955
117662
  // 1. we can rely on remarkMdx to parse MDXish
115956
117663
  // 2. we need to parse JSX comments into mdxTextExpression nodes so that the transformers can pick them up
@@ -115992,10 +117699,8 @@ async function stripComments(doc, { mdx, mdxish } = {}) {
115992
117699
  },
115993
117700
  ],
115994
117701
  });
115995
- const file = await processor.process(replaced);
115996
- const stringified = String(file).trim();
115997
- const restored = restoreMagicBlocks(stringified, blocks);
115998
- return restored;
117702
+ const file = await processor.process(doc);
117703
+ return String(file).trim();
115999
117704
  }
116000
117705
  /* harmony default export */ const lib_stripComments = (stripComments);
116001
117706