@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.js CHANGED
@@ -11908,7 +11908,9 @@ const CreateHeading = (depth) => {
11908
11908
  ;// ./components/Image/index.tsx
11909
11909
 
11910
11910
  const Image = (Props) => {
11911
- const { align = '', alt = '', border = false, caption, className = '', height = 'auto', src, title = '', width = 'auto', lazy = true, children, } = Props;
11911
+ const { align = '', alt = '', border: borderProp = false, caption, className = '', height = 'auto', src, title = '', width = 'auto', lazy = true, children, } = Props;
11912
+ // Normalize border: MDXish passes {false} as the string "false", not a boolean
11913
+ const border = borderProp === true || borderProp === 'true';
11912
11914
  const [lightbox, setLightbox] = external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_.useState(false);
11913
11915
  if (className === 'emoji') {
11914
11916
  return external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_.createElement("img", { alt: alt, height: height, loading: lazy ? 'lazy' : 'eager', src: src, title: title, width: width });
@@ -14345,7 +14347,7 @@ function htmlExtension(all, extension) {
14345
14347
 
14346
14348
  ;// ./node_modules/micromark-util-character/index.js
14347
14349
  /**
14348
- * @typedef {import('micromark-util-types').Code} Code
14350
+ * @import {Code} from 'micromark-util-types'
14349
14351
  */
14350
14352
 
14351
14353
  /**
@@ -14571,7 +14573,9 @@ const unicodeWhitespace = regexCheck(/\s/);
14571
14573
  * Create a code check from a regex.
14572
14574
  *
14573
14575
  * @param {RegExp} regex
14576
+ * Expression.
14574
14577
  * @returns {(code: Code) => boolean}
14578
+ * Check.
14575
14579
  */
14576
14580
  function regexCheck(regex) {
14577
14581
  return check;
@@ -53098,6 +53102,59 @@ const isCalloutStructure = (node) => {
53098
53102
  const firstTextChild = firstChild.children?.[0];
53099
53103
  return firstTextChild?.type === 'text';
53100
53104
  };
53105
+ /**
53106
+ * Finds the first text node containing a newline in a paragraph's children.
53107
+ * Returns the index and the newline position within that text node.
53108
+ */
53109
+ const findNewlineInParagraph = (paragraph) => {
53110
+ for (let i = 0; i < paragraph.children.length; i += 1) {
53111
+ const child = paragraph.children[i];
53112
+ if (child.type === 'text' && typeof child.value === 'string') {
53113
+ const newlineIndex = child.value.indexOf('\n');
53114
+ if (newlineIndex !== -1) {
53115
+ return { index: i, newlineIndex };
53116
+ }
53117
+ }
53118
+ }
53119
+ return null;
53120
+ };
53121
+ /**
53122
+ * Splits a paragraph at the first newline, separating heading content (before \n)
53123
+ * from body content (after \n). Mutates the paragraph to contain only heading children.
53124
+ */
53125
+ const splitParagraphAtNewline = (paragraph) => {
53126
+ const splitPoint = findNewlineInParagraph(paragraph);
53127
+ if (!splitPoint)
53128
+ return null;
53129
+ const { index, newlineIndex } = splitPoint;
53130
+ const originalChildren = paragraph.children;
53131
+ const textNode = originalChildren[index];
53132
+ const beforeNewline = textNode.value.slice(0, newlineIndex);
53133
+ const afterNewline = textNode.value.slice(newlineIndex + 1);
53134
+ // Split paragraph: heading = children[0..index-1] + text before newline
53135
+ const headingChildren = originalChildren.slice(0, index);
53136
+ if (beforeNewline.length > 0 || headingChildren.length === 0) {
53137
+ headingChildren.push({ type: 'text', value: beforeNewline });
53138
+ }
53139
+ paragraph.children = headingChildren;
53140
+ // Body = text after newline + remaining children from original array
53141
+ const bodyChildren = [];
53142
+ if (afterNewline.length > 0) {
53143
+ bodyChildren.push({ type: 'text', value: afterNewline });
53144
+ }
53145
+ bodyChildren.push(...originalChildren.slice(index + 1));
53146
+ return bodyChildren.length > 0 ? bodyChildren : null;
53147
+ };
53148
+ /**
53149
+ * Removes the icon/match prefix from the first text node in a paragraph.
53150
+ * This is needed to clean up the raw AST after we've extracted the icon.
53151
+ */
53152
+ const removeIconPrefix = (paragraph, prefixLength) => {
53153
+ const firstTextNode = findFirst(paragraph);
53154
+ if (firstTextNode && 'value' in firstTextNode && typeof firstTextNode.value === 'string') {
53155
+ firstTextNode.value = firstTextNode.value.slice(prefixLength);
53156
+ }
53157
+ };
53101
53158
  const processBlockquote = (node, index, parent) => {
53102
53159
  if (!isCalloutStructure(node)) {
53103
53160
  // Only stringify empty blockquotes (no extractable text content)
@@ -53122,22 +53179,39 @@ const processBlockquote = (node, index, parent) => {
53122
53179
  const firstParagraph = node.children[0];
53123
53180
  const startText = lib_plain(firstParagraph).toString();
53124
53181
  const [match, icon] = startText.match(callouts_regex) || [];
53182
+ const firstParagraphOriginalEnd = firstParagraph.position.end;
53125
53183
  if (icon && match) {
53126
- const heading = startText.slice(match.length);
53127
- const empty = !heading.length && firstParagraph.children.length === 1;
53184
+ // Handle cases where heading and body are on the same line separated by a newline.
53185
+ // Example: "> ⚠️ **Bold heading**\nBody text here"
53186
+ const bodyChildren = splitParagraphAtNewline(firstParagraph);
53187
+ const didSplit = bodyChildren !== null;
53188
+ // Extract heading text after removing the icon prefix.
53189
+ // Use `plain()` to handle complex markdown structures (bold, inline code, etc.)
53190
+ const headingText = lib_plain(firstParagraph)
53191
+ .toString()
53192
+ .slice(match.length);
53193
+ // Clean up the raw AST by removing the icon prefix from the first text node
53194
+ removeIconPrefix(firstParagraph, match.length);
53195
+ const empty = !headingText.length && firstParagraph.children.length === 1;
53128
53196
  const theme = themes[icon] || 'default';
53129
- const firstChild = findFirst(node.children[0]);
53130
- if (firstChild && 'value' in firstChild && typeof firstChild.value === 'string') {
53131
- firstChild.value = firstChild.value.slice(match.length);
53132
- }
53133
- if (heading) {
53197
+ // Convert the first paragraph (first children of node) to a heading if it has content or was split
53198
+ if (headingText || didSplit) {
53134
53199
  node.children[0] = wrapHeading(node);
53135
- // @note: We add to the offset/column the length of the unicode
53136
- // character that was stripped off, so that the start position of the
53137
- // heading/text matches where it actually starts.
53200
+ // Adjust position to account for the stripped icon prefix
53138
53201
  node.children[0].position.start.offset += match.length;
53139
53202
  node.children[0].position.start.column += match.length;
53140
53203
  }
53204
+ // Insert body content as a separate paragraph after the heading
53205
+ if (bodyChildren) {
53206
+ node.children.splice(1, 0, {
53207
+ type: 'paragraph',
53208
+ children: bodyChildren,
53209
+ position: {
53210
+ start: node.children[0].position.end,
53211
+ end: firstParagraphOriginalEnd,
53212
+ },
53213
+ });
53214
+ }
53141
53215
  Object.assign(node, {
53142
53216
  type: NodeTypes.callout,
53143
53217
  data: {
@@ -70517,7 +70591,7 @@ const toAttributes = (object, keys = []) => {
70517
70591
  if (keys.length > 0 && !keys.includes(name))
70518
70592
  return;
70519
70593
  let value;
70520
- if (typeof v === 'undefined' || v === null || v === '') {
70594
+ if (typeof v === 'undefined' || v === null || v === '' || v === false) {
70521
70595
  return;
70522
70596
  }
70523
70597
  else if (typeof v === 'string') {
@@ -86552,7 +86626,7 @@ const CUSTOM_PROP_BOUNDARIES = [
86552
86626
  /**
86553
86627
  * Tags that should be passed through and handled at runtime (not by the mdxish plugin)
86554
86628
  */
86555
- const RUNTIME_COMPONENT_TAGS = new Set(['Variable', 'variable', 'rdme-pin']);
86629
+ const RUNTIME_COMPONENT_TAGS = new Set(['Variable', 'variable', 'html-block', 'rdme-pin']);
86556
86630
  /**
86557
86631
  * Standard HTML tags that should never be treated as custom components.
86558
86632
  * Uses the html-tags package, converted to a Set<string> for efficient lookups.
@@ -91665,6 +91739,10 @@ ${reformatHTML(html)}
91665
91739
  const plain_plain = (node) => node.value;
91666
91740
  /* harmony default export */ const compile_plain = (plain_plain);
91667
91741
 
91742
+ ;// ./processor/compile/variable.ts
91743
+ const variable = (node) => `{user.${node.data?.hProperties?.name || ''}}`;
91744
+ /* harmony default export */ const compile_variable = (variable);
91745
+
91668
91746
  ;// ./processor/compile/index.ts
91669
91747
 
91670
91748
 
@@ -91674,7 +91752,8 @@ const plain_plain = (node) => node.value;
91674
91752
 
91675
91753
 
91676
91754
 
91677
- function compilers() {
91755
+
91756
+ function compilers(mdxish = false) {
91678
91757
  const data = this.data();
91679
91758
  const toMarkdownExtensions = data.toMarkdownExtensions || (data.toMarkdownExtensions = []);
91680
91759
  const handlers = {
@@ -91685,6 +91764,7 @@ function compilers() {
91685
91764
  [NodeTypes.glossary]: compile_compatibility,
91686
91765
  [NodeTypes.htmlBlock]: html_block,
91687
91766
  [NodeTypes.reusableContent]: compile_compatibility,
91767
+ ...(mdxish && { [NodeTypes.variable]: compile_variable }),
91688
91768
  embed: compile_compatibility,
91689
91769
  escape: compile_compatibility,
91690
91770
  figure: compile_compatibility,
@@ -91695,6 +91775,9 @@ function compilers() {
91695
91775
  };
91696
91776
  toMarkdownExtensions.push({ extensions: [{ handlers }] });
91697
91777
  }
91778
+ function mdxishCompilers() {
91779
+ return compilers.call(this, true);
91780
+ }
91698
91781
  /* harmony default export */ const processor_compile = (compilers);
91699
91782
 
91700
91783
  ;// ./processor/transform/escape-pipes-in-tables.ts
@@ -93405,13 +93488,17 @@ const htmlBlockHandler = (_state, node) => {
93405
93488
  const embedHandler = (state, node) => {
93406
93489
  // Assert to get the minimum properties we need
93407
93490
  const { data } = node;
93491
+ // Magic block embeds (hName === 'embed-block') render as Embed component
93492
+ // which doesn't use children - it renders based on props only
93493
+ const isMagicBlockEmbed = data?.hName === NodeTypes.embedBlock;
93408
93494
  return {
93409
93495
  type: 'element',
93410
93496
  // To differentiate between regular embeds and magic block embeds,
93411
93497
  // magic block embeds have a certain hName
93412
- tagName: data?.hName === NodeTypes.embedBlock ? 'Embed' : 'embed',
93498
+ tagName: isMagicBlockEmbed ? 'Embed' : 'embed',
93413
93499
  properties: data?.hProperties,
93414
- children: state.all(node),
93500
+ // Don't include children for magic block embeds - Embed component renders based on props
93501
+ children: isMagicBlockEmbed ? [] : state.all(node),
93415
93502
  };
93416
93503
  };
93417
93504
  const mdxComponentHandlers = {
@@ -93424,7 +93511,102 @@ const mdxComponentHandlers = {
93424
93511
  [NodeTypes.htmlBlock]: htmlBlockHandler,
93425
93512
  };
93426
93513
 
93514
+ ;// ./lib/utils/mdxish/protect-code-blocks.ts
93515
+ /**
93516
+ * Replaces code blocks and inline code with placeholders to protect them from preprocessing.
93517
+ *
93518
+ * @param content - The markdown content to process
93519
+ * @returns Object containing protected content and arrays of original code blocks
93520
+ * @example
93521
+ * ```typescript
93522
+ * const input = 'Text with `inline code` and ```fenced block```';
93523
+ * protectCodeBlocks(input)
93524
+ * // Returns: {
93525
+ * // protectedCode: {
93526
+ * // codeBlocks: ['```fenced block```'],
93527
+ * // inlineCode: ['`inline code`']
93528
+ * // },
93529
+ * // protectedContent: 'Text with ___INLINE_CODE_0___ and ___CODE_BLOCK_0___'
93530
+ * // }
93531
+ * ```
93532
+ */
93533
+ function protectCodeBlocks(content) {
93534
+ const codeBlocks = [];
93535
+ const inlineCode = [];
93536
+ let protectedContent = '';
93537
+ let remaining = content;
93538
+ let codeBlockStart = remaining.indexOf('```');
93539
+ while (codeBlockStart !== -1) {
93540
+ protectedContent += remaining.slice(0, codeBlockStart);
93541
+ remaining = remaining.slice(codeBlockStart);
93542
+ const codeBlockEnd = remaining.indexOf('```', 3);
93543
+ if (codeBlockEnd === -1) {
93544
+ break;
93545
+ }
93546
+ const match = remaining.slice(0, codeBlockEnd + 3);
93547
+ const index = codeBlocks.length;
93548
+ codeBlocks.push(match);
93549
+ protectedContent += `___CODE_BLOCK_${index}___`;
93550
+ remaining = remaining.slice(codeBlockEnd + 3);
93551
+ codeBlockStart = remaining.indexOf('```');
93552
+ }
93553
+ protectedContent += remaining;
93554
+ protectedContent = protectedContent.replace(/`([^`\n]+)`/g, match => {
93555
+ const index = inlineCode.length;
93556
+ inlineCode.push(match);
93557
+ return `___INLINE_CODE_${index}___`;
93558
+ });
93559
+ return { protectedCode: { codeBlocks, inlineCode }, protectedContent };
93560
+ }
93561
+ /**
93562
+ * Restores inline code by replacing placeholders with original content.
93563
+ *
93564
+ * @param content - Content with inline code placeholders
93565
+ * @param protectedCode - The protected code arrays
93566
+ * @returns Content with inline code restored
93567
+ */
93568
+ function restoreInlineCode(content, protectedCode) {
93569
+ return content.replace(/___INLINE_CODE_(\d+)___/g, (_m, idx) => {
93570
+ return protectedCode.inlineCode[parseInt(idx, 10)];
93571
+ });
93572
+ }
93573
+ /**
93574
+ * Restores fenced code blocks by replacing placeholders with original content.
93575
+ *
93576
+ * @param content - Content with code block placeholders
93577
+ * @param protectedCode - The protected code arrays
93578
+ * @returns Content with code blocks restored
93579
+ */
93580
+ function restoreFencedCodeBlocks(content, protectedCode) {
93581
+ return content.replace(/___CODE_BLOCK_(\d+)___/g, (_m, idx) => {
93582
+ return protectedCode.codeBlocks[parseInt(idx, 10)];
93583
+ });
93584
+ }
93585
+ /**
93586
+ * Restores all code blocks and inline code by replacing placeholders with original content.
93587
+ *
93588
+ * @param content - Content with code placeholders
93589
+ * @param protectedCode - The protected code arrays
93590
+ * @returns Content with all code blocks and inline code restored
93591
+ * @example
93592
+ * ```typescript
93593
+ * const content = 'Text with ___INLINE_CODE_0___ and ___CODE_BLOCK_0___';
93594
+ * const protectedCode = {
93595
+ * codeBlocks: ['```js\ncode\n```'],
93596
+ * inlineCode: ['`inline`']
93597
+ * };
93598
+ * restoreCodeBlocks(content, protectedCode)
93599
+ * // Returns: 'Text with `inline` and ```js\ncode\n```'
93600
+ * ```
93601
+ */
93602
+ function restoreCodeBlocks(content, protectedCode) {
93603
+ let restored = restoreFencedCodeBlocks(content, protectedCode);
93604
+ restored = restoreInlineCode(restored, protectedCode);
93605
+ return restored;
93606
+ }
93607
+
93427
93608
  ;// ./processor/transform/mdxish/preprocess-jsx-expressions.ts
93609
+
93428
93610
  // Base64 encode (Node.js + browser compatible)
93429
93611
  function base64Encode(str) {
93430
93612
  if (typeof Buffer !== 'undefined') {
@@ -93491,52 +93673,6 @@ function protectHTMLBlockContent(content) {
93491
93673
  return `${openTag}${HTML_BLOCK_CONTENT_START}${encoded}${HTML_BLOCK_CONTENT_END}${closeTag}`;
93492
93674
  });
93493
93675
  }
93494
- /**
93495
- * Replaces code blocks and inline code with placeholders to protect them from JSX processing.
93496
- *
93497
- * @param content
93498
- * @returns Object containing protected content and arrays of original code blocks
93499
- * @example
93500
- * ```typescript
93501
- * const input = 'Text with `inline code` and ```fenced block```';
93502
- * protectCodeBlocks(input)
93503
- * // Returns: {
93504
- * // protectedCode: {
93505
- * // codeBlocks: ['```fenced block```'],
93506
- * // inlineCode: ['`inline code`']
93507
- * // },
93508
- * // protectedContent: 'Text with ___INLINE_CODE_0___ and ___CODE_BLOCK_0___'
93509
- * // }
93510
- * ```
93511
- */
93512
- function protectCodeBlocks(content) {
93513
- const codeBlocks = [];
93514
- const inlineCode = [];
93515
- let protectedContent = '';
93516
- let remaining = content;
93517
- let codeBlockStart = remaining.indexOf('```');
93518
- while (codeBlockStart !== -1) {
93519
- protectedContent += remaining.slice(0, codeBlockStart);
93520
- remaining = remaining.slice(codeBlockStart);
93521
- const codeBlockEnd = remaining.indexOf('```', 3);
93522
- if (codeBlockEnd === -1) {
93523
- break;
93524
- }
93525
- const match = remaining.slice(0, codeBlockEnd + 3);
93526
- const index = codeBlocks.length;
93527
- codeBlocks.push(match);
93528
- protectedContent += `___CODE_BLOCK_${index}___`;
93529
- remaining = remaining.slice(codeBlockEnd + 3);
93530
- codeBlockStart = remaining.indexOf('```');
93531
- }
93532
- protectedContent += remaining;
93533
- protectedContent = protectedContent.replace(/`[^`]+`/g, match => {
93534
- const index = inlineCode.length;
93535
- inlineCode.push(match);
93536
- return `___INLINE_CODE_${index}___`;
93537
- });
93538
- return { protectedCode: { codeBlocks, inlineCode }, protectedContent };
93539
- }
93540
93676
  /**
93541
93677
  * Removes JSX-style comments (e.g., { /* comment *\/ }) from content.
93542
93678
  *
@@ -93579,16 +93715,6 @@ function extractBalancedBraces(content, start) {
93579
93715
  return null;
93580
93716
  return { content: content.slice(start, pos - 1), end: pos };
93581
93717
  }
93582
- function restoreInlineCode(content, protectedCode) {
93583
- return content.replace(/___INLINE_CODE_(\d+)___/g, (_m, idx) => {
93584
- return protectedCode.inlineCode[parseInt(idx, 10)];
93585
- });
93586
- }
93587
- function restoreCodeBlocks(content, protectedCode) {
93588
- return content.replace(/___CODE_BLOCK_(\d+)___/g, (_m, idx) => {
93589
- return protectedCode.codeBlocks[parseInt(idx, 10)];
93590
- });
93591
- }
93592
93718
  /**
93593
93719
  * Escapes unbalanced braces in content to prevent MDX expression parsing errors.
93594
93720
  * Handles: already-escaped braces, string literals inside expressions, nested balanced braces.
@@ -93719,28 +93845,6 @@ function evaluateAttributeExpressions(content, context, protectedCode) {
93719
93845
  result += content.slice(lastEnd);
93720
93846
  return result;
93721
93847
  }
93722
- /**
93723
- * Restores code blocks and inline code by replacing placeholders with original content.
93724
- *
93725
- * @param content
93726
- * @param protectedCode
93727
- * @returns Content with all code blocks and inline code restored
93728
- * @example
93729
- * ```typescript
93730
- * const content = 'Text with ___INLINE_CODE_0___ and ___CODE_BLOCK_0___';
93731
- * const protectedCode = {
93732
- * codeBlocks: ['```js\ncode\n```'],
93733
- * inlineCode: ['`inline`']
93734
- * };
93735
- * restoreCodeBlocks(content, protectedCode)
93736
- * // Returns: 'Text with `inline` and ```js\ncode\n```'
93737
- * ```
93738
- */
93739
- function restoreProtectedCodes(content, protectedCode) {
93740
- let restored = restoreCodeBlocks(content, protectedCode);
93741
- restored = restoreInlineCode(restored, protectedCode);
93742
- return restored;
93743
- }
93744
93848
  /**
93745
93849
  * Preprocesses JSX-like expressions in markdown before parsing.
93746
93850
  * Inline expressions are handled separately; attribute expressions are processed here.
@@ -93763,7 +93867,7 @@ function preprocessJSXExpressions(content, context = {}) {
93763
93867
  // Step 4: Escape unbalanced braces to prevent MDX expression parsing errors
93764
93868
  processed = escapeUnbalancedBraces(processed);
93765
93869
  // Step 5: Restore protected code blocks
93766
- processed = restoreProtectedCodes(processed, protectedCode);
93870
+ processed = restoreCodeBlocks(processed, protectedCode);
93767
93871
  return processed;
93768
93872
  }
93769
93873
 
@@ -93818,360 +93922,407 @@ const evaluateExpressions = ({ context = {} } = {}) => tree => {
93818
93922
  };
93819
93923
  /* harmony default export */ const evaluate_expressions = (evaluateExpressions);
93820
93924
 
93821
- ;// ./processor/transform/mdxish/mdxish-html-blocks.ts
93822
-
93823
-
93824
-
93925
+ ;// ./processor/transform/mdxish/normalize-malformed-md-syntax.ts
93825
93926
 
93826
- /**
93827
- * Decodes HTMLBlock content that was protected during preprocessing.
93828
- * Content is wrapped in <!--RDMX_HTMLBLOCK:base64:RDMX_HTMLBLOCK-->
93829
- */
93830
- function decodeProtectedContent(content) {
93831
- // Escape special regex characters in the markers
93832
- const startEscaped = HTML_BLOCK_CONTENT_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
93833
- const endEscaped = HTML_BLOCK_CONTENT_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
93834
- const markerRegex = new RegExp(`${startEscaped}([A-Za-z0-9+/=]+)${endEscaped}`, 'g');
93835
- return content.replace(markerRegex, (_match, encoded) => {
93836
- try {
93837
- return base64Decode(encoded);
93927
+ // Marker patterns for multi-node emphasis detection
93928
+ const MARKER_PATTERNS = [
93929
+ { isBold: true, marker: '**' },
93930
+ { isBold: true, marker: '__' },
93931
+ { isBold: false, marker: '*' },
93932
+ { isBold: false, marker: '_' },
93933
+ ];
93934
+ // Patterns to detect for bold (** and __) and italic (* and _) syntax:
93935
+ // Bold: ** text**, **text **, word** text**, ** text **
93936
+ // Italic: * text*, *text *, word* text*, * text *
93937
+ // Same patterns for underscore variants
93938
+ // We use separate patterns for each marker type to allow this flexibility.
93939
+ // Pattern for ** bold **
93940
+ // Groups: 1=wordBefore, 2=marker, 3=contentWithSpaceAfter, 4=trailingSpace1, 5=contentWithSpaceBefore, 6=trailingSpace2, 7=afterChar
93941
+ // trailingSpace1 is for "** text **" pattern, trailingSpace2 is for "**text **" pattern
93942
+ const asteriskBoldRegex = /([^*\s]+)?\s*(\*\*)(?:\s+((?:[^*\n]|\*(?!\*))+?)(\s*)\2|((?:[^*\n]|\*(?!\*))+?)(\s+)\2)(\S|$)?/g;
93943
+ // Pattern for __ bold __
93944
+ const underscoreBoldRegex = /([^_\s]+)?\s*(__)(?:\s+((?:[^_\n]|_(?!_))+?)(\s*)\2|((?:[^_\n]|_(?!_))+?)(\s+)\2)(\S|$)?/g;
93945
+ // Pattern for * italic *
93946
+ const asteriskItalicRegex = /([^*\s]+)?\s*(\*)(?!\*)(?:\s+([^*\n]+?)(\s*)\2|([^*\n]+?)(\s+)\2)(\S|$)?/g;
93947
+ // Pattern for _ italic _
93948
+ const underscoreItalicRegex = /([^_\s]+)?\s*(_)(?!_)(?:\s+([^_\n]+?)(\s*)\2|([^_\n]+?)(\s+)\2)(\S|$)?/g;
93949
+ // CommonMark ignores intraword underscores or asteriks, but we want to italicize/bold the inner part
93950
+ // Pattern for intraword _word_ in words like hello_world_
93951
+ const intrawordUnderscoreItalicRegex = /(\w)_(?!_)([a-zA-Z0-9]+)_(?![\w_])/g;
93952
+ // Pattern for intraword __word__ in words like hello__world__
93953
+ const intrawordUnderscoreBoldRegex = /(\w)__([a-zA-Z0-9]+)__(?![\w_])/g;
93954
+ // Pattern for intraword *word* in words like hello*world*
93955
+ const intrawordAsteriskItalicRegex = /(\w)\*(?!\*)([a-zA-Z0-9]+)\*(?![\w*])/g;
93956
+ // Pattern for intraword **word** in words like hello**world**
93957
+ const intrawordAsteriskBoldRegex = /(\w)\*\*([a-zA-Z0-9]+)\*\*(?![\w*])/g;
93958
+ /**
93959
+ * Finds opening emphasis marker in a text value.
93960
+ * Returns marker info if found, null otherwise.
93961
+ */
93962
+ function findOpeningMarker(text) {
93963
+ const results = MARKER_PATTERNS.map(({ isBold, marker }) => {
93964
+ if (marker === '*' && text.startsWith('**'))
93965
+ return null;
93966
+ if (marker === '_' && text.startsWith('__'))
93967
+ return null;
93968
+ if (text.startsWith(marker) && text.length > marker.length) {
93969
+ return { isBold, marker, textAfter: text.slice(marker.length), textBefore: '' };
93838
93970
  }
93839
- catch {
93840
- return encoded;
93971
+ const idx = text.indexOf(marker);
93972
+ if (idx > 0 && !/\s/.test(text[idx - 1])) {
93973
+ if (marker === '*' && text.slice(idx).startsWith('**'))
93974
+ return null;
93975
+ if (marker === '_' && text.slice(idx).startsWith('__'))
93976
+ return null;
93977
+ const after = text.slice(idx + marker.length);
93978
+ if (after.length > 0) {
93979
+ return { isBold, marker, textAfter: after, textBefore: text.slice(0, idx) };
93980
+ }
93841
93981
  }
93982
+ return null;
93842
93983
  });
93984
+ return results.find(r => r !== null) ?? null;
93843
93985
  }
93844
93986
  /**
93845
- * Collects text content from a node and its children recursively
93987
+ * Finds the end/closing marker in a text node for multi-node emphasis.
93846
93988
  */
93847
- function collectTextContent(node) {
93848
- const parts = [];
93849
- if (node.type === 'text' && node.value) {
93850
- parts.push(node.value);
93851
- }
93852
- else if (node.type === 'html' && node.value) {
93853
- parts.push(node.value);
93854
- }
93855
- else if (node.type === 'inlineCode' && node.value) {
93856
- parts.push(node.value);
93857
- }
93858
- else if (node.type === 'code' && node.value) {
93859
- // Reconstruct code fence syntax (markdown parser consumes opening ```)
93860
- const lang = node.lang || '';
93861
- const fence = `\`\`\`${lang ? `${lang}\n` : ''}`;
93862
- parts.push(fence);
93863
- parts.push(node.value);
93864
- // Add newline before closing fence if missing
93865
- const closingFence = node.value.endsWith('\n') ? '```' : '\n```';
93866
- parts.push(closingFence);
93989
+ function findEndMarker(text, marker) {
93990
+ const spacePattern = ` ${marker}`;
93991
+ const spaceIdx = text.indexOf(spacePattern);
93992
+ if (spaceIdx >= 0) {
93993
+ if (marker === '*' && text.slice(spaceIdx + 1).startsWith('**'))
93994
+ return null;
93995
+ if (marker === '_' && text.slice(spaceIdx + 1).startsWith('__'))
93996
+ return null;
93997
+ return {
93998
+ textAfter: text.slice(spaceIdx + spacePattern.length),
93999
+ textBefore: text.slice(0, spaceIdx),
94000
+ };
93867
94001
  }
93868
- else if (node.children && Array.isArray(node.children)) {
93869
- node.children.forEach(child => {
93870
- if (typeof child === 'object' && child !== null) {
93871
- parts.push(collectTextContent(child));
93872
- }
93873
- });
94002
+ if (text.startsWith(marker)) {
94003
+ if (marker === '*' && text.startsWith('**'))
94004
+ return null;
94005
+ if (marker === '_' && text.startsWith('__'))
94006
+ return null;
94007
+ return {
94008
+ textAfter: text.slice(marker.length),
94009
+ textBefore: '',
94010
+ };
93874
94011
  }
93875
- return parts.join('');
94012
+ return null;
93876
94013
  }
93877
94014
  /**
93878
- * Extracts boolean attribute from HTML tag. Handles JSX (safeMode={true}) and string (safeMode="true") syntax.
93879
- * Returns "true"/"false" string to survive rehypeRaw serialization.
94015
+ * Scan children for an opening emphasis marker in a text node.
93880
94016
  */
93881
- function extractBooleanAttr(attrs, name) {
93882
- // Try JSX syntax: name={true|false}
93883
- const jsxMatch = attrs.match(new RegExp(`${name}=\\{(true|false)\\}`));
93884
- if (jsxMatch) {
93885
- return jsxMatch[1];
93886
- }
93887
- // Try string syntax: name="true"|true
93888
- const stringMatch = attrs.match(new RegExp(`${name}="?(true|false)"?`));
93889
- if (stringMatch) {
93890
- return stringMatch[1];
93891
- }
93892
- return undefined;
94017
+ function findOpeningInChildren(children) {
94018
+ let result = null;
94019
+ children.some((child, idx) => {
94020
+ if (child.type !== 'text')
94021
+ return false;
94022
+ const found = findOpeningMarker(child.value);
94023
+ if (found) {
94024
+ result = { idx, opening: found };
94025
+ return true;
94026
+ }
94027
+ return false;
94028
+ });
94029
+ return result;
93893
94030
  }
93894
94031
  /**
93895
- * Extracts runScripts attribute from HTML tag. Returns boolean for "true"/"false", string for other values, or undefined if not found.
94032
+ * Scan children (after openingIdx) for a closing emphasis marker.
93896
94033
  */
93897
- function extractRunScriptsAttr(attrs) {
93898
- const runScriptsMatch = attrs.match(/runScripts="?([^">\s]+)"?/);
93899
- if (!runScriptsMatch) {
93900
- return undefined;
93901
- }
93902
- const value = runScriptsMatch[1];
93903
- if (value === 'true') {
93904
- return true;
93905
- }
93906
- if (value === 'false') {
94034
+ function findClosingInChildren(children, openingIdx, marker) {
94035
+ let result = null;
94036
+ children.slice(openingIdx + 1).some((child, relativeIdx) => {
94037
+ if (child.type !== 'text')
94038
+ return false;
94039
+ const found = findEndMarker(child.value, marker);
94040
+ if (found) {
94041
+ result = { closingIdx: openingIdx + 1 + relativeIdx, closing: found };
94042
+ return true;
94043
+ }
93907
94044
  return false;
93908
- }
93909
- return value;
94045
+ });
94046
+ return result;
93910
94047
  }
93911
94048
  /**
93912
- * Creates an HTMLBlock node from HTML string and optional attributes
94049
+ * Build the replacement nodes for a matched emphasis pair.
93913
94050
  */
93914
- function createHTMLBlockNode(htmlString, position, runScripts, safeMode) {
93915
- return {
93916
- position,
93917
- children: [{ type: 'text', value: htmlString }],
93918
- type: NodeTypes.htmlBlock,
93919
- data: {
93920
- hName: 'html-block',
93921
- hProperties: {
93922
- html: htmlString,
93923
- ...(runScripts !== undefined && { runScripts }),
93924
- ...(safeMode !== undefined && { safeMode }),
93925
- },
93926
- },
93927
- };
94051
+ function buildReplacementNodes(container, { opening, openingIdx, closing, closingIdx }) {
94052
+ const newNodes = [];
94053
+ if (opening.textBefore) {
94054
+ newNodes.push({ type: 'text', value: `${opening.textBefore} ` });
94055
+ }
94056
+ const emphasisChildren = [];
94057
+ const openingText = opening.textAfter.replace(/^\s+/, '');
94058
+ if (openingText) {
94059
+ emphasisChildren.push({ type: 'text', value: openingText });
94060
+ }
94061
+ container.children.slice(openingIdx + 1, closingIdx).forEach(child => {
94062
+ emphasisChildren.push(child);
94063
+ });
94064
+ const closingText = closing.textBefore.replace(/\s+$/, '');
94065
+ if (closingText) {
94066
+ emphasisChildren.push({ type: 'text', value: closingText });
94067
+ }
94068
+ if (emphasisChildren.length > 0) {
94069
+ const emphasisNode = opening.isBold
94070
+ ? { type: 'strong', children: emphasisChildren }
94071
+ : { type: 'emphasis', children: emphasisChildren };
94072
+ newNodes.push(emphasisNode);
94073
+ }
94074
+ if (closing.textAfter) {
94075
+ newNodes.push({ type: 'text', value: closing.textAfter });
94076
+ }
94077
+ return newNodes;
93928
94078
  }
93929
94079
  /**
93930
- * Checks for opening tag only (for split detection)
94080
+ * Find and transform one multi-node emphasis pair in the container.
94081
+ * Returns true if a pair was found and transformed, false otherwise.
93931
94082
  */
93932
- function hasOpeningTagOnly(node) {
93933
- let hasOpening = false;
93934
- let hasClosed = false;
93935
- let attrs = '';
93936
- const check = (n) => {
93937
- if (n.type === 'html' && n.value) {
93938
- if (n.value === '<HTMLBlock>') {
93939
- hasOpening = true;
93940
- }
93941
- else {
93942
- const match = n.value.match(/^<HTMLBlock(\s[^>]*)?>$/);
93943
- if (match) {
93944
- hasOpening = true;
93945
- attrs = match[1] || '';
93946
- }
93947
- }
93948
- if (n.value === '</HTMLBlock>' || n.value.includes('</HTMLBlock>')) {
93949
- hasClosed = true;
93950
- }
93951
- }
93952
- if (n.children && Array.isArray(n.children)) {
93953
- n.children.forEach(child => {
93954
- check(child);
93955
- });
93956
- }
93957
- };
93958
- check(node);
93959
- // Return true only if opening without closing (split case)
93960
- return { attrs, found: hasOpening && !hasClosed };
94083
+ function processOneEmphasisPair(container) {
94084
+ const openingResult = findOpeningInChildren(container.children);
94085
+ if (!openingResult)
94086
+ return false;
94087
+ const { idx: openingIdx, opening } = openingResult;
94088
+ const closingResult = findClosingInChildren(container.children, openingIdx, opening.marker);
94089
+ if (!closingResult)
94090
+ return false;
94091
+ const { closingIdx, closing } = closingResult;
94092
+ const newNodes = buildReplacementNodes(container, { opening, openingIdx, closing, closingIdx });
94093
+ const deleteCount = closingIdx - openingIdx + 1;
94094
+ container.children.splice(openingIdx, deleteCount, ...newNodes);
94095
+ return true;
93961
94096
  }
93962
94097
  /**
93963
- * Checks if a node contains an HTMLBlock closing tag
94098
+ * Handle malformed emphasis that spans multiple AST nodes.
94099
+ * E.g., "**bold [link](url)**" where markers are in different text nodes.
93964
94100
  */
93965
- function hasClosingTag(node) {
93966
- if (node.type === 'html' && node.value) {
93967
- if (node.value === '</HTMLBlock>' || node.value.includes('</HTMLBlock>'))
93968
- return true;
93969
- }
93970
- if (node.children && Array.isArray(node.children)) {
93971
- return node.children.some(child => hasClosingTag(child));
93972
- }
93973
- return false;
94101
+ function visitMultiNodeEmphasis(tree) {
94102
+ const containerTypes = ['paragraph', 'heading', 'tableCell', 'listItem', 'blockquote'];
94103
+ visit(tree, node => {
94104
+ if (!containerTypes.includes(node.type))
94105
+ return;
94106
+ if (!('children' in node) || !Array.isArray(node.children))
94107
+ return;
94108
+ const container = node;
94109
+ let foundPair = true;
94110
+ while (foundPair) {
94111
+ foundPair = processOneEmphasisPair(container);
94112
+ }
94113
+ });
93974
94114
  }
93975
94115
  /**
93976
- * Transforms HTMLBlock MDX JSX to html-block nodes. Handles <HTMLBlock>{`...`}</HTMLBlock> syntax.
94116
+ * A remark plugin that normalizes malformed bold and italic markers in text nodes.
94117
+ * Detects patterns like `** bold**`, `Hello** Wrong Bold**`, `__ bold__`, `Hello__ Wrong Bold__`,
94118
+ * `* italic*`, `Hello* Wrong Italic*`, `_ italic_`, or `Hello_ Wrong Italic_`
94119
+ * and converts them to proper strong/emphasis nodes, matching the behavior of the legacy rdmd engine.
94120
+ *
94121
+ * Supports both asterisk (`**bold**`, `*italic*`) and underscore (`__bold__`, `_italic_`) syntax.
94122
+ * Also supports snake_case content like `** some_snake_case**`.
94123
+ *
94124
+ * This runs after remark-parse, which (in v11+) is strict and doesn't parse
94125
+ * malformed emphasis syntax. This plugin post-processes the AST to handle these cases.
93977
94126
  */
93978
- const mdxishHtmlBlocks = () => tree => {
93979
- // Handle HTMLBlock split across root children (caused by newlines)
93980
- visit(tree, 'root', (root) => {
93981
- const children = root.children;
93982
- let i = 0;
93983
- while (i < children.length) {
93984
- const child = children[i];
93985
- const { attrs, found: hasOpening } = hasOpeningTagOnly(child);
93986
- if (hasOpening) {
93987
- // Find closing tag in subsequent siblings
93988
- let closingIdx = -1;
93989
- for (let j = i + 1; j < children.length; j += 1) {
93990
- if (hasClosingTag(children[j])) {
93991
- closingIdx = j;
93992
- break;
93993
- }
94127
+ const normalizeEmphasisAST = () => (tree) => {
94128
+ visit(tree, 'text', function visitor(node, index, parent) {
94129
+ if (index === undefined || !parent)
94130
+ return undefined;
94131
+ // Skip if inside code blocks or inline code
94132
+ if (parent.type === 'inlineCode' || parent.type === 'code') {
94133
+ return undefined;
94134
+ }
94135
+ const text = node.value;
94136
+ const allMatches = [];
94137
+ [...text.matchAll(asteriskBoldRegex)].forEach(match => {
94138
+ allMatches.push({ isBold: true, marker: '**', match });
94139
+ });
94140
+ [...text.matchAll(underscoreBoldRegex)].forEach(match => {
94141
+ allMatches.push({ isBold: true, marker: '__', match });
94142
+ });
94143
+ [...text.matchAll(asteriskItalicRegex)].forEach(match => {
94144
+ allMatches.push({ isBold: false, marker: '*', match });
94145
+ });
94146
+ [...text.matchAll(underscoreItalicRegex)].forEach(match => {
94147
+ allMatches.push({ isBold: false, marker: '_', match });
94148
+ });
94149
+ [...text.matchAll(intrawordUnderscoreItalicRegex)].forEach(match => {
94150
+ allMatches.push({ isBold: false, isIntraword: true, marker: '_', match });
94151
+ });
94152
+ [...text.matchAll(intrawordUnderscoreBoldRegex)].forEach(match => {
94153
+ allMatches.push({ isBold: true, isIntraword: true, marker: '__', match });
94154
+ });
94155
+ [...text.matchAll(intrawordAsteriskItalicRegex)].forEach(match => {
94156
+ allMatches.push({ isBold: false, isIntraword: true, marker: '*', match });
94157
+ });
94158
+ [...text.matchAll(intrawordAsteriskBoldRegex)].forEach(match => {
94159
+ allMatches.push({ isBold: true, isIntraword: true, marker: '**', match });
94160
+ });
94161
+ if (allMatches.length === 0)
94162
+ return undefined;
94163
+ allMatches.sort((a, b) => (a.match.index ?? 0) - (b.match.index ?? 0));
94164
+ const filteredMatches = [];
94165
+ let lastEnd = 0;
94166
+ allMatches.forEach(info => {
94167
+ const start = info.match.index ?? 0;
94168
+ const end = start + info.match[0].length;
94169
+ if (start >= lastEnd) {
94170
+ filteredMatches.push(info);
94171
+ lastEnd = end;
94172
+ }
94173
+ });
94174
+ if (filteredMatches.length === 0)
94175
+ return undefined;
94176
+ const parts = [];
94177
+ let lastIndex = 0;
94178
+ filteredMatches.forEach(({ isBold, isIntraword, marker, match }) => {
94179
+ const matchIndex = match.index ?? 0;
94180
+ const fullMatch = match[0];
94181
+ if (isIntraword) {
94182
+ // handles cases like hello_world_ where we only want to italicize 'world'
94183
+ const charBefore = match[1] || ''; // e.g., "l" in "hello_world_"
94184
+ const content = match[2]; // e.g., "world"
94185
+ const combinedBefore = text.slice(lastIndex, matchIndex) + charBefore;
94186
+ if (combinedBefore) {
94187
+ parts.push({ type: 'text', value: combinedBefore });
93994
94188
  }
93995
- if (closingIdx !== -1) {
93996
- // Collect inner content between tags
93997
- const contentParts = [];
93998
- for (let j = i; j <= closingIdx; j += 1) {
93999
- const node = children[j];
94000
- contentParts.push(collectTextContent(node));
94001
- }
94002
- // Remove the opening/closing tags and template literal syntax from content
94003
- let content = contentParts.join('');
94004
- content = content.replace(/^<HTMLBlock[^>]*>\s*\{?\s*`?/, '').replace(/`?\s*\}?\s*<\/HTMLBlock>$/, '');
94005
- // Decode protected content that was base64 encoded during preprocessing
94006
- content = decodeProtectedContent(content);
94007
- const htmlString = formatHtmlForMdxish(content);
94008
- const runScripts = extractRunScriptsAttr(attrs);
94009
- const safeMode = extractBooleanAttr(attrs, 'safeMode');
94010
- // Replace range with single HTMLBlock node
94011
- const mdNode = createHTMLBlockNode(htmlString, children[i].position, runScripts, safeMode);
94012
- root.children.splice(i, closingIdx - i + 1, mdNode);
94189
+ if (isBold) {
94190
+ parts.push({
94191
+ type: 'strong',
94192
+ children: [{ type: 'text', value: content }],
94193
+ });
94194
+ }
94195
+ else {
94196
+ parts.push({
94197
+ type: 'emphasis',
94198
+ children: [{ type: 'text', value: content }],
94199
+ });
94013
94200
  }
94201
+ lastIndex = matchIndex + fullMatch.length;
94202
+ return;
94014
94203
  }
94015
- i += 1;
94016
- }
94017
- });
94018
- // Handle HTMLBlock parsed as HTML elements (when template literal contains block-level HTML tags)
94019
- visit(tree, 'html', (node, index, parent) => {
94020
- if (!parent || index === undefined)
94021
- return;
94022
- const value = node.value;
94023
- if (!value)
94024
- return;
94025
- // Case 1: Full HTMLBlock in single node
94026
- const fullMatch = value.match(/^<HTMLBlock(\s[^>]*)?>([\s\S]*)<\/HTMLBlock>$/);
94027
- if (fullMatch) {
94028
- const attrs = fullMatch[1] || '';
94029
- let content = fullMatch[2] || '';
94030
- // Remove template literal syntax if present: {`...`}
94031
- content = content.replace(/^\s*\{\s*`/, '').replace(/`\s*\}\s*$/, '');
94032
- // Decode protected content that was base64 encoded during preprocessing
94033
- content = decodeProtectedContent(content);
94034
- const htmlString = formatHtmlForMdxish(content);
94035
- const runScripts = extractRunScriptsAttr(attrs);
94036
- const safeMode = extractBooleanAttr(attrs, 'safeMode');
94037
- parent.children[index] = createHTMLBlockNode(htmlString, node.position, runScripts, safeMode);
94038
- return;
94039
- }
94040
- // Case 2: Opening tag only (split by blank lines)
94041
- if (value === '<HTMLBlock>' || value.match(/^<HTMLBlock\s[^>]*>$/)) {
94042
- const siblings = parent.children;
94043
- let closingIdx = -1;
94044
- // Find closing tag in siblings
94045
- for (let i = index + 1; i < siblings.length; i += 1) {
94046
- const sibling = siblings[i];
94047
- if (sibling.type === 'html') {
94048
- const sibVal = sibling.value;
94049
- if (sibVal === '</HTMLBlock>' || sibVal?.includes('</HTMLBlock>')) {
94050
- closingIdx = i;
94051
- break;
94052
- }
94204
+ if (matchIndex > lastIndex) {
94205
+ const beforeText = text.slice(lastIndex, matchIndex);
94206
+ if (beforeText) {
94207
+ parts.push({ type: 'text', value: beforeText });
94053
94208
  }
94054
94209
  }
94055
- if (closingIdx === -1)
94056
- return;
94057
- // Collect content between tags, skipping template literal delimiters
94058
- const contentParts = [];
94059
- for (let i = index + 1; i < closingIdx; i += 1) {
94060
- const sibling = siblings[i];
94061
- // Skip template literal delimiters
94062
- if (sibling.type === 'text') {
94063
- const textVal = sibling.value;
94064
- if (textVal === '{' || textVal === '}' || textVal === '{`' || textVal === '`}') {
94065
- // eslint-disable-next-line no-continue
94066
- continue;
94067
- }
94068
- }
94069
- contentParts.push(collectTextContent(sibling));
94210
+ const wordBefore = match[1]; // e.g., "Hello" in "Hello** Wrong Bold**"
94211
+ const contentWithSpaceAfter = match[3]; // Content when there's a space after opening markers
94212
+ const trailingSpace1 = match[4] || ''; // Space before closing markers (for "** text **" pattern)
94213
+ const contentWithSpaceBefore = match[5]; // Content when there's only a space before closing markers
94214
+ const trailingSpace2 = match[6] || ''; // Space before closing markers (for "**text **" pattern)
94215
+ const trailingSpace = trailingSpace1 || trailingSpace2; // Combined trailing space
94216
+ const content = (contentWithSpaceAfter || contentWithSpaceBefore || '').trim();
94217
+ const afterChar = match[7]; // Character after closing markers (if any)
94218
+ const markerPos = fullMatch.indexOf(marker);
94219
+ const spacesBeforeMarkers = wordBefore
94220
+ ? fullMatch.slice(wordBefore.length, markerPos)
94221
+ : fullMatch.slice(0, markerPos);
94222
+ const shouldAddSpace = !!contentWithSpaceAfter && !!wordBefore && !spacesBeforeMarkers;
94223
+ if (wordBefore) {
94224
+ const spacing = spacesBeforeMarkers + (shouldAddSpace ? ' ' : '');
94225
+ parts.push({ type: 'text', value: wordBefore + spacing });
94070
94226
  }
94071
- // Decode protected content that was base64 encoded during preprocessing
94072
- const decodedContent = decodeProtectedContent(contentParts.join(''));
94073
- const htmlString = formatHtmlForMdxish(decodedContent);
94074
- const runScripts = extractRunScriptsAttr(value);
94075
- const safeMode = extractBooleanAttr(value, 'safeMode');
94076
- // Replace opening tag with HTMLBlock node, remove consumed siblings
94077
- parent.children[index] = createHTMLBlockNode(htmlString, node.position, runScripts, safeMode);
94078
- parent.children.splice(index + 1, closingIdx - index);
94079
- }
94080
- });
94081
- // Handle HTMLBlock inside paragraphs (parsed as inline elements)
94082
- visit(tree, 'paragraph', (node, index, parent) => {
94083
- if (!parent || index === undefined)
94084
- return;
94085
- const children = node.children || [];
94086
- let htmlBlockStartIdx = -1;
94087
- let htmlBlockEndIdx = -1;
94088
- let templateLiteralStartIdx = -1;
94089
- let templateLiteralEndIdx = -1;
94090
- for (let i = 0; i < children.length; i += 1) {
94091
- const child = children[i];
94092
- if (child.type === 'html' && typeof child.value === 'string') {
94093
- const value = child.value;
94094
- if (value === '<HTMLBlock>' || value.match(/^<HTMLBlock\s[^>]*>$/)) {
94095
- htmlBlockStartIdx = i;
94096
- }
94097
- else if (value === '</HTMLBlock>') {
94098
- htmlBlockEndIdx = i;
94099
- }
94227
+ else if (spacesBeforeMarkers) {
94228
+ parts.push({ type: 'text', value: spacesBeforeMarkers });
94100
94229
  }
94101
- // Find opening brace after HTMLBlock start
94102
- if (htmlBlockStartIdx !== -1 && templateLiteralStartIdx === -1 && child.type === 'text') {
94103
- const value = child.value;
94104
- if (value === '{') {
94105
- templateLiteralStartIdx = i;
94230
+ if (content) {
94231
+ if (isBold) {
94232
+ parts.push({
94233
+ type: 'strong',
94234
+ children: [{ type: 'text', value: content }],
94235
+ });
94106
94236
  }
94107
- }
94108
- // Find closing brace before HTMLBlock end
94109
- if (htmlBlockStartIdx !== -1 && htmlBlockEndIdx === -1 && child.type === 'text') {
94110
- const value = child.value;
94111
- if (value === '}') {
94112
- templateLiteralEndIdx = i;
94237
+ else {
94238
+ parts.push({
94239
+ type: 'emphasis',
94240
+ children: [{ type: 'text', value: content }],
94241
+ });
94113
94242
  }
94114
94243
  }
94115
- }
94116
- if (htmlBlockStartIdx !== -1 &&
94117
- htmlBlockEndIdx !== -1 &&
94118
- templateLiteralStartIdx !== -1 &&
94119
- templateLiteralEndIdx !== -1 &&
94120
- templateLiteralStartIdx < templateLiteralEndIdx) {
94121
- const openingTag = children[htmlBlockStartIdx];
94122
- // Collect content between braces (handles code blocks)
94123
- const templateContent = [];
94124
- for (let i = templateLiteralStartIdx + 1; i < templateLiteralEndIdx; i += 1) {
94125
- const child = children[i];
94126
- templateContent.push(collectTextContent(child));
94244
+ if (afterChar) {
94245
+ const prefix = trailingSpace ? ' ' : '';
94246
+ parts.push({ type: 'text', value: prefix + afterChar });
94247
+ }
94248
+ lastIndex = matchIndex + fullMatch.length;
94249
+ });
94250
+ if (lastIndex < text.length) {
94251
+ const remainingText = text.slice(lastIndex);
94252
+ if (remainingText) {
94253
+ parts.push({ type: 'text', value: remainingText });
94127
94254
  }
94128
- // Decode protected content that was base64 encoded during preprocessing
94129
- const decodedContent = decodeProtectedContent(templateContent.join(''));
94130
- const htmlString = formatHtmlForMdxish(decodedContent);
94131
- const runScripts = openingTag.value ? extractRunScriptsAttr(openingTag.value) : undefined;
94132
- const safeMode = openingTag.value ? extractBooleanAttr(openingTag.value, 'safeMode') : undefined;
94133
- const mdNode = createHTMLBlockNode(htmlString, node.position, runScripts, safeMode);
94134
- parent.children[index] = mdNode;
94135
94255
  }
94136
- });
94137
- // Ensure html-block nodes have HTML in children as text node
94138
- visit(tree, 'html-block', (node) => {
94139
- const html = node.data?.hProperties?.html;
94140
- if (html &&
94141
- (!node.children ||
94142
- node.children.length === 0 ||
94143
- (node.children.length === 1 && node.children[0].type === 'text' && node.children[0].value !== html))) {
94144
- node.children = [
94145
- {
94146
- type: 'text',
94147
- value: html,
94148
- },
94149
- ];
94256
+ if (parts.length > 0) {
94257
+ parent.children.splice(index, 1, ...parts);
94258
+ return [SKIP, index + parts.length];
94150
94259
  }
94260
+ return undefined;
94151
94261
  });
94262
+ // Handle malformed emphasis spanning multiple nodes (e.g., **text [link](url) **)
94263
+ visitMultiNodeEmphasis(tree);
94152
94264
  return tree;
94153
94265
  };
94154
- /* harmony default export */ const mdxish_html_blocks = (mdxishHtmlBlocks);
94266
+ /* harmony default export */ const normalize_malformed_md_syntax = (normalizeEmphasisAST);
94267
+
94268
+ ;// ./processor/transform/mdxish/magic-blocks/placeholder.ts
94269
+ const EMPTY_IMAGE_PLACEHOLDER = {
94270
+ type: 'image',
94271
+ url: '',
94272
+ alt: '',
94273
+ title: '',
94274
+ data: { hProperties: {} },
94275
+ };
94276
+ const EMPTY_EMBED_PLACEHOLDER = {
94277
+ type: 'embed',
94278
+ children: [{ type: 'link', url: '', title: '', children: [{ type: 'text', value: '' }] }],
94279
+ data: { hName: 'embed-block', hProperties: { url: '', href: '', title: '' } },
94280
+ };
94281
+ const EMPTY_RECIPE_PLACEHOLDER = {
94282
+ type: 'mdxJsxFlowElement',
94283
+ name: 'Recipe',
94284
+ attributes: [],
94285
+ children: [],
94286
+ };
94287
+ const EMPTY_CALLOUT_PLACEHOLDER = {
94288
+ type: 'mdxJsxFlowElement',
94289
+ name: 'Callout',
94290
+ attributes: [
94291
+ { type: 'mdxJsxAttribute', name: 'icon', value: '📘' },
94292
+ { type: 'mdxJsxAttribute', name: 'theme', value: 'info' },
94293
+ { type: 'mdxJsxAttribute', name: 'type', value: 'info' },
94294
+ { type: 'mdxJsxAttribute', name: 'empty', value: 'true' },
94295
+ ],
94296
+ children: [{ type: 'heading', depth: 3, children: [{ type: 'text', value: '' }] }],
94297
+ };
94298
+ const EMPTY_TABLE_PLACEHOLDER = {
94299
+ type: 'table',
94300
+ align: ['left', 'left'],
94301
+ children: [
94302
+ { type: 'tableRow', children: [{ type: 'tableCell', children: [{ type: 'text', value: '' }] }] },
94303
+ { type: 'tableRow', children: [{ type: 'tableCell', children: [{ type: 'text', value: '' }] }] },
94304
+ ],
94305
+ };
94306
+ const EMPTY_CODE_PLACEHOLDER = {
94307
+ type: 'code',
94308
+ value: '',
94309
+ lang: null,
94310
+ meta: null,
94311
+ };
94312
+
94313
+ ;// ./processor/transform/mdxish/magic-blocks/magic-block-transformer.ts
94314
+
94155
94315
 
94156
- ;// ./processor/transform/mdxish/mdxish-magic-blocks.ts
94157
94316
 
94158
94317
 
94159
94318
 
94160
94319
 
94161
94320
 
94162
94321
  /**
94163
- * Matches legacy magic block syntax: [block:TYPE]...JSON...[/block]
94164
- * Group 1: block type (e.g., "image", "code", "callout")
94165
- * Group 2: JSON content between the tags
94166
- * Taken from the v6 branch
94167
- */
94168
- const RGXP = /^\s*\[block:([^\]]*)\]([^]+?)\[\/block\]/;
94169
- /**
94170
- * Wraps a node in a "pinned" container if sidebar: true is set in the JSON.
94171
- * Pinned blocks are displayed in a sidebar/floating position in the UI.
94322
+ * Wraps a node in a "pinned" container if sidebar: true is set.
94172
94323
  */
94173
- const wrapPinnedBlocks = (node, json) => {
94174
- if (!json.sidebar)
94324
+ const wrapPinnedBlocks = (node, data) => {
94325
+ if (!data.sidebar)
94175
94326
  return node;
94176
94327
  return {
94177
94328
  children: [node],
@@ -94187,34 +94338,24 @@ const imgSizeValues = {
94187
94338
  original: 'auto',
94188
94339
  };
94189
94340
  /**
94190
- * Proxy that resolves image sizing values:
94191
- * - "full" → "100%", "original" → "auto" (from imgSizeValues)
94192
- * - Pure numbers like "50" → "50%" (percentage)
94193
- * - Anything else passes through as-is (e.g., "200px")
94341
+ * Proxy that resolves image sizing values.
94194
94342
  */
94195
94343
  const imgWidthBySize = new Proxy(imgSizeValues, {
94196
94344
  get: (widths, size) => (size?.match(/^\d+$/) ? `${size}%` : size in widths ? widths[size] : size),
94197
94345
  });
94198
- // Simple text to inline nodes (just returns text node - no markdown parsing)
94199
94346
  const textToInline = (text) => [{ type: 'text', value: text }];
94200
- // Simple text to block nodes (wraps in paragraph)
94201
94347
  const textToBlock = (text) => [{ children: textToInline(text), type: 'paragraph' }];
94202
94348
  /** Parses markdown and html to markdown nodes */
94203
- const contentParser = unified().use(remarkParse).use(remarkGfm);
94204
- // Table cells may contain html or markdown content, so we need to parse it accordingly instead of keeping it as raw text
94349
+ const contentParser = unified().use(remarkParse).use(remarkGfm).use(normalize_malformed_md_syntax);
94205
94350
  const parseTableCell = (text) => {
94206
94351
  if (!text.trim())
94207
94352
  return [{ type: 'text', value: '' }];
94208
94353
  const tree = contentParser.runSync(contentParser.parse(text));
94209
- // If there are multiple block-level nodes, keep them as-is to preserve the document structure and spacing
94210
94354
  if (tree.children.length > 1) {
94211
94355
  return tree.children;
94212
94356
  }
94213
- return tree.children.flatMap(n =>
94214
- // This unwraps the extra p node that might appear & wrapping the content
94215
- n.type === 'paragraph' && 'children' in n ? n.children : [n]);
94357
+ return tree.children.flatMap(n => n.type === 'paragraph' && 'children' in n ? n.children : [n]);
94216
94358
  };
94217
- // Parse markdown/HTML into block-level nodes (preserves paragraphs, headings, lists, etc.)
94218
94359
  const parseBlock = (text) => {
94219
94360
  if (!text.trim())
94220
94361
  return [{ type: 'paragraph', children: [{ type: 'text', value: '' }] }];
@@ -94222,44 +94363,43 @@ const parseBlock = (text) => {
94222
94363
  return tree.children;
94223
94364
  };
94224
94365
  /**
94225
- * Parse a magic block string and return MDAST nodes.
94226
- *
94227
- * @param raw - The raw magic block string including [block:TYPE] and [/block] tags
94228
- * @param options - Parsing options for compatibility and error handling
94229
- * @returns Array of MDAST nodes representing the parsed block
94230
- */
94231
- function parseMagicBlock(raw, options = {}) {
94232
- const { alwaysThrow = false, compatibilityMode = false, safeMode = false } = options;
94233
- const matchResult = RGXP.exec(raw);
94234
- if (!matchResult)
94235
- return [];
94236
- const [, rawType, jsonStr] = matchResult;
94237
- const type = rawType?.trim();
94238
- if (!type)
94239
- return [];
94240
- let json;
94241
- try {
94242
- json = JSON.parse(jsonStr);
94366
+ * Transform a magicBlock node into final MDAST nodes.
94367
+ */
94368
+ function transformMagicBlock(blockType, data, rawValue, options = {}) {
94369
+ const { compatibilityMode = false, safeMode = false } = options;
94370
+ // Handle empty data by returning placeholder nodes for known block types
94371
+ // This allows the editor to show appropriate placeholder UI instead of nothing
94372
+ if (Object.keys(data).length < 1) {
94373
+ switch (blockType) {
94374
+ case 'image':
94375
+ return [EMPTY_IMAGE_PLACEHOLDER];
94376
+ case 'embed':
94377
+ return [EMPTY_EMBED_PLACEHOLDER];
94378
+ case 'code':
94379
+ return [EMPTY_CODE_PLACEHOLDER];
94380
+ case 'callout':
94381
+ return [EMPTY_CALLOUT_PLACEHOLDER];
94382
+ case 'parameters':
94383
+ case 'table':
94384
+ return [EMPTY_TABLE_PLACEHOLDER];
94385
+ case 'recipe':
94386
+ case 'tutorial-tile':
94387
+ return [EMPTY_RECIPE_PLACEHOLDER];
94388
+ default:
94389
+ return [{ type: 'paragraph', children: [{ type: 'text', value: rawValue }] }];
94390
+ }
94243
94391
  }
94244
- catch (err) {
94245
- // eslint-disable-next-line no-console
94246
- console.error('Invalid Magic Block JSON:', err);
94247
- if (alwaysThrow)
94248
- throw new Error('Invalid Magic Block JSON');
94249
- return [];
94250
- }
94251
- if (Object.keys(json).length < 1)
94252
- return [];
94253
- // Each case handles a different magic block type and returns appropriate MDAST nodes
94254
- switch (type) {
94255
- // Code blocks: single code block or tabbed code blocks (multiple languages)
94392
+ switch (blockType) {
94256
94393
  case 'code': {
94257
- const codeJson = json;
94394
+ const codeJson = data;
94395
+ if (!codeJson.codes || !Array.isArray(codeJson.codes)) {
94396
+ return [wrapPinnedBlocks(EMPTY_CODE_PLACEHOLDER, data)];
94397
+ }
94258
94398
  const children = codeJson.codes.map(obj => ({
94259
94399
  className: 'tab-panel',
94260
94400
  data: { hName: 'code', hProperties: { lang: obj.language, meta: obj.name || null } },
94261
94401
  lang: obj.language,
94262
- meta: obj.name || null, // Tab name shown in the UI
94402
+ meta: obj.name || null,
94263
94403
  type: 'code',
94264
94404
  value: obj.code.trim(),
94265
94405
  }));
@@ -94269,31 +94409,31 @@ function parseMagicBlock(raw, options = {}) {
94269
94409
  if (!children[0].value)
94270
94410
  return [];
94271
94411
  if (!(children[0].meta || children[0].lang))
94272
- return [wrapPinnedBlocks(children[0], json)];
94412
+ return [wrapPinnedBlocks(children[0], data)];
94273
94413
  }
94274
94414
  // Multiple code blocks or a single code block with a tab name (meta or language) renders as a code tabs block
94275
- return [wrapPinnedBlocks({ children, className: 'tabs', data: { hName: 'CodeTabs' }, type: 'code-tabs' }, json)];
94415
+ return [wrapPinnedBlocks({ children, className: 'tabs', data: { hName: 'CodeTabs' }, type: 'code-tabs' }, data)];
94276
94416
  }
94277
- // API header: renders as a heading element (h1-h6)
94278
94417
  case 'api-header': {
94279
- const headerJson = json;
94280
- // In compatibility mode, default to h1; otherwise h2
94418
+ const headerJson = data;
94281
94419
  const depth = headerJson.level || (compatibilityMode ? 1 : 2);
94282
94420
  return [
94283
94421
  wrapPinnedBlocks({
94284
94422
  children: 'title' in headerJson ? textToInline(headerJson.title || '') : [],
94285
94423
  depth,
94286
94424
  type: 'heading',
94287
- }, json),
94425
+ }, data),
94288
94426
  ];
94289
94427
  }
94290
- // Image block: renders as <img> or <figure> with caption
94291
94428
  case 'image': {
94292
- const imageJson = json;
94429
+ const imageJson = data;
94430
+ if (!imageJson.images || !Array.isArray(imageJson.images)) {
94431
+ return [wrapPinnedBlocks(EMPTY_IMAGE_PLACEHOLDER, data)];
94432
+ }
94293
94433
  const imgData = imageJson.images.find(i => i.image);
94294
- if (!imgData?.image)
94295
- return [];
94296
- // Image array format: [url, title?, alt?]
94434
+ if (!imgData?.image) {
94435
+ return [wrapPinnedBlocks(EMPTY_IMAGE_PLACEHOLDER, data)];
94436
+ }
94297
94437
  const [url, title, alt] = imgData.image;
94298
94438
  const block = {
94299
94439
  alt: alt || imgData.caption || '',
@@ -94308,57 +94448,66 @@ function parseMagicBlock(raw, options = {}) {
94308
94448
  type: 'image',
94309
94449
  url,
94310
94450
  };
94311
- // Wrap in <figure> if caption is present
94312
94451
  const img = imgData.caption
94313
94452
  ? {
94314
94453
  children: [
94315
94454
  block,
94316
- { children: textToBlock(imgData.caption), data: { hName: 'figcaption' }, type: 'figcaption' },
94455
+ { children: parseBlock(imgData.caption), data: { hName: 'figcaption' }, type: 'figcaption' },
94317
94456
  ],
94318
94457
  data: { hName: 'figure' },
94319
94458
  type: 'figure',
94320
94459
  url,
94321
94460
  }
94322
94461
  : block;
94323
- return [wrapPinnedBlocks(img, json)];
94462
+ return [wrapPinnedBlocks(img, data)];
94324
94463
  }
94325
- // Callout: info/warning/error boxes with icon and theme
94326
94464
  case 'callout': {
94327
- const calloutJson = json;
94328
- // Preset callout types map to [icon, theme] tuples
94465
+ const calloutJson = data;
94329
94466
  const types = {
94330
94467
  danger: ['❗️', 'error'],
94331
94468
  info: ['📘', 'info'],
94332
94469
  success: ['👍', 'okay'],
94333
94470
  warning: ['🚧', 'warn'],
94334
94471
  };
94335
- // Resolve type to [icon, theme] - use preset if available, otherwise custom
94336
94472
  const resolvedType = typeof calloutJson.type === 'string' && calloutJson.type in types
94337
94473
  ? types[calloutJson.type]
94338
94474
  : [calloutJson.icon || '👍', typeof calloutJson.type === 'string' ? calloutJson.type : 'default'];
94339
94475
  const [icon, theme] = Array.isArray(resolvedType) ? resolvedType : ['👍', 'default'];
94340
94476
  if (!(calloutJson.title || calloutJson.body))
94341
94477
  return [];
94342
- // Parses html & markdown content
94343
- const titleBlocks = parseBlock(calloutJson.title || '');
94344
- const bodyBlocks = parseBlock(calloutJson.body || '');
94478
+ const hasTitle = !!calloutJson.title?.trim();
94479
+ const hasBody = !!calloutJson.body?.trim();
94480
+ const empty = !hasTitle;
94345
94481
  const children = [];
94346
- if (titleBlocks.length > 0 && titleBlocks[0].type === 'paragraph') {
94347
- const firstTitle = titleBlocks[0];
94348
- const heading = {
94482
+ if (hasTitle) {
94483
+ const titleBlocks = parseBlock(calloutJson.title || '');
94484
+ if (titleBlocks.length > 0 && titleBlocks[0].type === 'paragraph') {
94485
+ const firstTitle = titleBlocks[0];
94486
+ const heading = {
94487
+ type: 'heading',
94488
+ depth: 3,
94489
+ children: (firstTitle.children || []),
94490
+ };
94491
+ children.push(heading);
94492
+ children.push(...titleBlocks.slice(1));
94493
+ }
94494
+ else {
94495
+ children.push(...titleBlocks);
94496
+ }
94497
+ }
94498
+ else {
94499
+ // Add empty heading placeholder so body goes to children.slice(1)
94500
+ // The Callout component expects children[0] to be the heading
94501
+ children.push({
94349
94502
  type: 'heading',
94350
94503
  depth: 3,
94351
- children: (firstTitle.children || []),
94352
- };
94353
- children.push(heading);
94354
- children.push(...titleBlocks.slice(1), ...bodyBlocks);
94504
+ children: [{ type: 'text', value: '' }],
94505
+ });
94355
94506
  }
94356
- else {
94357
- children.push(...titleBlocks, ...bodyBlocks);
94507
+ if (hasBody) {
94508
+ const bodyBlocks = parseBlock(calloutJson.body || '');
94509
+ children.push(...bodyBlocks);
94358
94510
  }
94359
- // If there is no title or title is empty
94360
- const empty = !titleBlocks.length || !titleBlocks[0].children[0]?.value;
94361
- // Create mdxJsxFlowElement directly for mdxish
94362
94511
  const calloutElement = {
94363
94512
  type: 'mdxJsxFlowElement',
94364
94513
  name: 'Callout',
@@ -94370,23 +94519,17 @@ function parseMagicBlock(raw, options = {}) {
94370
94519
  ]),
94371
94520
  children: children,
94372
94521
  };
94373
- return [wrapPinnedBlocks(calloutElement, json)];
94522
+ return [wrapPinnedBlocks(calloutElement, data)];
94374
94523
  }
94375
- // Parameters: renders as a table (used for API parameters, etc.)
94376
94524
  case 'parameters': {
94377
- const paramsJson = json;
94378
- const { cols, data, rows } = paramsJson;
94379
- if (!Object.keys(data).length)
94525
+ const paramsJson = data;
94526
+ const { cols, data: tableData, rows } = paramsJson;
94527
+ if (!tableData || !Object.keys(tableData).length)
94380
94528
  return [];
94381
- /**
94382
- * Convert sparse key-value data to 2D array.
94383
- * Keys are formatted as "ROW-COL" where ROW is "h" for header or a number.
94384
- * Example: { "h-0": "Name", "h-1": "Type", "0-0": "id", "0-1": "string" }
94385
- * Becomes: [["Name", "Type"], ["id", "string"]]
94386
- */
94387
- const sparseData = Object.entries(data).reduce((mapped, [key, v]) => {
94529
+ if (typeof cols !== 'number' || typeof rows !== 'number' || cols < 1 || rows < 0)
94530
+ return [];
94531
+ const sparseData = Object.entries(tableData).reduce((mapped, [key, v]) => {
94388
94532
  const [row, col] = key.split('-');
94389
- // Header row ("h") becomes index 0, data rows are offset by 1
94390
94533
  const rowIndex = row === 'h' ? 0 : parseInt(row, 10) + 1;
94391
94534
  const colIndex = parseInt(col, 10);
94392
94535
  if (!mapped[rowIndex])
@@ -94394,9 +94537,8 @@ function parseMagicBlock(raw, options = {}) {
94394
94537
  mapped[rowIndex][colIndex] = v;
94395
94538
  return mapped;
94396
94539
  }, []);
94397
- // In compatibility mode, wrap cell content in paragraphs; otherwise inline text
94398
94540
  const tokenizeCell = compatibilityMode ? textToBlock : parseTableCell;
94399
- const children = Array.from({ length: rows + 1 }, (_, y) => ({
94541
+ const tableChildren = Array.from({ length: rows + 1 }, (_, y) => ({
94400
94542
  children: Array.from({ length: cols }, (__, x) => ({
94401
94543
  children: sparseData[y]?.[x] ? tokenizeCell(sparseData[y][x]) : [{ type: 'text', value: '' }],
94402
94544
  type: y === 0 ? 'tableHead' : 'tableCell',
@@ -94404,14 +94546,15 @@ function parseMagicBlock(raw, options = {}) {
94404
94546
  type: 'tableRow',
94405
94547
  }));
94406
94548
  return [
94407
- wrapPinnedBlocks({ align: paramsJson.align ?? new Array(cols).fill('left'), children, type: 'table' }, json),
94549
+ wrapPinnedBlocks({ align: paramsJson.align ?? new Array(cols).fill('left'), children: tableChildren, type: 'table' }, data),
94408
94550
  ];
94409
94551
  }
94410
- // Embed: external content (YouTube, etc.) with provider detection
94411
94552
  case 'embed': {
94412
- const embedJson = json;
94553
+ const embedJson = data;
94554
+ if (!embedJson.url) {
94555
+ return [wrapPinnedBlocks(EMPTY_EMBED_PLACEHOLDER, data)];
94556
+ }
94413
94557
  const { html, title, url } = embedJson;
94414
- // Extract provider name from URL hostname (e.g., "youtube.com" → "youtube.com")
94415
94558
  try {
94416
94559
  embedJson.provider = new URL(url).hostname
94417
94560
  .split(/(?:www)?\./)
@@ -94428,12 +94571,13 @@ function parseMagicBlock(raw, options = {}) {
94428
94571
  ],
94429
94572
  data: { hName: 'embed-block', hProperties: { ...embedJson, href: url, html, title, url } },
94430
94573
  type: 'embed',
94431
- }, json),
94574
+ }, data),
94432
94575
  ];
94433
94576
  }
94434
- // HTML block: raw HTML content (scripts enabled only in compatibility mode)
94435
94577
  case 'html': {
94436
- const htmlJson = json;
94578
+ const htmlJson = data;
94579
+ if (typeof htmlJson.html !== 'string')
94580
+ return [];
94437
94581
  return [
94438
94582
  wrapPinnedBlocks({
94439
94583
  data: {
@@ -94441,39 +94585,33 @@ function parseMagicBlock(raw, options = {}) {
94441
94585
  hProperties: { html: htmlJson.html, runScripts: compatibilityMode, safeMode },
94442
94586
  },
94443
94587
  type: 'html-block',
94444
- }, json),
94588
+ }, data),
94445
94589
  ];
94446
94590
  }
94447
- // Recipe/TutorialTile: renders as Recipe component
94448
94591
  case 'recipe':
94449
94592
  case 'tutorial-tile': {
94450
- const recipeJson = json;
94593
+ const recipeJson = data;
94451
94594
  if (!recipeJson.slug || !recipeJson.title)
94452
94595
  return [];
94453
- // Create mdxJsxFlowElement directly for mdxish flow
94454
- // Note: Don't wrap in pinned blocks for mdxish - rehypeMdxishComponents handles component resolution
94455
- // The node structure matches what mdxishComponentBlocks creates for JSX tags
94456
94596
  const recipeNode = {
94457
94597
  type: 'mdxJsxFlowElement',
94458
94598
  name: 'Recipe',
94459
94599
  attributes: toAttributes(recipeJson, ['slug', 'title']),
94460
94600
  children: [],
94461
- // Position is optional but helps with debugging
94462
94601
  position: undefined,
94463
94602
  };
94464
94603
  return [recipeNode];
94465
94604
  }
94466
- // Unknown block types: render as generic div with JSON properties
94467
94605
  default: {
94468
- const text = json.text || json.html || '';
94606
+ const text = data.text || data.html || '';
94469
94607
  return [
94470
- wrapPinnedBlocks({ children: textToBlock(text), data: { hName: type || 'div', hProperties: json, ...json }, type: 'div' }, json),
94608
+ wrapPinnedBlocks({ children: textToBlock(text), data: { hName: blockType || 'div', hProperties: data, ...data }, type: 'div' }, data),
94471
94609
  ];
94472
94610
  }
94473
94611
  }
94474
94612
  }
94475
94613
  /**
94476
- * Block-level node types that cannot be nested inside paragraphs.
94614
+ * Check if a child node is a flow element that needs unwrapping.
94477
94615
  */
94478
94616
  const blockTypes = [
94479
94617
  'heading',
@@ -94499,29 +94637,19 @@ const blockTypes = [
94499
94637
  */
94500
94638
  const isBlockNode = (node) => blockTypes.includes(node.type);
94501
94639
  /**
94502
- * Unified plugin that restores magic blocks from placeholder tokens.
94503
- *
94504
- * During preprocessing, extractMagicBlocks replaces [block:TYPE]...[/block]
94505
- * with inline code tokens like `__MAGIC_BLOCK_0__`. This plugin finds those
94506
- * tokens in the parsed MDAST and replaces them with the parsed block content.
94640
+ * Unified plugin that transforms magicBlock nodes into final MDAST nodes.
94507
94641
  */
94508
- const magicBlockRestorer = ({ blocks }) => tree => {
94509
- if (!blocks.length)
94510
- return;
94511
- // Map: key → original raw magic block content
94512
- const magicBlockKeys = new Map(blocks.map(({ key, raw }) => [key, raw]));
94513
- // Collect replacements to apply (we need to visit in reverse to maintain indices)
94642
+ const magicBlockTransformer = (options = {}) => tree => {
94514
94643
  const replacements = [];
94515
- // First pass: collect all replacements
94516
- visit(tree, 'inlineCode', (node, index, parent) => {
94517
- if (!parent || index == null)
94518
- return undefined;
94519
- const raw = magicBlockKeys.get(node.value);
94520
- if (!raw)
94521
- return undefined;
94522
- const children = parseMagicBlock(raw);
94523
- if (!children.length)
94644
+ visit(tree, 'magicBlock', (node, index, parent) => {
94645
+ if (!parent || index === undefined)
94524
94646
  return undefined;
94647
+ const children = transformMagicBlock(node.blockType, node.data, node.value, options);
94648
+ if (!children.length) {
94649
+ // Remove the node if transformation returns nothing
94650
+ parent.children.splice(index, 1);
94651
+ return [SKIP, index];
94652
+ }
94525
94653
  // If parent is a paragraph and we're inserting block nodes (which must not be in paragraphs), lift them out
94526
94654
  if (parent.type === 'paragraph' && children.some(child => isBlockNode(child))) {
94527
94655
  const blockNodes = [];
@@ -94590,7 +94718,557 @@ const magicBlockRestorer = ({ blocks }) => tree => {
94590
94718
  }
94591
94719
  }
94592
94720
  };
94593
- /* harmony default export */ const mdxish_magic_blocks = (magicBlockRestorer);
94721
+ /* harmony default export */ const magic_block_transformer = (magicBlockTransformer);
94722
+
94723
+ ;// ./processor/transform/mdxish/mdxish-html-blocks.ts
94724
+
94725
+
94726
+
94727
+
94728
+ /**
94729
+ * Decodes HTMLBlock content that was protected during preprocessing.
94730
+ * Content is wrapped in <!--RDMX_HTMLBLOCK:base64:RDMX_HTMLBLOCK-->
94731
+ */
94732
+ function decodeProtectedContent(content) {
94733
+ // Escape special regex characters in the markers
94734
+ const startEscaped = HTML_BLOCK_CONTENT_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
94735
+ const endEscaped = HTML_BLOCK_CONTENT_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
94736
+ const markerRegex = new RegExp(`${startEscaped}([A-Za-z0-9+/=]+)${endEscaped}`, 'g');
94737
+ return content.replace(markerRegex, (_match, encoded) => {
94738
+ try {
94739
+ return base64Decode(encoded);
94740
+ }
94741
+ catch {
94742
+ return encoded;
94743
+ }
94744
+ });
94745
+ }
94746
+ /**
94747
+ * Collects text content from a node and its children recursively
94748
+ */
94749
+ function collectTextContent(node) {
94750
+ const parts = [];
94751
+ if (node.type === 'text' && node.value) {
94752
+ parts.push(node.value);
94753
+ }
94754
+ else if (node.type === 'html' && node.value) {
94755
+ parts.push(node.value);
94756
+ }
94757
+ else if (node.type === 'inlineCode' && node.value) {
94758
+ parts.push(node.value);
94759
+ }
94760
+ else if (node.type === 'code' && node.value) {
94761
+ // Reconstruct code fence syntax (markdown parser consumes opening ```)
94762
+ const lang = node.lang || '';
94763
+ const fence = `\`\`\`${lang ? `${lang}\n` : ''}`;
94764
+ parts.push(fence);
94765
+ parts.push(node.value);
94766
+ // Add newline before closing fence if missing
94767
+ const closingFence = node.value.endsWith('\n') ? '```' : '\n```';
94768
+ parts.push(closingFence);
94769
+ }
94770
+ else if (node.children && Array.isArray(node.children)) {
94771
+ node.children.forEach(child => {
94772
+ if (typeof child === 'object' && child !== null) {
94773
+ parts.push(collectTextContent(child));
94774
+ }
94775
+ });
94776
+ }
94777
+ return parts.join('');
94778
+ }
94779
+ /**
94780
+ * Extracts boolean attribute from HTML tag. Handles JSX (safeMode={true}) and string (safeMode="true") syntax.
94781
+ * Returns "true"/"false" string to survive rehypeRaw serialization.
94782
+ */
94783
+ function extractBooleanAttr(attrs, name) {
94784
+ // Try JSX syntax: name={true|false}
94785
+ const jsxMatch = attrs.match(new RegExp(`${name}=\\{(true|false)\\}`));
94786
+ if (jsxMatch) {
94787
+ return jsxMatch[1];
94788
+ }
94789
+ // Try string syntax: name="true"|true
94790
+ const stringMatch = attrs.match(new RegExp(`${name}="?(true|false)"?`));
94791
+ if (stringMatch) {
94792
+ return stringMatch[1];
94793
+ }
94794
+ return undefined;
94795
+ }
94796
+ /**
94797
+ * Extracts runScripts attribute from HTML tag. Returns boolean for "true"/"false", string for other values, or undefined if not found.
94798
+ */
94799
+ function extractRunScriptsAttr(attrs) {
94800
+ const runScriptsMatch = attrs.match(/runScripts="?([^">\s]+)"?/);
94801
+ if (!runScriptsMatch) {
94802
+ return undefined;
94803
+ }
94804
+ const value = runScriptsMatch[1];
94805
+ if (value === 'true') {
94806
+ return true;
94807
+ }
94808
+ if (value === 'false') {
94809
+ return false;
94810
+ }
94811
+ return value;
94812
+ }
94813
+ /**
94814
+ * Creates an HTMLBlock node from HTML string and optional attributes
94815
+ */
94816
+ function createHTMLBlockNode(htmlString, position, runScripts, safeMode) {
94817
+ return {
94818
+ position,
94819
+ children: [{ type: 'text', value: htmlString }],
94820
+ type: NodeTypes.htmlBlock,
94821
+ data: {
94822
+ hName: 'html-block',
94823
+ hProperties: {
94824
+ html: htmlString,
94825
+ ...(runScripts !== undefined && { runScripts }),
94826
+ ...(safeMode !== undefined && { safeMode }),
94827
+ },
94828
+ },
94829
+ };
94830
+ }
94831
+ /**
94832
+ * Checks for opening tag only (for split detection)
94833
+ */
94834
+ function hasOpeningTagOnly(node) {
94835
+ let hasOpening = false;
94836
+ let hasClosed = false;
94837
+ let attrs = '';
94838
+ const check = (n) => {
94839
+ if (n.type === 'html' && n.value) {
94840
+ if (n.value === '<HTMLBlock>') {
94841
+ hasOpening = true;
94842
+ }
94843
+ else {
94844
+ const match = n.value.match(/^<HTMLBlock(\s[^>]*)?>$/);
94845
+ if (match) {
94846
+ hasOpening = true;
94847
+ attrs = match[1] || '';
94848
+ }
94849
+ }
94850
+ if (n.value === '</HTMLBlock>' || n.value.includes('</HTMLBlock>')) {
94851
+ hasClosed = true;
94852
+ }
94853
+ }
94854
+ if (n.children && Array.isArray(n.children)) {
94855
+ n.children.forEach(child => {
94856
+ check(child);
94857
+ });
94858
+ }
94859
+ };
94860
+ check(node);
94861
+ // Return true only if opening without closing (split case)
94862
+ return { attrs, found: hasOpening && !hasClosed };
94863
+ }
94864
+ /**
94865
+ * Checks if a node contains an HTMLBlock closing tag
94866
+ */
94867
+ function hasClosingTag(node) {
94868
+ if (node.type === 'html' && node.value) {
94869
+ if (node.value === '</HTMLBlock>' || node.value.includes('</HTMLBlock>'))
94870
+ return true;
94871
+ }
94872
+ if (node.children && Array.isArray(node.children)) {
94873
+ return node.children.some(child => hasClosingTag(child));
94874
+ }
94875
+ return false;
94876
+ }
94877
+ /**
94878
+ * Transforms HTMLBlock MDX JSX to html-block nodes. Handles <HTMLBlock>{`...`}</HTMLBlock> syntax.
94879
+ */
94880
+ const mdxishHtmlBlocks = () => tree => {
94881
+ // Handle HTMLBlock split across root children (caused by newlines)
94882
+ visit(tree, 'root', (root) => {
94883
+ const children = root.children;
94884
+ let i = 0;
94885
+ while (i < children.length) {
94886
+ const child = children[i];
94887
+ const { attrs, found: hasOpening } = hasOpeningTagOnly(child);
94888
+ if (hasOpening) {
94889
+ // Find closing tag in subsequent siblings
94890
+ let closingIdx = -1;
94891
+ for (let j = i + 1; j < children.length; j += 1) {
94892
+ if (hasClosingTag(children[j])) {
94893
+ closingIdx = j;
94894
+ break;
94895
+ }
94896
+ }
94897
+ if (closingIdx !== -1) {
94898
+ // Collect inner content between tags
94899
+ const contentParts = [];
94900
+ for (let j = i; j <= closingIdx; j += 1) {
94901
+ const node = children[j];
94902
+ contentParts.push(collectTextContent(node));
94903
+ }
94904
+ // Remove the opening/closing tags and template literal syntax from content
94905
+ let content = contentParts.join('');
94906
+ content = content.replace(/^<HTMLBlock[^>]*>\s*\{?\s*`?/, '').replace(/`?\s*\}?\s*<\/HTMLBlock>$/, '');
94907
+ // Decode protected content that was base64 encoded during preprocessing
94908
+ content = decodeProtectedContent(content);
94909
+ const htmlString = formatHtmlForMdxish(content);
94910
+ const runScripts = extractRunScriptsAttr(attrs);
94911
+ const safeMode = extractBooleanAttr(attrs, 'safeMode');
94912
+ // Replace range with single HTMLBlock node
94913
+ const mdNode = createHTMLBlockNode(htmlString, children[i].position, runScripts, safeMode);
94914
+ root.children.splice(i, closingIdx - i + 1, mdNode);
94915
+ }
94916
+ }
94917
+ i += 1;
94918
+ }
94919
+ });
94920
+ // Handle HTMLBlock parsed as HTML elements (when template literal contains block-level HTML tags)
94921
+ visit(tree, 'html', (node, index, parent) => {
94922
+ if (!parent || index === undefined)
94923
+ return;
94924
+ const value = node.value;
94925
+ if (!value)
94926
+ return;
94927
+ // Case 1: Full HTMLBlock in single node
94928
+ const fullMatch = value.match(/^<HTMLBlock(\s[^>]*)?>([\s\S]*)<\/HTMLBlock>$/);
94929
+ if (fullMatch) {
94930
+ const attrs = fullMatch[1] || '';
94931
+ let content = fullMatch[2] || '';
94932
+ // Remove template literal syntax if present: {`...`}
94933
+ content = content.replace(/^\s*\{\s*`/, '').replace(/`\s*\}\s*$/, '');
94934
+ // Decode protected content that was base64 encoded during preprocessing
94935
+ content = decodeProtectedContent(content);
94936
+ const htmlString = formatHtmlForMdxish(content);
94937
+ const runScripts = extractRunScriptsAttr(attrs);
94938
+ const safeMode = extractBooleanAttr(attrs, 'safeMode');
94939
+ parent.children[index] = createHTMLBlockNode(htmlString, node.position, runScripts, safeMode);
94940
+ return;
94941
+ }
94942
+ // Case 2: Opening tag only (split by blank lines)
94943
+ if (value === '<HTMLBlock>' || value.match(/^<HTMLBlock\s[^>]*>$/)) {
94944
+ const siblings = parent.children;
94945
+ let closingIdx = -1;
94946
+ // Find closing tag in siblings
94947
+ for (let i = index + 1; i < siblings.length; i += 1) {
94948
+ const sibling = siblings[i];
94949
+ if (sibling.type === 'html') {
94950
+ const sibVal = sibling.value;
94951
+ if (sibVal === '</HTMLBlock>' || sibVal?.includes('</HTMLBlock>')) {
94952
+ closingIdx = i;
94953
+ break;
94954
+ }
94955
+ }
94956
+ }
94957
+ if (closingIdx === -1)
94958
+ return;
94959
+ // Collect content between tags, skipping template literal delimiters
94960
+ const contentParts = [];
94961
+ for (let i = index + 1; i < closingIdx; i += 1) {
94962
+ const sibling = siblings[i];
94963
+ // Skip template literal delimiters
94964
+ if (sibling.type === 'text') {
94965
+ const textVal = sibling.value;
94966
+ if (textVal === '{' || textVal === '}' || textVal === '{`' || textVal === '`}') {
94967
+ // eslint-disable-next-line no-continue
94968
+ continue;
94969
+ }
94970
+ }
94971
+ contentParts.push(collectTextContent(sibling));
94972
+ }
94973
+ // Decode protected content that was base64 encoded during preprocessing
94974
+ const decodedContent = decodeProtectedContent(contentParts.join(''));
94975
+ const htmlString = formatHtmlForMdxish(decodedContent);
94976
+ const runScripts = extractRunScriptsAttr(value);
94977
+ const safeMode = extractBooleanAttr(value, 'safeMode');
94978
+ // Replace opening tag with HTMLBlock node, remove consumed siblings
94979
+ parent.children[index] = createHTMLBlockNode(htmlString, node.position, runScripts, safeMode);
94980
+ parent.children.splice(index + 1, closingIdx - index);
94981
+ }
94982
+ });
94983
+ // Handle HTMLBlock inside paragraphs (parsed as inline elements)
94984
+ visit(tree, 'paragraph', (node, index, parent) => {
94985
+ if (!parent || index === undefined)
94986
+ return;
94987
+ const children = node.children || [];
94988
+ let htmlBlockStartIdx = -1;
94989
+ let htmlBlockEndIdx = -1;
94990
+ let templateLiteralStartIdx = -1;
94991
+ let templateLiteralEndIdx = -1;
94992
+ for (let i = 0; i < children.length; i += 1) {
94993
+ const child = children[i];
94994
+ if (child.type === 'html' && typeof child.value === 'string') {
94995
+ const value = child.value;
94996
+ if (value === '<HTMLBlock>' || value.match(/^<HTMLBlock\s[^>]*>$/)) {
94997
+ htmlBlockStartIdx = i;
94998
+ }
94999
+ else if (value === '</HTMLBlock>') {
95000
+ htmlBlockEndIdx = i;
95001
+ }
95002
+ }
95003
+ // Find opening brace after HTMLBlock start
95004
+ if (htmlBlockStartIdx !== -1 && templateLiteralStartIdx === -1 && child.type === 'text') {
95005
+ const value = child.value;
95006
+ if (value === '{') {
95007
+ templateLiteralStartIdx = i;
95008
+ }
95009
+ }
95010
+ // Find closing brace before HTMLBlock end
95011
+ if (htmlBlockStartIdx !== -1 && htmlBlockEndIdx === -1 && child.type === 'text') {
95012
+ const value = child.value;
95013
+ if (value === '}') {
95014
+ templateLiteralEndIdx = i;
95015
+ }
95016
+ }
95017
+ }
95018
+ if (htmlBlockStartIdx !== -1 &&
95019
+ htmlBlockEndIdx !== -1 &&
95020
+ templateLiteralStartIdx !== -1 &&
95021
+ templateLiteralEndIdx !== -1 &&
95022
+ templateLiteralStartIdx < templateLiteralEndIdx) {
95023
+ const openingTag = children[htmlBlockStartIdx];
95024
+ // Collect content between braces (handles code blocks)
95025
+ const templateContent = [];
95026
+ for (let i = templateLiteralStartIdx + 1; i < templateLiteralEndIdx; i += 1) {
95027
+ const child = children[i];
95028
+ templateContent.push(collectTextContent(child));
95029
+ }
95030
+ // Decode protected content that was base64 encoded during preprocessing
95031
+ const decodedContent = decodeProtectedContent(templateContent.join(''));
95032
+ const htmlString = formatHtmlForMdxish(decodedContent);
95033
+ const runScripts = openingTag.value ? extractRunScriptsAttr(openingTag.value) : undefined;
95034
+ const safeMode = openingTag.value ? extractBooleanAttr(openingTag.value, 'safeMode') : undefined;
95035
+ const mdNode = createHTMLBlockNode(htmlString, node.position, runScripts, safeMode);
95036
+ parent.children[index] = mdNode;
95037
+ }
95038
+ });
95039
+ // Ensure html-block nodes have HTML in children as text node
95040
+ visit(tree, 'html-block', (node) => {
95041
+ const html = node.data?.hProperties?.html;
95042
+ if (html &&
95043
+ (!node.children ||
95044
+ node.children.length === 0 ||
95045
+ (node.children.length === 1 && node.children[0].type === 'text' && node.children[0].value !== html))) {
95046
+ node.children = [
95047
+ {
95048
+ type: 'text',
95049
+ value: html,
95050
+ },
95051
+ ];
95052
+ }
95053
+ });
95054
+ return tree;
95055
+ };
95056
+ /* harmony default export */ const mdxish_html_blocks = (mdxishHtmlBlocks);
95057
+
95058
+ ;// ./processor/transform/mdxish/mdxish-jsx-to-mdast.ts
95059
+
95060
+
95061
+
95062
+ const transformImage = (jsx) => {
95063
+ const attrs = getAttrs(jsx);
95064
+ const { align, alt = '', border, caption, className, height, lazy, src = '', title = '', width } = attrs;
95065
+ const hProperties = {
95066
+ alt,
95067
+ src,
95068
+ title,
95069
+ ...(align && { align }),
95070
+ ...(border !== undefined && { border: String(border) }),
95071
+ ...(caption && { caption }),
95072
+ ...(className && { className }),
95073
+ ...(height !== undefined && { height: String(height) }),
95074
+ ...(lazy !== undefined && { lazy }),
95075
+ ...(width !== undefined && { width: String(width) }),
95076
+ };
95077
+ return {
95078
+ type: NodeTypes.imageBlock,
95079
+ align,
95080
+ alt,
95081
+ border: border !== undefined ? String(border) : undefined,
95082
+ caption,
95083
+ className,
95084
+ height: height !== undefined ? String(height) : undefined,
95085
+ lazy,
95086
+ src,
95087
+ title,
95088
+ width: width !== undefined ? String(width) : undefined,
95089
+ data: {
95090
+ hName: 'img',
95091
+ hProperties,
95092
+ },
95093
+ position: jsx.position,
95094
+ };
95095
+ };
95096
+ const transformCallout = (jsx) => {
95097
+ const attrs = getAttrs(jsx);
95098
+ const { empty = false, icon = '', theme = '' } = attrs;
95099
+ return {
95100
+ type: NodeTypes.callout,
95101
+ children: jsx.children,
95102
+ data: {
95103
+ hName: 'Callout',
95104
+ hProperties: {
95105
+ empty,
95106
+ icon,
95107
+ theme,
95108
+ },
95109
+ },
95110
+ position: jsx.position,
95111
+ };
95112
+ };
95113
+ const transformEmbed = (jsx) => {
95114
+ const attrs = getAttrs(jsx);
95115
+ const { favicon, html, iframe, image, providerName, providerUrl, title = '', url = '' } = attrs;
95116
+ return {
95117
+ type: NodeTypes.embedBlock,
95118
+ title,
95119
+ url,
95120
+ data: {
95121
+ hName: 'embed',
95122
+ hProperties: {
95123
+ title,
95124
+ url,
95125
+ ...(favicon && { favicon }),
95126
+ ...(html && { html }),
95127
+ ...(iframe !== undefined && { iframe }),
95128
+ ...(image && { image }),
95129
+ ...(providerName && { providerName }),
95130
+ ...(providerUrl && { providerUrl }),
95131
+ },
95132
+ },
95133
+ position: jsx.position,
95134
+ };
95135
+ };
95136
+ const transformRecipe = (jsx) => {
95137
+ const attrs = getAttrs(jsx);
95138
+ const { backgroundColor = '', emoji = '', id = '', link = '', slug = '', title = '' } = attrs;
95139
+ return {
95140
+ type: NodeTypes.recipe,
95141
+ backgroundColor,
95142
+ emoji,
95143
+ id,
95144
+ link,
95145
+ slug,
95146
+ title,
95147
+ position: jsx.position,
95148
+ };
95149
+ };
95150
+ /**
95151
+ * Transform a magic block image node into an ImageBlock.
95152
+ * Magic block images have structure: { type: 'image', url, title, alt, data.hProperties }
95153
+ */
95154
+ const transformMagicBlockImage = (node) => {
95155
+ const { alt = '', data, position, title = '', url = '' } = node;
95156
+ const hProps = data?.hProperties || {};
95157
+ const { align, border, width } = hProps;
95158
+ const hProperties = {
95159
+ alt,
95160
+ src: url,
95161
+ title,
95162
+ ...(align && { align }),
95163
+ ...(border && { border }),
95164
+ ...(width && { width }),
95165
+ };
95166
+ return {
95167
+ type: NodeTypes.imageBlock,
95168
+ align,
95169
+ alt,
95170
+ border,
95171
+ src: url,
95172
+ title,
95173
+ width,
95174
+ data: {
95175
+ hName: 'img',
95176
+ hProperties,
95177
+ },
95178
+ position,
95179
+ };
95180
+ };
95181
+ /**
95182
+ * Transform a magic block embed node into an EmbedBlock.
95183
+ * Magic block embeds have structure: { type: 'embed', children, data.hProperties }
95184
+ */
95185
+ const transformMagicBlockEmbed = (node) => {
95186
+ const { data, position } = node;
95187
+ const hProps = data?.hProperties || {};
95188
+ const { favicon, html, image, providerName, providerUrl, title = '', url = '' } = hProps;
95189
+ return {
95190
+ type: NodeTypes.embedBlock,
95191
+ title,
95192
+ url,
95193
+ data: {
95194
+ hName: 'embed',
95195
+ hProperties: {
95196
+ title,
95197
+ url,
95198
+ ...(favicon && { favicon }),
95199
+ ...(html && { html }),
95200
+ ...(image && { image }),
95201
+ ...(providerName && { providerName }),
95202
+ ...(providerUrl && { providerUrl }),
95203
+ },
95204
+ },
95205
+ position,
95206
+ };
95207
+ };
95208
+ const COMPONENT_MAP = {
95209
+ Callout: transformCallout,
95210
+ Embed: transformEmbed,
95211
+ Image: transformImage,
95212
+ Recipe: transformRecipe,
95213
+ };
95214
+ /**
95215
+ * Transform mdxJsxFlowElement nodes and magic block nodes into proper MDAST node types.
95216
+ *
95217
+ * This transformer runs after mdxishComponentBlocks and converts:
95218
+ * - JSX component elements (Image, Callout, Embed, Recipe) into their corresponding MDAST types
95219
+ * - Magic block image nodes (type: 'image') into image-block
95220
+ * - Magic block embed nodes (type: 'embed') into embed-block
95221
+ * - Figure nodes containing images (from magic blocks with captions) - transforms the inner image
95222
+ *
95223
+ * This is controlled by the `newEditorTypes` flag to maintain backwards compatibility.
95224
+ */
95225
+ const mdxishJsxToMdast = () => tree => {
95226
+ // Transform JSX components (Image, Callout, Embed, Recipe)
95227
+ visit(tree, 'mdxJsxFlowElement', (node, index, parent) => {
95228
+ if (!parent || index === undefined || !node.name)
95229
+ return;
95230
+ const transformer = COMPONENT_MAP[node.name];
95231
+ if (!transformer)
95232
+ return;
95233
+ const newNode = transformer(node);
95234
+ // Replace the JSX node with the MDAST node
95235
+ parent.children[index] = newNode;
95236
+ });
95237
+ // Transform magic block images (type: 'image') to image-block
95238
+ // Note: Standard markdown images are wrapped in paragraphs and handled by imageTransformer
95239
+ // Magic block images are direct children of root, so we handle them here
95240
+ visit(tree, 'image', (node, index, parent) => {
95241
+ if (!parent || index === undefined)
95242
+ return SKIP;
95243
+ // Skip images inside paragraphs (those are standard markdown images handled by imageTransformer)
95244
+ if (parent.type === 'paragraph')
95245
+ return SKIP;
95246
+ const newNode = transformMagicBlockImage(node);
95247
+ parent.children[index] = newNode;
95248
+ return SKIP;
95249
+ });
95250
+ // Transform magic block embeds (type: 'embed') to embed-block
95251
+ visit(tree, 'embed', (node, index, parent) => {
95252
+ if (!parent || index === undefined)
95253
+ return SKIP;
95254
+ const newNode = transformMagicBlockEmbed(node);
95255
+ parent.children[index] = newNode;
95256
+ return SKIP;
95257
+ });
95258
+ // Transform images inside figure nodes (magic blocks with captions)
95259
+ const isFigure = (node) => node.type === 'figure';
95260
+ visit(tree, isFigure, node => {
95261
+ // Find and transform the image child
95262
+ node.children = node.children.map(child => {
95263
+ if (child.type === 'image') {
95264
+ return transformMagicBlockImage(child);
95265
+ }
95266
+ return child;
95267
+ });
95268
+ });
95269
+ return tree;
95270
+ };
95271
+ /* harmony default export */ const mdxish_jsx_to_mdast = (mdxishJsxToMdast);
94594
95272
 
94595
95273
  ;// ./processor/transform/mdxish/mdxish-mermaid.ts
94596
95274
 
@@ -94632,25 +95310,50 @@ const componentTagPattern = /<(\/?[A-Z][A-Za-z0-9_]*)([^>]*?)(\/?)>/g;
94632
95310
 
94633
95311
  ;// ./processor/transform/mdxish/mdxish-snake-case-components.ts
94634
95312
 
95313
+
94635
95314
  /**
94636
95315
  * Replaces snake_case component names with valid HTML placeholders.
94637
95316
  * Required because remark-parse rejects tags with underscores.
94638
95317
  * Example: `<Snake_case />` → `<MDXishSnakeCase0 />`
95318
+ *
95319
+ * Code blocks and inline code are protected and will not be transformed.
95320
+ *
95321
+ * @param content - The markdown content to process
95322
+ * @param options - Options including knownComponents to filter by
94639
95323
  */
94640
- function processSnakeCaseComponent(content) {
95324
+ function processSnakeCaseComponent(content, options = {}) {
95325
+ const { knownComponents } = options;
94641
95326
  // Early exit if no potential snake_case components
94642
95327
  if (!/[A-Z][A-Za-z0-9]*_[A-Za-z0-9_]*/.test(content)) {
94643
95328
  return { content, mapping: {} };
94644
95329
  }
95330
+ // Step 1: Extract code blocks to protect them from transformation
95331
+ const { protectedCode, protectedContent } = protectCodeBlocks(content);
95332
+ // Find the highest existing placeholder number to avoid collisions
95333
+ // e.g., if content has <MDXishSnakeCase0 />, start counter from 1
95334
+ const placeholderPattern = /MDXishSnakeCase(\d+)/g;
95335
+ let startCounter = 0;
95336
+ let placeholderMatch;
95337
+ while ((placeholderMatch = placeholderPattern.exec(content)) !== null) {
95338
+ const num = parseInt(placeholderMatch[1], 10);
95339
+ if (num >= startCounter) {
95340
+ startCounter = num + 1;
95341
+ }
95342
+ }
94645
95343
  const mapping = {};
94646
95344
  const reverseMap = new Map();
94647
- let counter = 0;
94648
- const processedContent = content.replace(componentTagPattern, (match, tagName, attrs, selfClosing) => {
95345
+ let counter = startCounter;
95346
+ // Step 2: Transform snake_case components in non-code content
95347
+ const processedContent = protectedContent.replace(componentTagPattern, (match, tagName, attrs, selfClosing) => {
94649
95348
  if (!tagName.includes('_')) {
94650
95349
  return match;
94651
95350
  }
94652
95351
  const isClosing = tagName.startsWith('/');
94653
95352
  const cleanTagName = isClosing ? tagName.slice(1) : tagName;
95353
+ // Only transform if it's a known component (or if no filter is provided)
95354
+ if (knownComponents && !knownComponents.has(cleanTagName)) {
95355
+ return match;
95356
+ }
94654
95357
  let placeholder = reverseMap.get(cleanTagName);
94655
95358
  if (!placeholder) {
94656
95359
  // eslint-disable-next-line no-plusplus
@@ -94661,8 +95364,10 @@ function processSnakeCaseComponent(content) {
94661
95364
  const processedTagName = isClosing ? `/${placeholder}` : placeholder;
94662
95365
  return `<${processedTagName}${attrs}${selfClosing}>`;
94663
95366
  });
95367
+ // Step 3: Restore code blocks (untouched)
95368
+ const finalContent = restoreCodeBlocks(processedContent, protectedCode);
94664
95369
  return {
94665
- content: processedContent,
95370
+ content: finalContent,
94666
95371
  mapping,
94667
95372
  };
94668
95373
  }
@@ -94679,139 +95384,6 @@ function restoreSnakeCase(placeholderName, mapping) {
94679
95384
  return matchingKey ? mapping[matchingKey] : placeholderName;
94680
95385
  }
94681
95386
 
94682
- ;// ./processor/transform/mdxish/normalize-malformed-md-syntax.ts
94683
-
94684
- // Patterns to detect for bold (** and __) and italic (* and _) syntax:
94685
- // Bold: ** text**, **text **, word** text**, ** text **
94686
- // Italic: * text*, *text *, word* text*, * text *
94687
- // Same patterns for underscore variants
94688
- // We use separate patterns for each marker type to allow this flexibility.
94689
- // Pattern for ** bold **
94690
- // Groups: 1=wordBefore, 2=marker, 3=contentWithSpaceAfter, 4=trailingSpace1, 5=contentWithSpaceBefore, 6=trailingSpace2, 7=afterChar
94691
- // trailingSpace1 is for "** text **" pattern, trailingSpace2 is for "**text **" pattern
94692
- const asteriskBoldRegex = /([^*\s]+)?\s*(\*\*)(?:\s+((?:[^*\n]|\*(?!\*))+?)(\s*)\2|((?:[^*\n]|\*(?!\*))+?)(\s+)\2)(\S|$)?/g;
94693
- // Pattern for __ bold __
94694
- const underscoreBoldRegex = /([^_\s]+)?\s*(__)(?:\s+((?:[^_\n]|_(?!_))+?)(\s*)\2|((?:[^_\n]|_(?!_))+?)(\s+)\2)(\S|$)?/g;
94695
- // Pattern for * italic *
94696
- const asteriskItalicRegex = /([^*\s]+)?\s*(\*)(?!\*)(?:\s+([^*\n]+?)(\s*)\2|([^*\n]+?)(\s+)\2)(\S|$)?/g;
94697
- // Pattern for _ italic _
94698
- const underscoreItalicRegex = /([^_\s]+)?\s*(_)(?!_)(?:\s+([^_\n]+?)(\s*)\2|([^_\n]+?)(\s+)\2)(\S|$)?/g;
94699
- /**
94700
- * A remark plugin that normalizes malformed bold and italic markers in text nodes.
94701
- * Detects patterns like `** bold**`, `Hello** Wrong Bold**`, `__ bold__`, `Hello__ Wrong Bold__`,
94702
- * `* italic*`, `Hello* Wrong Italic*`, `_ italic_`, or `Hello_ Wrong Italic_`
94703
- * and converts them to proper strong/emphasis nodes, matching the behavior of the legacy rdmd engine.
94704
- *
94705
- * Supports both asterisk (`**bold**`, `*italic*`) and underscore (`__bold__`, `_italic_`) syntax.
94706
- * Also supports snake_case content like `** some_snake_case**`.
94707
- *
94708
- * This runs after remark-parse, which (in v11+) is strict and doesn't parse
94709
- * malformed emphasis syntax. This plugin post-processes the AST to handle these cases.
94710
- */
94711
- const normalizeEmphasisAST = () => (tree) => {
94712
- visit(tree, 'text', function visitor(node, index, parent) {
94713
- if (index === undefined || !parent)
94714
- return undefined;
94715
- // Skip if inside code blocks or inline code
94716
- if (parent.type === 'inlineCode' || parent.type === 'code') {
94717
- return undefined;
94718
- }
94719
- const text = node.value;
94720
- const allMatches = [];
94721
- [...text.matchAll(asteriskBoldRegex)].forEach(match => {
94722
- allMatches.push({ isBold: true, marker: '**', match });
94723
- });
94724
- [...text.matchAll(underscoreBoldRegex)].forEach(match => {
94725
- allMatches.push({ isBold: true, marker: '__', match });
94726
- });
94727
- [...text.matchAll(asteriskItalicRegex)].forEach(match => {
94728
- allMatches.push({ isBold: false, marker: '*', match });
94729
- });
94730
- [...text.matchAll(underscoreItalicRegex)].forEach(match => {
94731
- allMatches.push({ isBold: false, marker: '_', match });
94732
- });
94733
- if (allMatches.length === 0)
94734
- return undefined;
94735
- allMatches.sort((a, b) => (a.match.index ?? 0) - (b.match.index ?? 0));
94736
- const filteredMatches = [];
94737
- let lastEnd = 0;
94738
- allMatches.forEach(info => {
94739
- const start = info.match.index ?? 0;
94740
- const end = start + info.match[0].length;
94741
- if (start >= lastEnd) {
94742
- filteredMatches.push(info);
94743
- lastEnd = end;
94744
- }
94745
- });
94746
- if (filteredMatches.length === 0)
94747
- return undefined;
94748
- const parts = [];
94749
- let lastIndex = 0;
94750
- filteredMatches.forEach(({ match, marker, isBold }) => {
94751
- const matchIndex = match.index ?? 0;
94752
- const fullMatch = match[0];
94753
- if (matchIndex > lastIndex) {
94754
- const beforeText = text.slice(lastIndex, matchIndex);
94755
- if (beforeText) {
94756
- parts.push({ type: 'text', value: beforeText });
94757
- }
94758
- }
94759
- const wordBefore = match[1]; // e.g., "Hello" in "Hello** Wrong Bold**"
94760
- const contentWithSpaceAfter = match[3]; // Content when there's a space after opening markers
94761
- const trailingSpace1 = match[4] || ''; // Space before closing markers (for "** text **" pattern)
94762
- const contentWithSpaceBefore = match[5]; // Content when there's only a space before closing markers
94763
- const trailingSpace2 = match[6] || ''; // Space before closing markers (for "**text **" pattern)
94764
- const trailingSpace = trailingSpace1 || trailingSpace2; // Combined trailing space
94765
- const content = (contentWithSpaceAfter || contentWithSpaceBefore || '').trim();
94766
- const afterChar = match[7]; // Character after closing markers (if any)
94767
- const markerPos = fullMatch.indexOf(marker);
94768
- const spacesBeforeMarkers = wordBefore
94769
- ? fullMatch.slice(wordBefore.length, markerPos)
94770
- : fullMatch.slice(0, markerPos);
94771
- const shouldAddSpace = !!contentWithSpaceAfter && !!wordBefore && !spacesBeforeMarkers;
94772
- if (wordBefore) {
94773
- const spacing = spacesBeforeMarkers + (shouldAddSpace ? ' ' : '');
94774
- parts.push({ type: 'text', value: wordBefore + spacing });
94775
- }
94776
- else if (spacesBeforeMarkers) {
94777
- parts.push({ type: 'text', value: spacesBeforeMarkers });
94778
- }
94779
- if (content) {
94780
- if (isBold) {
94781
- parts.push({
94782
- type: 'strong',
94783
- children: [{ type: 'text', value: content }],
94784
- });
94785
- }
94786
- else {
94787
- parts.push({
94788
- type: 'emphasis',
94789
- children: [{ type: 'text', value: content }],
94790
- });
94791
- }
94792
- }
94793
- if (afterChar) {
94794
- const prefix = trailingSpace ? ' ' : '';
94795
- parts.push({ type: 'text', value: prefix + afterChar });
94796
- }
94797
- lastIndex = matchIndex + fullMatch.length;
94798
- });
94799
- if (lastIndex < text.length) {
94800
- const remainingText = text.slice(lastIndex);
94801
- if (remainingText) {
94802
- parts.push({ type: 'text', value: remainingText });
94803
- }
94804
- }
94805
- if (parts.length > 0) {
94806
- parent.children.splice(index, 1, ...parts);
94807
- return [SKIP, index + parts.length];
94808
- }
94809
- return undefined;
94810
- });
94811
- return tree;
94812
- };
94813
- /* harmony default export */ const normalize_malformed_md_syntax = (normalizeEmphasisAST);
94814
-
94815
95387
  ;// ./processor/transform/mdxish/normalize-table-separator.ts
94816
95388
  /**
94817
95389
  * Preprocessor to normalize malformed GFM table separator syntax.
@@ -95044,57 +95616,1172 @@ const variablesTextTransformer = () => tree => {
95044
95616
  };
95045
95617
  /* harmony default export */ const variables_text = (variablesTextTransformer);
95046
95618
 
95047
- ;// ./lib/utils/extractMagicBlocks.ts
95619
+ ;// ./lib/mdast-util/magic-block/index.ts
95620
+ const contextMap = new WeakMap();
95048
95621
  /**
95049
- * The content matching in this regex captures everything between `[block:TYPE]`
95050
- * and `[/block]`, including new lines. Negative lookahead for the closing
95051
- * `[/block]` tag is required to prevent greedy matching to ensure it stops at
95052
- * the first closing tag it encounters preventing vulnerability to polynomial
95053
- * backtracking issues.
95622
+ * Find the magicBlock token in the token ancestry.
95054
95623
  */
95055
- const MAGIC_BLOCK_REGEX = /\[block:[^\]]{1,100}\](?:(?!\[block:)(?!\[\/block\])[\s\S])*\[\/block\]/g;
95624
+ function findMagicBlockToken() {
95625
+ // Walk up the token stack to find the magicBlock token
95626
+ const events = this.tokenStack;
95627
+ for (let i = events.length - 1; i >= 0; i -= 1) {
95628
+ const token = events[i][0];
95629
+ if (token.type === 'magicBlock') {
95630
+ return token;
95631
+ }
95632
+ }
95633
+ return undefined;
95634
+ }
95056
95635
  /**
95057
- * Extract legacy magic block syntax from a markdown string.
95058
- * Returns the modified markdown and an array of extracted blocks.
95636
+ * Enter handler: Create a new magicBlock node.
95059
95637
  */
95060
- function extractMagicBlocks(markdown) {
95061
- const blocks = [];
95062
- let index = 0;
95063
- const replaced = markdown.replace(MAGIC_BLOCK_REGEX, match => {
95064
- /**
95065
- * Key is the unique identifier for the magic block
95066
- */
95067
- const key = `__MAGIC_BLOCK_${index}__`;
95068
- /**
95069
- * Token is a wrapper around the `key` to serialize & influence how the
95070
- * magic block is parsed in the remark pipeline.
95071
- * - Use backticks so it becomes a code span, preventing `remarkParse` from
95072
- * parsing special characters in the token as markdown syntax
95073
- * - Prepend a newline to ensure it is parsed as a block level node
95074
- * - Append a newline to ensure it is separated from following content
95075
- */
95076
- const token = `\n\`${key}\`\n`;
95077
- blocks.push({ key, raw: match, token });
95078
- index += 1;
95079
- return token;
95080
- });
95081
- return { replaced, blocks };
95638
+ function enterMagicBlock(token) {
95639
+ // Initialize context for this magic block
95640
+ contextMap.set(token, { blockType: '', dataChunks: [] });
95641
+ this.enter({
95642
+ type: 'magicBlock',
95643
+ blockType: '',
95644
+ data: {},
95645
+ value: '',
95646
+ }, token);
95082
95647
  }
95083
95648
  /**
95084
- * Restore extracted magic blocks back into a markdown string.
95649
+ * Exit handler for block type: Extract the block type from the token.
95085
95650
  */
95086
- function restoreMagicBlocks(replaced, blocks) {
95087
- // If a magic block is at the start or end of the document, the extraction
95088
- // token's newlines will have been trimmed during processing. We need to
95089
- // account for that here to ensure the token is found and replaced correctly.
95090
- // These extra newlines will be removed again when the final string is trimmed.
95091
- const content = `\n${replaced}\n`;
95092
- const restoredContent = blocks.reduce((acc, { token, raw }) => {
95093
- // Ensure each magic block is separated by newlines when restored.
95094
- return acc.split(token).join(`\n${raw}\n`);
95095
- }, content);
95096
- return restoredContent.trim();
95651
+ function exitMagicBlockType(token) {
95652
+ const blockToken = findMagicBlockToken.call(this);
95653
+ if (!blockToken)
95654
+ return;
95655
+ const context = contextMap.get(blockToken);
95656
+ if (context) {
95657
+ context.blockType = this.sliceSerialize(token);
95658
+ }
95659
+ }
95660
+ /**
95661
+ * Exit handler for block data: Accumulate JSON content chunks.
95662
+ */
95663
+ function exitMagicBlockData(token) {
95664
+ const blockToken = findMagicBlockToken.call(this);
95665
+ if (!blockToken)
95666
+ return;
95667
+ const context = contextMap.get(blockToken);
95668
+ if (context) {
95669
+ context.dataChunks.push(this.sliceSerialize(token));
95670
+ }
95671
+ }
95672
+ /**
95673
+ * Exit handler for line endings: Preserve newlines in multiline blocks.
95674
+ */
95675
+ function exitMagicBlockLineEnding(token) {
95676
+ const blockToken = findMagicBlockToken.call(this);
95677
+ if (!blockToken)
95678
+ return;
95679
+ const context = contextMap.get(blockToken);
95680
+ if (context) {
95681
+ context.dataChunks.push(this.sliceSerialize(token));
95682
+ }
95097
95683
  }
95684
+ /**
95685
+ * Exit handler for end marker: If this is a failed end marker check (not the final marker),
95686
+ * add its content to the data chunks so we don't lose characters like '['.
95687
+ */
95688
+ function exitMagicBlockMarkerEnd(token) {
95689
+ const blockToken = findMagicBlockToken.call(this);
95690
+ if (!blockToken)
95691
+ return;
95692
+ // Get the content of the marker
95693
+ const markerContent = this.sliceSerialize(token);
95694
+ // If this marker doesn't end with ']', it's a failed check and content belongs to data
95695
+ // The successful end marker would be "[/block]"
95696
+ if (!markerContent.endsWith(']') || markerContent !== '[/block]') {
95697
+ const context = contextMap.get(blockToken);
95698
+ if (context) {
95699
+ context.dataChunks.push(markerContent);
95700
+ }
95701
+ }
95702
+ }
95703
+ /**
95704
+ * Exit handler: Finalize the magicBlock node with parsed JSON data.
95705
+ */
95706
+ function exitMagicBlock(token) {
95707
+ const context = contextMap.get(token);
95708
+ const node = this.stack[this.stack.length - 1];
95709
+ if (context) {
95710
+ const rawJson = context.dataChunks.join('');
95711
+ node.blockType = context.blockType;
95712
+ node.value = `[block:${context.blockType}]${rawJson}[/block]`;
95713
+ // Parse JSON data
95714
+ try {
95715
+ node.data = JSON.parse(rawJson.trim());
95716
+ }
95717
+ catch {
95718
+ // Invalid JSON - store empty object but keep the raw value
95719
+ node.data = {};
95720
+ }
95721
+ // Clean up context
95722
+ contextMap.delete(token);
95723
+ }
95724
+ this.exit(token);
95725
+ }
95726
+ /**
95727
+ * Handler to serialize magicBlock nodes back to markdown.
95728
+ */
95729
+ const handleMagicBlock = (node) => {
95730
+ const magicNode = node;
95731
+ // If we have the original raw value, use it
95732
+ if (magicNode.value) {
95733
+ return magicNode.value;
95734
+ }
95735
+ // Otherwise reconstruct from parsed data
95736
+ const json = JSON.stringify(magicNode.data, null, 2);
95737
+ return `[block:${magicNode.blockType}]\n${json}\n[/block]`;
95738
+ };
95739
+ /**
95740
+ * Create an extension for `mdast-util-from-markdown` to enable magic blocks.
95741
+ *
95742
+ * Converts micromark magic block tokens into `magicBlock` MDAST nodes.
95743
+ *
95744
+ * @returns Extension for `mdast-util-from-markdown`
95745
+ */
95746
+ function magicBlockFromMarkdown() {
95747
+ return {
95748
+ enter: {
95749
+ magicBlock: enterMagicBlock,
95750
+ },
95751
+ exit: {
95752
+ magicBlockType: exitMagicBlockType,
95753
+ magicBlockData: exitMagicBlockData,
95754
+ magicBlockLineEnding: exitMagicBlockLineEnding,
95755
+ magicBlockMarkerEnd: exitMagicBlockMarkerEnd,
95756
+ magicBlock: exitMagicBlock,
95757
+ },
95758
+ };
95759
+ }
95760
+ /**
95761
+ * Create an extension for `mdast-util-to-markdown` to serialize magic blocks.
95762
+ *
95763
+ * Converts `magicBlock` MDAST nodes back to `[block:TYPE]JSON[/block]` syntax.
95764
+ *
95765
+ * @returns Extension for `mdast-util-to-markdown`
95766
+ */
95767
+ function magicBlockToMarkdown() {
95768
+ return {
95769
+ handlers: {
95770
+ magicBlock: handleMagicBlock,
95771
+ },
95772
+ };
95773
+ }
95774
+
95775
+ ;// ./node_modules/micromark-util-symbol/lib/codes.js
95776
+ /**
95777
+ * Character codes.
95778
+ *
95779
+ * This module is compiled away!
95780
+ *
95781
+ * micromark works based on character codes.
95782
+ * This module contains constants for the ASCII block and the replacement
95783
+ * character.
95784
+ * A couple of them are handled in a special way, such as the line endings
95785
+ * (CR, LF, and CR+LF, commonly known as end-of-line: EOLs), the tab (horizontal
95786
+ * tab) and its expansion based on what column it’s at (virtual space),
95787
+ * and the end-of-file (eof) character.
95788
+ * As values are preprocessed before handling them, the actual characters LF,
95789
+ * CR, HT, and NUL (which is present as the replacement character), are
95790
+ * guaranteed to not exist.
95791
+ *
95792
+ * Unicode basic latin block.
95793
+ */
95794
+ const codes = /** @type {const} */ ({
95795
+ carriageReturn: -5,
95796
+ lineFeed: -4,
95797
+ carriageReturnLineFeed: -3,
95798
+ horizontalTab: -2,
95799
+ virtualSpace: -1,
95800
+ eof: null,
95801
+ nul: 0,
95802
+ soh: 1,
95803
+ stx: 2,
95804
+ etx: 3,
95805
+ eot: 4,
95806
+ enq: 5,
95807
+ ack: 6,
95808
+ bel: 7,
95809
+ bs: 8,
95810
+ ht: 9, // `\t`
95811
+ lf: 10, // `\n`
95812
+ vt: 11, // `\v`
95813
+ ff: 12, // `\f`
95814
+ cr: 13, // `\r`
95815
+ so: 14,
95816
+ si: 15,
95817
+ dle: 16,
95818
+ dc1: 17,
95819
+ dc2: 18,
95820
+ dc3: 19,
95821
+ dc4: 20,
95822
+ nak: 21,
95823
+ syn: 22,
95824
+ etb: 23,
95825
+ can: 24,
95826
+ em: 25,
95827
+ sub: 26,
95828
+ esc: 27,
95829
+ fs: 28,
95830
+ gs: 29,
95831
+ rs: 30,
95832
+ us: 31,
95833
+ space: 32,
95834
+ exclamationMark: 33, // `!`
95835
+ quotationMark: 34, // `"`
95836
+ numberSign: 35, // `#`
95837
+ dollarSign: 36, // `$`
95838
+ percentSign: 37, // `%`
95839
+ ampersand: 38, // `&`
95840
+ apostrophe: 39, // `'`
95841
+ leftParenthesis: 40, // `(`
95842
+ rightParenthesis: 41, // `)`
95843
+ asterisk: 42, // `*`
95844
+ plusSign: 43, // `+`
95845
+ comma: 44, // `,`
95846
+ dash: 45, // `-`
95847
+ dot: 46, // `.`
95848
+ slash: 47, // `/`
95849
+ digit0: 48, // `0`
95850
+ digit1: 49, // `1`
95851
+ digit2: 50, // `2`
95852
+ digit3: 51, // `3`
95853
+ digit4: 52, // `4`
95854
+ digit5: 53, // `5`
95855
+ digit6: 54, // `6`
95856
+ digit7: 55, // `7`
95857
+ digit8: 56, // `8`
95858
+ digit9: 57, // `9`
95859
+ colon: 58, // `:`
95860
+ semicolon: 59, // `;`
95861
+ lessThan: 60, // `<`
95862
+ equalsTo: 61, // `=`
95863
+ greaterThan: 62, // `>`
95864
+ questionMark: 63, // `?`
95865
+ atSign: 64, // `@`
95866
+ uppercaseA: 65, // `A`
95867
+ uppercaseB: 66, // `B`
95868
+ uppercaseC: 67, // `C`
95869
+ uppercaseD: 68, // `D`
95870
+ uppercaseE: 69, // `E`
95871
+ uppercaseF: 70, // `F`
95872
+ uppercaseG: 71, // `G`
95873
+ uppercaseH: 72, // `H`
95874
+ uppercaseI: 73, // `I`
95875
+ uppercaseJ: 74, // `J`
95876
+ uppercaseK: 75, // `K`
95877
+ uppercaseL: 76, // `L`
95878
+ uppercaseM: 77, // `M`
95879
+ uppercaseN: 78, // `N`
95880
+ uppercaseO: 79, // `O`
95881
+ uppercaseP: 80, // `P`
95882
+ uppercaseQ: 81, // `Q`
95883
+ uppercaseR: 82, // `R`
95884
+ uppercaseS: 83, // `S`
95885
+ uppercaseT: 84, // `T`
95886
+ uppercaseU: 85, // `U`
95887
+ uppercaseV: 86, // `V`
95888
+ uppercaseW: 87, // `W`
95889
+ uppercaseX: 88, // `X`
95890
+ uppercaseY: 89, // `Y`
95891
+ uppercaseZ: 90, // `Z`
95892
+ leftSquareBracket: 91, // `[`
95893
+ backslash: 92, // `\`
95894
+ rightSquareBracket: 93, // `]`
95895
+ caret: 94, // `^`
95896
+ underscore: 95, // `_`
95897
+ graveAccent: 96, // `` ` ``
95898
+ lowercaseA: 97, // `a`
95899
+ lowercaseB: 98, // `b`
95900
+ lowercaseC: 99, // `c`
95901
+ lowercaseD: 100, // `d`
95902
+ lowercaseE: 101, // `e`
95903
+ lowercaseF: 102, // `f`
95904
+ lowercaseG: 103, // `g`
95905
+ lowercaseH: 104, // `h`
95906
+ lowercaseI: 105, // `i`
95907
+ lowercaseJ: 106, // `j`
95908
+ lowercaseK: 107, // `k`
95909
+ lowercaseL: 108, // `l`
95910
+ lowercaseM: 109, // `m`
95911
+ lowercaseN: 110, // `n`
95912
+ lowercaseO: 111, // `o`
95913
+ lowercaseP: 112, // `p`
95914
+ lowercaseQ: 113, // `q`
95915
+ lowercaseR: 114, // `r`
95916
+ lowercaseS: 115, // `s`
95917
+ lowercaseT: 116, // `t`
95918
+ lowercaseU: 117, // `u`
95919
+ lowercaseV: 118, // `v`
95920
+ lowercaseW: 119, // `w`
95921
+ lowercaseX: 120, // `x`
95922
+ lowercaseY: 121, // `y`
95923
+ lowercaseZ: 122, // `z`
95924
+ leftCurlyBrace: 123, // `{`
95925
+ verticalBar: 124, // `|`
95926
+ rightCurlyBrace: 125, // `}`
95927
+ tilde: 126, // `~`
95928
+ del: 127,
95929
+ // Unicode Specials block.
95930
+ byteOrderMarker: 65_279,
95931
+ // Unicode Specials block.
95932
+ replacementCharacter: 65_533 // `�`
95933
+ })
95934
+
95935
+ ;// ./lib/micromark/magic-block/syntax.ts
95936
+
95937
+
95938
+ /**
95939
+ * Known magic block types that the tokenizer will recognize.
95940
+ * Unknown types will not be tokenized as magic blocks.
95941
+ */
95942
+ const KNOWN_BLOCK_TYPES = new Set([
95943
+ 'code',
95944
+ 'api-header',
95945
+ 'image',
95946
+ 'callout',
95947
+ 'parameters',
95948
+ 'table',
95949
+ 'embed',
95950
+ 'html',
95951
+ 'recipe',
95952
+ 'tutorial-tile',
95953
+ ]);
95954
+ /**
95955
+ * Check if a character is valid for a magic block type identifier.
95956
+ * Types can contain alphanumeric characters and hyphens (e.g., "api-header", "tutorial-tile")
95957
+ */
95958
+ function isTypeChar(code) {
95959
+ return asciiAlphanumeric(code) || code === codes.dash;
95960
+ }
95961
+ /**
95962
+ * Creates the opening marker state machine: [block:
95963
+ * Returns the first state function to start parsing.
95964
+ */
95965
+ function createOpeningMarkerParser(effects, nok, onComplete) {
95966
+ const expectB = (code) => {
95967
+ if (code !== codes.lowercaseB)
95968
+ return nok(code);
95969
+ effects.consume(code);
95970
+ return expectL;
95971
+ };
95972
+ const expectL = (code) => {
95973
+ if (code !== codes.lowercaseL)
95974
+ return nok(code);
95975
+ effects.consume(code);
95976
+ return expectO;
95977
+ };
95978
+ const expectO = (code) => {
95979
+ if (code !== codes.lowercaseO)
95980
+ return nok(code);
95981
+ effects.consume(code);
95982
+ return expectC;
95983
+ };
95984
+ const expectC = (code) => {
95985
+ if (code !== codes.lowercaseC)
95986
+ return nok(code);
95987
+ effects.consume(code);
95988
+ return expectK;
95989
+ };
95990
+ const expectK = (code) => {
95991
+ if (code !== codes.lowercaseK)
95992
+ return nok(code);
95993
+ effects.consume(code);
95994
+ return expectColon;
95995
+ };
95996
+ const expectColon = (code) => {
95997
+ if (code !== codes.colon)
95998
+ return nok(code);
95999
+ effects.consume(code);
96000
+ effects.exit('magicBlockMarkerStart');
96001
+ effects.enter('magicBlockType');
96002
+ return onComplete;
96003
+ };
96004
+ return expectB;
96005
+ }
96006
+ /**
96007
+ * Creates the type capture state machine.
96008
+ * Captures type characters until ] and validates against known types.
96009
+ */
96010
+ function createTypeCaptureParser(effects, nok, onComplete, blockTypeRef) {
96011
+ const captureTypeFirst = (code) => {
96012
+ // Reject empty type name [block:]
96013
+ if (code === codes.rightSquareBracket) {
96014
+ return nok(code);
96015
+ }
96016
+ if (isTypeChar(code)) {
96017
+ blockTypeRef.value += String.fromCharCode(code);
96018
+ effects.consume(code);
96019
+ return captureType;
96020
+ }
96021
+ return nok(code);
96022
+ };
96023
+ const captureType = (code) => {
96024
+ if (code === codes.rightSquareBracket) {
96025
+ if (!KNOWN_BLOCK_TYPES.has(blockTypeRef.value)) {
96026
+ return nok(code);
96027
+ }
96028
+ effects.exit('magicBlockType');
96029
+ effects.enter('magicBlockMarkerTypeEnd');
96030
+ effects.consume(code);
96031
+ effects.exit('magicBlockMarkerTypeEnd');
96032
+ return onComplete;
96033
+ }
96034
+ if (isTypeChar(code)) {
96035
+ blockTypeRef.value += String.fromCharCode(code);
96036
+ effects.consume(code);
96037
+ return captureType;
96038
+ }
96039
+ return nok(code);
96040
+ };
96041
+ return { first: captureTypeFirst, remaining: captureType };
96042
+ }
96043
+ /**
96044
+ * Creates the closing marker state machine: /block]
96045
+ * Handles partial matches by calling onMismatch to fall back to data capture.
96046
+ */
96047
+ function createClosingMarkerParser(effects, onSuccess, onMismatch, onEof, jsonState) {
96048
+ const handleMismatch = (code) => {
96049
+ if (jsonState && code === codes.quotationMark) {
96050
+ jsonState.inString = true;
96051
+ }
96052
+ return onMismatch(code);
96053
+ };
96054
+ const expectSlash = (code) => {
96055
+ if (code === null)
96056
+ return onEof(code);
96057
+ if (code !== codes.slash)
96058
+ return handleMismatch(code);
96059
+ effects.consume(code);
96060
+ return expectB;
96061
+ };
96062
+ const expectB = (code) => {
96063
+ if (code === null)
96064
+ return onEof(code);
96065
+ if (code !== codes.lowercaseB)
96066
+ return handleMismatch(code);
96067
+ effects.consume(code);
96068
+ return expectL;
96069
+ };
96070
+ const expectL = (code) => {
96071
+ if (code === null)
96072
+ return onEof(code);
96073
+ if (code !== codes.lowercaseL)
96074
+ return handleMismatch(code);
96075
+ effects.consume(code);
96076
+ return expectO;
96077
+ };
96078
+ const expectO = (code) => {
96079
+ if (code === null)
96080
+ return onEof(code);
96081
+ if (code !== codes.lowercaseO)
96082
+ return handleMismatch(code);
96083
+ effects.consume(code);
96084
+ return expectC;
96085
+ };
96086
+ const expectC = (code) => {
96087
+ if (code === null)
96088
+ return onEof(code);
96089
+ if (code !== codes.lowercaseC)
96090
+ return handleMismatch(code);
96091
+ effects.consume(code);
96092
+ return expectK;
96093
+ };
96094
+ const expectK = (code) => {
96095
+ if (code === null)
96096
+ return onEof(code);
96097
+ if (code !== codes.lowercaseK)
96098
+ return handleMismatch(code);
96099
+ effects.consume(code);
96100
+ return expectBracket;
96101
+ };
96102
+ const expectBracket = (code) => {
96103
+ if (code === null)
96104
+ return onEof(code);
96105
+ if (code !== codes.rightSquareBracket)
96106
+ return handleMismatch(code);
96107
+ effects.consume(code);
96108
+ return onSuccess;
96109
+ };
96110
+ return { expectSlash };
96111
+ }
96112
+ /**
96113
+ * Partial construct for checking non-lazy continuation.
96114
+ * This is used by the flow tokenizer to check if we can continue
96115
+ * parsing on the next line.
96116
+ */
96117
+ const syntax_nonLazyContinuation = {
96118
+ partial: true,
96119
+ tokenize: syntax_tokenizeNonLazyContinuation,
96120
+ };
96121
+ /**
96122
+ * Tokenizer for non-lazy continuation checking.
96123
+ * Returns ok if the next line is non-lazy (can continue), nok if lazy.
96124
+ */
96125
+ function syntax_tokenizeNonLazyContinuation(effects, ok, nok) {
96126
+ const lineStart = (code) => {
96127
+ // `this` here refers to the micromark parser
96128
+ // since we are just passing functions as references and not actually calling them
96129
+ // micromarks's internal parser will call this automatically with appropriate arguments
96130
+ return this.parser.lazy[this.now().line] ? nok(code) : ok(code);
96131
+ };
96132
+ const start = (code) => {
96133
+ if (code === null) {
96134
+ return nok(code);
96135
+ }
96136
+ if (!markdownLineEnding(code)) {
96137
+ return nok(code);
96138
+ }
96139
+ effects.enter('magicBlockLineEnding');
96140
+ effects.consume(code);
96141
+ effects.exit('magicBlockLineEnding');
96142
+ return lineStart;
96143
+ };
96144
+ return start;
96145
+ }
96146
+ /**
96147
+ * Create a micromark extension for magic block syntax.
96148
+ *
96149
+ * This extension handles both single-line and multiline magic blocks:
96150
+ * - Flow construct (concrete): Handles block-level multiline magic blocks at document level
96151
+ * - Text construct: Handles inline magic blocks in lists, paragraphs, etc.
96152
+ *
96153
+ * The flow construct is marked as "concrete" which prevents it from being
96154
+ * interrupted by container markers (like `>` for blockquotes or `-` for lists).
96155
+ */
96156
+ function magicBlock() {
96157
+ return {
96158
+ // Flow construct - handles block-level magic blocks at document root
96159
+ // Marked as concrete to prevent interruption by containers
96160
+ flow: {
96161
+ [codes.leftSquareBracket]: {
96162
+ name: 'magicBlock',
96163
+ concrete: true,
96164
+ tokenize: tokenizeMagicBlockFlow,
96165
+ },
96166
+ },
96167
+ // Text construct - handles magic blocks in inline contexts (lists, paragraphs)
96168
+ text: {
96169
+ [codes.leftSquareBracket]: {
96170
+ name: 'magicBlock',
96171
+ tokenize: tokenizeMagicBlockText,
96172
+ },
96173
+ },
96174
+ };
96175
+ }
96176
+ /**
96177
+ * Flow tokenizer for block-level magic blocks (multiline).
96178
+ * Uses the continuation checking pattern from code fences.
96179
+ */
96180
+ function tokenizeMagicBlockFlow(effects, ok, nok) {
96181
+ // State for tracking JSON content
96182
+ const jsonState = { escapeNext: false, inString: false };
96183
+ const blockTypeRef = { value: '' };
96184
+ let seenOpenBrace = false;
96185
+ // Create shared parsers for opening marker and type capture
96186
+ const typeParser = createTypeCaptureParser(effects, nok, beforeData, blockTypeRef);
96187
+ const openingMarkerParser = createOpeningMarkerParser(effects, nok, typeParser.first);
96188
+ return start;
96189
+ function start(code) {
96190
+ if (code !== codes.leftSquareBracket)
96191
+ return nok(code);
96192
+ effects.enter('magicBlock');
96193
+ effects.enter('magicBlockMarkerStart');
96194
+ effects.consume(code);
96195
+ return openingMarkerParser;
96196
+ }
96197
+ /**
96198
+ * State before data content - handles line endings or starts data capture.
96199
+ */
96200
+ function beforeData(code) {
96201
+ // EOF - magic block must be closed
96202
+ if (code === null) {
96203
+ effects.exit('magicBlock');
96204
+ return nok(code);
96205
+ }
96206
+ // Line ending before any data - check continuation
96207
+ if (markdownLineEnding(code)) {
96208
+ return effects.check(syntax_nonLazyContinuation, continuationOkBeforeData, after)(code);
96209
+ }
96210
+ // Check for closing marker directly (without entering data)
96211
+ // This handles cases like [block:type]\n{}\n\n[/block] where there are
96212
+ // newlines after the data object
96213
+ if (code === codes.leftSquareBracket) {
96214
+ effects.enter('magicBlockMarkerEnd');
96215
+ effects.consume(code);
96216
+ return expectSlashFromContinuation;
96217
+ }
96218
+ // If we've already seen the opening brace, just continue capturing data
96219
+ if (seenOpenBrace) {
96220
+ effects.enter('magicBlockData');
96221
+ return captureData(code);
96222
+ }
96223
+ // Skip whitespace (spaces/tabs) before the data - stay in beforeData
96224
+ // Don't enter magicBlockData token yet until we confirm there's a '{'
96225
+ if (code === codes.space || code === codes.horizontalTab) {
96226
+ return beforeDataWhitespace(code);
96227
+ }
96228
+ // Data must start with '{' for valid JSON
96229
+ if (code !== codes.leftCurlyBrace) {
96230
+ effects.exit('magicBlock');
96231
+ return nok(code);
96232
+ }
96233
+ // We have '{' - enter data token and start capturing
96234
+ seenOpenBrace = true;
96235
+ effects.enter('magicBlockData');
96236
+ return captureData(code);
96237
+ }
96238
+ /**
96239
+ * Consume whitespace before the data without creating a token.
96240
+ * Uses a temporary token to satisfy micromark's requirement.
96241
+ */
96242
+ function beforeDataWhitespace(code) {
96243
+ if (code === null) {
96244
+ effects.exit('magicBlock');
96245
+ return nok(code);
96246
+ }
96247
+ if (markdownLineEnding(code)) {
96248
+ return effects.check(syntax_nonLazyContinuation, continuationOkBeforeData, after)(code);
96249
+ }
96250
+ if (code === codes.space || code === codes.horizontalTab) {
96251
+ // We need to consume this but can't without a token - use magicBlockData
96252
+ // and track that we haven't seen '{' yet
96253
+ effects.enter('magicBlockData');
96254
+ effects.consume(code);
96255
+ return beforeDataWhitespaceContinue;
96256
+ }
96257
+ if (code === codes.leftCurlyBrace) {
96258
+ seenOpenBrace = true;
96259
+ effects.enter('magicBlockData');
96260
+ return captureData(code);
96261
+ }
96262
+ effects.exit('magicBlock');
96263
+ return nok(code);
96264
+ }
96265
+ /**
96266
+ * Continue consuming whitespace or validate the opening brace.
96267
+ */
96268
+ function beforeDataWhitespaceContinue(code) {
96269
+ if (code === null) {
96270
+ effects.exit('magicBlockData');
96271
+ effects.exit('magicBlock');
96272
+ return nok(code);
96273
+ }
96274
+ if (markdownLineEnding(code)) {
96275
+ effects.exit('magicBlockData');
96276
+ return effects.check(syntax_nonLazyContinuation, continuationOk, after)(code);
96277
+ }
96278
+ if (code === codes.space || code === codes.horizontalTab) {
96279
+ effects.consume(code);
96280
+ return beforeDataWhitespaceContinue;
96281
+ }
96282
+ if (code === codes.leftCurlyBrace) {
96283
+ seenOpenBrace = true;
96284
+ return captureData(code);
96285
+ }
96286
+ effects.exit('magicBlockData');
96287
+ effects.exit('magicBlock');
96288
+ return nok(code);
96289
+ }
96290
+ /**
96291
+ * Continuation OK before we've entered data token.
96292
+ */
96293
+ function continuationOkBeforeData(code) {
96294
+ effects.enter('magicBlockLineEnding');
96295
+ effects.consume(code);
96296
+ effects.exit('magicBlockLineEnding');
96297
+ return beforeData; // Stay in beforeData state - don't enter magicBlockData yet
96298
+ }
96299
+ function captureData(code) {
96300
+ // EOF - magic block must be closed
96301
+ if (code === null) {
96302
+ effects.exit('magicBlockData');
96303
+ effects.exit('magicBlock');
96304
+ return nok(code);
96305
+ }
96306
+ // At line ending, check if we can continue on the next line
96307
+ if (markdownLineEnding(code)) {
96308
+ effects.exit('magicBlockData');
96309
+ return effects.check(syntax_nonLazyContinuation, continuationOk, after)(code);
96310
+ }
96311
+ if (jsonState.escapeNext) {
96312
+ jsonState.escapeNext = false;
96313
+ effects.consume(code);
96314
+ return captureData;
96315
+ }
96316
+ if (jsonState.inString) {
96317
+ if (code === codes.backslash) {
96318
+ jsonState.escapeNext = true;
96319
+ effects.consume(code);
96320
+ return captureData;
96321
+ }
96322
+ if (code === codes.quotationMark) {
96323
+ jsonState.inString = false;
96324
+ }
96325
+ effects.consume(code);
96326
+ return captureData;
96327
+ }
96328
+ if (code === codes.quotationMark) {
96329
+ jsonState.inString = true;
96330
+ effects.consume(code);
96331
+ return captureData;
96332
+ }
96333
+ if (code === codes.leftSquareBracket) {
96334
+ effects.exit('magicBlockData');
96335
+ effects.enter('magicBlockMarkerEnd');
96336
+ effects.consume(code);
96337
+ return expectSlash;
96338
+ }
96339
+ effects.consume(code);
96340
+ return captureData;
96341
+ }
96342
+ /**
96343
+ * Called when non-lazy continuation check passes - we can continue parsing.
96344
+ */
96345
+ function continuationOk(code) {
96346
+ // Consume the line ending
96347
+ effects.enter('magicBlockLineEnding');
96348
+ effects.consume(code);
96349
+ effects.exit('magicBlockLineEnding');
96350
+ return continuationStart;
96351
+ }
96352
+ /**
96353
+ * Start of continuation line - check for more line endings, closing marker, or start capturing data.
96354
+ */
96355
+ function continuationStart(code) {
96356
+ // Handle consecutive line endings
96357
+ if (code === null) {
96358
+ effects.exit('magicBlock');
96359
+ return nok(code);
96360
+ }
96361
+ if (markdownLineEnding(code)) {
96362
+ return effects.check(syntax_nonLazyContinuation, continuationOkBeforeData, after)(code);
96363
+ }
96364
+ // Check if this is the start of the closing marker [/block]
96365
+ // If so, handle it directly without entering magicBlockData
96366
+ if (code === codes.leftSquareBracket) {
96367
+ effects.enter('magicBlockMarkerEnd');
96368
+ effects.consume(code);
96369
+ return expectSlashFromContinuation;
96370
+ }
96371
+ effects.enter('magicBlockData');
96372
+ return captureData(code);
96373
+ }
96374
+ /**
96375
+ * Check for closing marker slash when coming from continuation.
96376
+ * If not a closing marker, create an empty data token and continue.
96377
+ */
96378
+ function expectSlashFromContinuation(code) {
96379
+ if (code === null) {
96380
+ effects.exit('magicBlockMarkerEnd');
96381
+ effects.exit('magicBlock');
96382
+ return nok(code);
96383
+ }
96384
+ if (markdownLineEnding(code)) {
96385
+ effects.exit('magicBlockMarkerEnd');
96386
+ return effects.check(syntax_nonLazyContinuation, continuationOkBeforeData, after)(code);
96387
+ }
96388
+ if (code === codes.slash) {
96389
+ effects.consume(code);
96390
+ return expectClosingB;
96391
+ }
96392
+ // Not a closing marker - this is data content
96393
+ // Exit marker and enter data
96394
+ effects.exit('magicBlockMarkerEnd');
96395
+ effects.enter('magicBlockData');
96396
+ // The [ was consumed by the marker, so we need to conceptually "have it" in our data
96397
+ // But since we already consumed it into the marker, we need a different approach
96398
+ // Re-classify: consume this character as data and continue
96399
+ if (code === codes.quotationMark)
96400
+ jsonState.inString = true;
96401
+ effects.consume(code);
96402
+ return captureData;
96403
+ }
96404
+ function expectSlash(code) {
96405
+ if (code === null) {
96406
+ effects.exit('magicBlockMarkerEnd');
96407
+ effects.exit('magicBlock');
96408
+ return nok(code);
96409
+ }
96410
+ // At line ending during marker parsing
96411
+ if (markdownLineEnding(code)) {
96412
+ effects.exit('magicBlockMarkerEnd');
96413
+ return effects.check(syntax_nonLazyContinuation, continuationOk, after)(code);
96414
+ }
96415
+ if (code !== codes.slash) {
96416
+ effects.exit('magicBlockMarkerEnd');
96417
+ effects.enter('magicBlockData');
96418
+ if (code === codes.quotationMark)
96419
+ jsonState.inString = true;
96420
+ effects.consume(code);
96421
+ return captureData;
96422
+ }
96423
+ effects.consume(code);
96424
+ return expectClosingB;
96425
+ }
96426
+ function expectClosingB(code) {
96427
+ if (code === null) {
96428
+ effects.exit('magicBlockMarkerEnd');
96429
+ effects.exit('magicBlock');
96430
+ return nok(code);
96431
+ }
96432
+ if (code !== codes.lowercaseB) {
96433
+ effects.exit('magicBlockMarkerEnd');
96434
+ effects.enter('magicBlockData');
96435
+ if (code === codes.quotationMark)
96436
+ jsonState.inString = true;
96437
+ effects.consume(code);
96438
+ return captureData;
96439
+ }
96440
+ effects.consume(code);
96441
+ return expectClosingL;
96442
+ }
96443
+ function expectClosingL(code) {
96444
+ if (code === null) {
96445
+ effects.exit('magicBlockMarkerEnd');
96446
+ effects.exit('magicBlock');
96447
+ return nok(code);
96448
+ }
96449
+ if (code !== codes.lowercaseL) {
96450
+ effects.exit('magicBlockMarkerEnd');
96451
+ effects.enter('magicBlockData');
96452
+ if (code === codes.quotationMark)
96453
+ jsonState.inString = true;
96454
+ effects.consume(code);
96455
+ return captureData;
96456
+ }
96457
+ effects.consume(code);
96458
+ return expectClosingO;
96459
+ }
96460
+ function expectClosingO(code) {
96461
+ if (code === null) {
96462
+ effects.exit('magicBlockMarkerEnd');
96463
+ effects.exit('magicBlock');
96464
+ return nok(code);
96465
+ }
96466
+ if (code !== codes.lowercaseO) {
96467
+ effects.exit('magicBlockMarkerEnd');
96468
+ effects.enter('magicBlockData');
96469
+ if (code === codes.quotationMark)
96470
+ jsonState.inString = true;
96471
+ effects.consume(code);
96472
+ return captureData;
96473
+ }
96474
+ effects.consume(code);
96475
+ return expectClosingC;
96476
+ }
96477
+ function expectClosingC(code) {
96478
+ if (code === null) {
96479
+ effects.exit('magicBlockMarkerEnd');
96480
+ effects.exit('magicBlock');
96481
+ return nok(code);
96482
+ }
96483
+ if (code !== codes.lowercaseC) {
96484
+ effects.exit('magicBlockMarkerEnd');
96485
+ effects.enter('magicBlockData');
96486
+ if (code === codes.quotationMark)
96487
+ jsonState.inString = true;
96488
+ effects.consume(code);
96489
+ return captureData;
96490
+ }
96491
+ effects.consume(code);
96492
+ return expectClosingK;
96493
+ }
96494
+ function expectClosingK(code) {
96495
+ if (code === null) {
96496
+ effects.exit('magicBlockMarkerEnd');
96497
+ effects.exit('magicBlock');
96498
+ return nok(code);
96499
+ }
96500
+ if (code !== codes.lowercaseK) {
96501
+ effects.exit('magicBlockMarkerEnd');
96502
+ effects.enter('magicBlockData');
96503
+ if (code === codes.quotationMark)
96504
+ jsonState.inString = true;
96505
+ effects.consume(code);
96506
+ return captureData;
96507
+ }
96508
+ effects.consume(code);
96509
+ return expectClosingBracket;
96510
+ }
96511
+ function expectClosingBracket(code) {
96512
+ if (code === null) {
96513
+ effects.exit('magicBlockMarkerEnd');
96514
+ effects.exit('magicBlock');
96515
+ return nok(code);
96516
+ }
96517
+ if (code !== codes.rightSquareBracket) {
96518
+ effects.exit('magicBlockMarkerEnd');
96519
+ effects.enter('magicBlockData');
96520
+ if (code === codes.quotationMark)
96521
+ jsonState.inString = true;
96522
+ effects.consume(code);
96523
+ return captureData;
96524
+ }
96525
+ effects.consume(code);
96526
+ effects.exit('magicBlockMarkerEnd');
96527
+ // Check for trailing whitespace on the same line
96528
+ return consumeTrailing;
96529
+ }
96530
+ /**
96531
+ * Consume trailing whitespace (spaces/tabs) on the same line after [/block].
96532
+ * For concrete flow constructs, we must end at eol/eof or fail.
96533
+ * Trailing whitespace is consumed; other content causes nok (text tokenizer handles it).
96534
+ */
96535
+ function consumeTrailing(code) {
96536
+ // End of file - done
96537
+ if (code === null) {
96538
+ effects.exit('magicBlock');
96539
+ return ok(code);
96540
+ }
96541
+ // Line ending - done
96542
+ if (markdownLineEnding(code)) {
96543
+ effects.exit('magicBlock');
96544
+ return ok(code);
96545
+ }
96546
+ // Space or tab - consume as trailing whitespace
96547
+ if (code === codes.space || code === codes.horizontalTab) {
96548
+ effects.enter('magicBlockTrailing');
96549
+ effects.consume(code);
96550
+ return consumeTrailingContinue;
96551
+ }
96552
+ // Any other character - fail flow tokenizer, let text tokenizer handle it
96553
+ effects.exit('magicBlock');
96554
+ return nok(code);
96555
+ }
96556
+ /**
96557
+ * Continue consuming trailing whitespace.
96558
+ */
96559
+ function consumeTrailingContinue(code) {
96560
+ // End of file - done
96561
+ if (code === null) {
96562
+ effects.exit('magicBlockTrailing');
96563
+ effects.exit('magicBlock');
96564
+ return ok(code);
96565
+ }
96566
+ // Line ending - done
96567
+ if (markdownLineEnding(code)) {
96568
+ effects.exit('magicBlockTrailing');
96569
+ effects.exit('magicBlock');
96570
+ return ok(code);
96571
+ }
96572
+ // More space or tab - keep consuming
96573
+ if (code === codes.space || code === codes.horizontalTab) {
96574
+ effects.consume(code);
96575
+ return consumeTrailingContinue;
96576
+ }
96577
+ // Non-whitespace after whitespace - fail flow tokenizer
96578
+ effects.exit('magicBlockTrailing');
96579
+ effects.exit('magicBlock');
96580
+ return nok(code);
96581
+ }
96582
+ /**
96583
+ * Called when we can't continue (lazy line or EOF) - exit the construct.
96584
+ */
96585
+ function after(code) {
96586
+ effects.exit('magicBlock');
96587
+ return nok(code);
96588
+ }
96589
+ }
96590
+ /**
96591
+ * Text tokenizer for single-line magic blocks only.
96592
+ * Used in inline contexts like list items and paragraphs.
96593
+ * Multiline blocks are handled by the flow tokenizer.
96594
+ */
96595
+ function tokenizeMagicBlockText(effects, ok, nok) {
96596
+ // State for tracking JSON content
96597
+ const jsonState = { escapeNext: false, inString: false };
96598
+ const blockTypeRef = { value: '' };
96599
+ let seenOpenBrace = false;
96600
+ // Create shared parsers for opening marker and type capture
96601
+ const typeParser = createTypeCaptureParser(effects, nok, beforeData, blockTypeRef);
96602
+ const openingMarkerParser = createOpeningMarkerParser(effects, nok, typeParser.first);
96603
+ // Success handler for closing marker - exits tokens and returns ok
96604
+ const closingSuccess = (code) => {
96605
+ effects.exit('magicBlockMarkerEnd');
96606
+ effects.exit('magicBlock');
96607
+ return ok(code);
96608
+ };
96609
+ // Mismatch handler - falls back to data capture
96610
+ const closingMismatch = (code) => {
96611
+ effects.exit('magicBlockMarkerEnd');
96612
+ effects.enter('magicBlockData');
96613
+ if (code === codes.quotationMark)
96614
+ jsonState.inString = true;
96615
+ effects.consume(code);
96616
+ return captureData;
96617
+ };
96618
+ // EOF handler
96619
+ const closingEof = (_code) => nok(_code);
96620
+ // Create closing marker parsers
96621
+ const closingFromBeforeData = createClosingMarkerParser(effects, closingSuccess, closingMismatch, closingEof, jsonState);
96622
+ const closingFromData = createClosingMarkerParser(effects, closingSuccess, closingMismatch, closingEof, jsonState);
96623
+ return start;
96624
+ function start(code) {
96625
+ if (code !== codes.leftSquareBracket)
96626
+ return nok(code);
96627
+ effects.enter('magicBlock');
96628
+ effects.enter('magicBlockMarkerStart');
96629
+ effects.consume(code);
96630
+ return openingMarkerParser;
96631
+ }
96632
+ /**
96633
+ * State before data content - handles line endings before entering data token.
96634
+ * Whitespace before '{' is allowed.
96635
+ */
96636
+ function beforeData(code) {
96637
+ // Fail on EOF - magic block must be closed
96638
+ if (code === null) {
96639
+ return nok(code);
96640
+ }
96641
+ // Handle line endings before any data - consume them without entering data token
96642
+ if (markdownLineEnding(code)) {
96643
+ effects.enter('magicBlockLineEnding');
96644
+ effects.consume(code);
96645
+ effects.exit('magicBlockLineEnding');
96646
+ return beforeData;
96647
+ }
96648
+ // Check for closing marker directly (without entering data)
96649
+ if (code === codes.leftSquareBracket) {
96650
+ effects.enter('magicBlockMarkerEnd');
96651
+ effects.consume(code);
96652
+ return closingFromBeforeData.expectSlash;
96653
+ }
96654
+ // If we've already seen the opening brace, just continue capturing data
96655
+ if (seenOpenBrace) {
96656
+ effects.enter('magicBlockData');
96657
+ return captureData(code);
96658
+ }
96659
+ // Skip whitespace (spaces/tabs) before the data
96660
+ if (code === codes.space || code === codes.horizontalTab) {
96661
+ return beforeDataWhitespace(code);
96662
+ }
96663
+ // Data must start with '{' for valid JSON
96664
+ if (code !== codes.leftCurlyBrace) {
96665
+ return nok(code);
96666
+ }
96667
+ // We have '{' - enter data token and start capturing
96668
+ seenOpenBrace = true;
96669
+ effects.enter('magicBlockData');
96670
+ return captureData(code);
96671
+ }
96672
+ /**
96673
+ * Consume whitespace before the data.
96674
+ */
96675
+ function beforeDataWhitespace(code) {
96676
+ if (code === null) {
96677
+ return nok(code);
96678
+ }
96679
+ if (markdownLineEnding(code)) {
96680
+ effects.enter('magicBlockLineEnding');
96681
+ effects.consume(code);
96682
+ effects.exit('magicBlockLineEnding');
96683
+ return beforeData;
96684
+ }
96685
+ if (code === codes.space || code === codes.horizontalTab) {
96686
+ effects.enter('magicBlockData');
96687
+ effects.consume(code);
96688
+ return beforeDataWhitespaceContinue;
96689
+ }
96690
+ if (code === codes.leftCurlyBrace) {
96691
+ seenOpenBrace = true;
96692
+ effects.enter('magicBlockData');
96693
+ return captureData(code);
96694
+ }
96695
+ return nok(code);
96696
+ }
96697
+ /**
96698
+ * Continue consuming whitespace or validate the opening brace.
96699
+ */
96700
+ function beforeDataWhitespaceContinue(code) {
96701
+ if (code === null) {
96702
+ effects.exit('magicBlockData');
96703
+ return nok(code);
96704
+ }
96705
+ if (markdownLineEnding(code)) {
96706
+ effects.exit('magicBlockData');
96707
+ effects.enter('magicBlockLineEnding');
96708
+ effects.consume(code);
96709
+ effects.exit('magicBlockLineEnding');
96710
+ return beforeData;
96711
+ }
96712
+ if (code === codes.space || code === codes.horizontalTab) {
96713
+ effects.consume(code);
96714
+ return beforeDataWhitespaceContinue;
96715
+ }
96716
+ if (code === codes.leftCurlyBrace) {
96717
+ seenOpenBrace = true;
96718
+ return captureData(code);
96719
+ }
96720
+ effects.exit('magicBlockData');
96721
+ return nok(code);
96722
+ }
96723
+ function captureData(code) {
96724
+ // Fail on EOF - magic block must be closed
96725
+ if (code === null) {
96726
+ effects.exit('magicBlockData');
96727
+ return nok(code);
96728
+ }
96729
+ // Handle multiline magic blocks within text/paragraphs
96730
+ // Exit data, consume line ending with proper token, then re-enter data
96731
+ if (markdownLineEnding(code)) {
96732
+ effects.exit('magicBlockData');
96733
+ effects.enter('magicBlockLineEnding');
96734
+ effects.consume(code);
96735
+ effects.exit('magicBlockLineEnding');
96736
+ return beforeData; // Go back to beforeData to handle potential empty lines
96737
+ }
96738
+ if (jsonState.escapeNext) {
96739
+ jsonState.escapeNext = false;
96740
+ effects.consume(code);
96741
+ return captureData;
96742
+ }
96743
+ if (jsonState.inString) {
96744
+ if (code === codes.backslash) {
96745
+ jsonState.escapeNext = true;
96746
+ effects.consume(code);
96747
+ return captureData;
96748
+ }
96749
+ if (code === codes.quotationMark) {
96750
+ jsonState.inString = false;
96751
+ }
96752
+ effects.consume(code);
96753
+ return captureData;
96754
+ }
96755
+ if (code === codes.quotationMark) {
96756
+ jsonState.inString = true;
96757
+ effects.consume(code);
96758
+ return captureData;
96759
+ }
96760
+ if (code === codes.leftSquareBracket) {
96761
+ effects.exit('magicBlockData');
96762
+ effects.enter('magicBlockMarkerEnd');
96763
+ effects.consume(code);
96764
+ return closingFromData.expectSlash;
96765
+ }
96766
+ effects.consume(code);
96767
+ return captureData;
96768
+ }
96769
+ }
96770
+ /* harmony default export */ const syntax = ((/* unused pure expression or super */ null && (magicBlock)));
96771
+
96772
+ ;// ./lib/micromark/magic-block/index.ts
96773
+ /**
96774
+ * Micromark extension for magic block syntax.
96775
+ *
96776
+ * Usage:
96777
+ * ```ts
96778
+ * import { magicBlock } from './lib/micromark/magic-block';
96779
+ *
96780
+ * const processor = unified()
96781
+ * .data('micromarkExtensions', [magicBlock()])
96782
+ * ```
96783
+ */
96784
+
95098
96785
 
95099
96786
  ;// ./lib/utils/mdxish/mdxish-load-components.ts
95100
96787
 
@@ -95161,46 +96848,56 @@ function loadComponents() {
95161
96848
 
95162
96849
 
95163
96850
 
96851
+
96852
+
95164
96853
 
95165
96854
 
95166
96855
 
95167
96856
  const defaultTransformers = [callouts, code_tabs, gemoji_, transform_embeds];
95168
96857
  function mdxishAstProcessor(mdContent, opts = {}) {
95169
- const { components: userComponents = {}, jsxContext = {}, useTailwind } = opts;
96858
+ const { components: userComponents = {}, jsxContext = {}, newEditorTypes = false, safeMode = false, useTailwind, } = opts;
95170
96859
  const components = {
95171
96860
  ...loadComponents(),
95172
96861
  ...userComponents,
95173
96862
  };
96863
+ // Build set of known component names for snake_case filtering
96864
+ const knownComponents = new Set(Object.keys(components));
95174
96865
  // Preprocessing pipeline: Transform content to be parser-ready
95175
- // Step 1: Extract legacy magic blocks
95176
- const { replaced: contentAfterMagicBlocks, blocks } = extractMagicBlocks(mdContent);
95177
- // Step 2: Normalize malformed table separator syntax (e.g., `|: ---` → `| :---`)
95178
- const contentAfterTableNormalization = normalizeTableSeparator(contentAfterMagicBlocks);
95179
- // Step 3: Evaluate JSX expressions in attributes
95180
- const contentAfterJSXEvaluation = preprocessJSXExpressions(contentAfterTableNormalization, jsxContext);
95181
- // Step 4: Replace snake_case component names with parser-safe placeholders
95182
- // (e.g., <Snake_case /> <MDXishSnakeCase0 /> which will be restored after parsing)
95183
- const { content: parserReadyContent, mapping: snakeCaseMapping } = processSnakeCaseComponent(contentAfterJSXEvaluation);
96866
+ // Step 1: Normalize malformed table separator syntax (e.g., `|: ---` → `| :---`)
96867
+ const contentAfterTableNormalization = normalizeTableSeparator(mdContent);
96868
+ // Step 2: Evaluate JSX expressions in attributes
96869
+ const contentAfterJSXEvaluation = safeMode
96870
+ ? contentAfterTableNormalization
96871
+ : preprocessJSXExpressions(contentAfterTableNormalization, jsxContext);
96872
+ // Step 3: Replace snake_case component names with parser-safe placeholders
96873
+ const { content: parserReadyContent, mapping: snakeCaseMapping } = processSnakeCaseComponent(contentAfterJSXEvaluation, { knownComponents });
95184
96874
  // Create string map for tailwind transformer
95185
96875
  const tempComponentsMap = Object.entries(components).reduce((acc, [key, value]) => {
95186
96876
  acc[key] = String(value);
95187
96877
  return acc;
95188
96878
  }, {});
96879
+ // Get mdxExpression extension and remove its flow construct to prevent
96880
+ // `{...}` from interrupting paragraphs (which breaks multiline magic blocks)
96881
+ const mdxExprExt = mdxExpression({ allowEmpty: true });
96882
+ const mdxExprTextOnly = {
96883
+ text: mdxExprExt.text,
96884
+ };
95189
96885
  const processor = unified()
95190
- .data('micromarkExtensions', [mdxExpression({ allowEmpty: true })]) // Parse inline JSX expressions as AST nodes for later evaluation
95191
- .data('fromMarkdownExtensions', [mdxExpressionFromMarkdown()])
96886
+ .data('micromarkExtensions', safeMode ? [magicBlock()] : [magicBlock(), mdxExprTextOnly])
96887
+ .data('fromMarkdownExtensions', safeMode ? [magicBlockFromMarkdown()] : [magicBlockFromMarkdown(), mdxExpressionFromMarkdown()])
95192
96888
  .use(remarkParse)
95193
96889
  .use(remarkFrontmatter)
95194
96890
  .use(normalize_malformed_md_syntax)
95195
- .use(mdxish_magic_blocks, { blocks })
96891
+ .use(magic_block_transformer)
95196
96892
  .use(transform_images, { isMdxish: true })
95197
96893
  .use(defaultTransformers)
95198
96894
  .use(mdxish_component_blocks)
95199
96895
  .use(restore_snake_case_component_name, { mapping: snakeCaseMapping })
95200
96896
  .use(mdxish_tables)
95201
96897
  .use(mdxish_html_blocks)
95202
- .use(evaluate_expressions, { context: jsxContext }) // Evaluate MDX expressions using jsxContext
95203
- .use(variables_text) // Parse {user.*} patterns from text (can't rely on remarkMdx)
96898
+ .use(newEditorTypes ? mdxish_jsx_to_mdast : undefined) // Convert JSX elements to MDAST types
96899
+ .use(safeMode ? undefined : evaluate_expressions, { context: jsxContext }) // Evaluate MDX expressions using jsxContext
96900
+ .use(variables_text) // Parse {user.*} patterns from text
95204
96901
  .use(useTailwind ? transform_tailwind : undefined, { components: tempComponentsMap })
95205
96902
  .use(remarkGfm);
95206
96903
  return {
@@ -95217,10 +96914,14 @@ function mdxishAstProcessor(mdContent, opts = {}) {
95217
96914
  * Converts an Mdast to a Markdown string.
95218
96915
  */
95219
96916
  function mdxishMdastToMd(mdast) {
95220
- const md = unified().use(remarkGfm).use(processor_compile).use(remarkStringify, {
96917
+ const md = unified()
96918
+ .use(remarkGfm)
96919
+ .use(mdxishCompilers)
96920
+ .use(remarkStringify, {
95221
96921
  bullet: '-',
95222
96922
  emphasis: '_',
95223
- }).stringify(mdast);
96923
+ })
96924
+ .stringify(mdast);
95224
96925
  return md;
95225
96926
  }
95226
96927
  /**
@@ -95716,11 +97417,14 @@ const tags = (doc) => {
95716
97417
 
95717
97418
 
95718
97419
 
97420
+
95719
97421
  const mdxishTags_tags = (doc) => {
95720
- const { replaced: sanitizedDoc } = extractMagicBlocks(doc);
95721
97422
  const set = new Set();
95722
- const processor = remark().use(mdxish_component_blocks);
95723
- const tree = processor.parse(sanitizedDoc);
97423
+ const processor = remark()
97424
+ .data('micromarkExtensions', [magicBlock()])
97425
+ .data('fromMarkdownExtensions', [magicBlockFromMarkdown()])
97426
+ .use(mdxish_component_blocks);
97427
+ const tree = processor.parse(doc);
95724
97428
  visit(processor.runSync(tree), isMDXElement, (node) => {
95725
97429
  if (node.name?.match(/^[A-Z]/)) {
95726
97430
  set.add(node.name);
@@ -95741,12 +97445,15 @@ const mdxishTags_tags = (doc) => {
95741
97445
 
95742
97446
 
95743
97447
 
97448
+
95744
97449
  /**
95745
97450
  * Removes Markdown and MDX comments.
95746
97451
  */
95747
97452
  async function stripComments(doc, { mdx, mdxish } = {}) {
95748
- const { replaced, blocks } = extractMagicBlocks(doc);
95749
- const processor = unified();
97453
+ const processor = unified()
97454
+ .data('micromarkExtensions', [magicBlock()])
97455
+ .data('fromMarkdownExtensions', [magicBlockFromMarkdown()])
97456
+ .data('toMarkdownExtensions', [magicBlockToMarkdown()]);
95750
97457
  // we still require these two extensions because:
95751
97458
  // 1. we can rely on remarkMdx to parse MDXish
95752
97459
  // 2. we need to parse JSX comments into mdxTextExpression nodes so that the transformers can pick them up
@@ -95788,10 +97495,8 @@ async function stripComments(doc, { mdx, mdxish } = {}) {
95788
97495
  },
95789
97496
  ],
95790
97497
  });
95791
- const file = await processor.process(replaced);
95792
- const stringified = String(file).trim();
95793
- const restored = restoreMagicBlocks(stringified, blocks);
95794
- return restored;
97498
+ const file = await processor.process(doc);
97499
+ return String(file).trim();
95795
97500
  }
95796
97501
  /* harmony default export */ const lib_stripComments = (stripComments);
95797
97502