@readme/markdown 13.8.4 → 13.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.node.js CHANGED
@@ -19232,7 +19232,8 @@ const Callout = (props) => {
19232
19232
  const children = external_react_.Children.toArray(props.children);
19233
19233
  const icon = props.icon;
19234
19234
  const isEmoji = icon && emoji_regex().test(icon);
19235
- const heading = empty ? external_react_.createElement("p", { className: 'callout-heading empty' }) : children[0];
19235
+ const hasBody = children.length > 1;
19236
+ const heading = empty ? (hasBody ? external_react_.createElement("p", { className: 'callout-heading empty' }) : null) : children[0];
19236
19237
  const theme = props.theme || (icon && themes[icon]) || 'default';
19237
19238
  return (
19238
19239
  // @ts-expect-error -- theme is not a valid attribute
@@ -24714,8 +24715,9 @@ function useScrollHighlight(navRef) {
24714
24715
  const nav = navRef.current;
24715
24716
  if (!nav)
24716
24717
  return;
24717
- const key = Array.from(nav.querySelectorAll('a[href^="#"]'))
24718
- .map(a => a.getAttribute('href'))
24718
+ const key = Array.from(nav.querySelectorAll('a'))
24719
+ .map(a => a.hash)
24720
+ .filter(Boolean)
24719
24721
  .join('\0');
24720
24722
  setTocKey(key);
24721
24723
  });
@@ -24737,9 +24739,11 @@ function useScrollHighlight(navRef) {
24737
24739
  const scrollParent = TableOfContents_getScrollParent(headings[0]);
24738
24740
  const isAtBottom = () => {
24739
24741
  if (scrollParent instanceof Window) {
24740
- return window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - SCROLL_BOTTOM_TOLERANCE;
24742
+ return document.documentElement.scrollHeight > window.innerHeight
24743
+ && window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - SCROLL_BOTTOM_TOLERANCE;
24741
24744
  }
24742
- return scrollParent.scrollTop + scrollParent.clientHeight >= scrollParent.scrollHeight - SCROLL_BOTTOM_TOLERANCE;
24745
+ return scrollParent.scrollHeight > scrollParent.clientHeight
24746
+ && scrollParent.scrollTop + scrollParent.clientHeight >= scrollParent.scrollHeight - SCROLL_BOTTOM_TOLERANCE;
24743
24747
  };
24744
24748
  const activate = (id) => {
24745
24749
  if (id === activeId)
@@ -24789,15 +24793,12 @@ function useScrollHighlight(navRef) {
24789
24793
  const onClick = (e) => {
24790
24794
  if (!(e.target instanceof Element))
24791
24795
  return;
24792
- const anchor = e.target.closest('a[href^="#"]');
24793
- if (!(anchor instanceof HTMLAnchorElement))
24796
+ const anchor = e.target.closest('a');
24797
+ if (!(anchor instanceof HTMLAnchorElement) || !anchor.hash)
24794
24798
  return;
24795
24799
  const id = decodeURIComponent(anchor.hash.slice(1));
24796
24800
  if (!linkMap.has(id))
24797
24801
  return;
24798
- if (window.location.hash !== anchor.hash) {
24799
- window.location.hash = anchor.hash;
24800
- }
24801
24802
  activate(id);
24802
24803
  clickLocked = true;
24803
24804
  let unlockTimer = null;
@@ -73340,267 +73341,1078 @@ const plain = (node, opts = {}) => {
73340
73341
  };
73341
73342
  /* harmony default export */ const lib_plain = (plain);
73342
73343
 
73343
- ;// ./processor/compile/gemoji.ts
73344
- const gemoji = (node) => `:${node.name}:`;
73345
- /* harmony default export */ const compile_gemoji = (gemoji);
73344
+ ;// ./lib/mdast.ts
73346
73345
 
73347
- ;// ./processor/compile/variable.ts
73348
- const variable = (node) => `{user.${node.data?.hProperties?.name || ''}}`;
73349
- /* harmony default export */ const compile_variable = (variable);
73346
+ const mdast = (text, opts = {}) => {
73347
+ const processor = ast_processor(opts);
73348
+ const tree = processor.parse(text);
73349
+ return processor.runSync(tree);
73350
+ };
73351
+ /* harmony default export */ const lib_mdast = (mdast);
73350
73352
 
73351
- ;// ./processor/transform/extract-text.ts
73353
+ ;// ./lib/utils/mdxish/protect-code-blocks.ts
73352
73354
  /**
73353
- * Extracts text content from a single AST node recursively.
73354
- * Works with both MDAST and HAST-like node structures.
73355
- *
73356
- * Placed this outside of the utils.ts file to avoid circular dependencies.
73355
+ * Replaces code blocks and inline code with placeholders to protect them from preprocessing.
73357
73356
  *
73358
- * @param node - The node to extract text from (can be MDAST Node or HAST-like structure)
73359
- * @returns The concatenated text content
73357
+ * @param content - The markdown content to process
73358
+ * @returns Object containing protected content and arrays of original code blocks
73359
+ * @example
73360
+ * ```typescript
73361
+ * const input = 'Text with `inline code` and ```fenced block```';
73362
+ * protectCodeBlocks(input)
73363
+ * // Returns: {
73364
+ * // protectedCode: {
73365
+ * // codeBlocks: ['```fenced block```'],
73366
+ * // inlineCode: ['`inline code`']
73367
+ * // },
73368
+ * // protectedContent: 'Text with ___INLINE_CODE_0___ and ___CODE_BLOCK_0___'
73369
+ * // }
73370
+ * ```
73360
73371
  */
73361
- const extractText = (node) => {
73362
- if ((node.type === 'text' || node.type === 'html') && typeof node.value === 'string') {
73363
- return node.value;
73364
- }
73365
- // When a blockquote contains only an image (no text), treat it as having content
73366
- // so the blockquote is no longer treated as empty and preserved correctly.
73367
- if (node.type === 'image') {
73368
- return typeof node.alt === 'string' && node.alt ? node.alt : '[image]';
73369
- }
73370
- if (node.children && Array.isArray(node.children)) {
73371
- return node.children
73372
- .map(child => {
73373
- if (child && typeof child === 'object' && 'type' in child) {
73374
- return extractText(child);
73375
- }
73376
- return '';
73377
- })
73378
- .join('');
73372
+ function protectCodeBlocks(content) {
73373
+ const codeBlocks = [];
73374
+ const inlineCode = [];
73375
+ let protectedContent = '';
73376
+ let remaining = content;
73377
+ let codeBlockStart = remaining.indexOf('```');
73378
+ while (codeBlockStart !== -1) {
73379
+ protectedContent += remaining.slice(0, codeBlockStart);
73380
+ remaining = remaining.slice(codeBlockStart);
73381
+ const codeBlockEnd = remaining.indexOf('```', 3);
73382
+ if (codeBlockEnd === -1) {
73383
+ break;
73384
+ }
73385
+ const match = remaining.slice(0, codeBlockEnd + 3);
73386
+ const index = codeBlocks.length;
73387
+ codeBlocks.push(match);
73388
+ protectedContent += `___CODE_BLOCK_${index}___`;
73389
+ remaining = remaining.slice(codeBlockEnd + 3);
73390
+ codeBlockStart = remaining.indexOf('```');
73379
73391
  }
73380
- return '';
73381
- };
73382
-
73383
- ;// ./processor/transform/callouts.ts
73384
-
73385
-
73386
-
73387
-
73388
-
73389
-
73390
-
73391
-
73392
-
73393
-
73394
-
73395
-
73396
-
73397
-
73398
- const titleParser = unified().use(remarkParse).use(remarkGfm);
73399
- // The title paragraph may contain custom AST nodes that `toMarkdown` doesn't
73400
- // natively understand
73401
- const toMarkdownExtensions = [
73402
- gfmToMarkdown(),
73403
- // For mdx variable syntaxes (e.g., {user.name})
73404
- mdxExpressionToMarkdown(),
73405
- // Important: This is required and would crash the parser if there's no variable and gemoji node handler
73406
- { handlers: { [NodeTypes.variable]: compile_variable, [NodeTypes.emoji]: compile_gemoji } },
73407
- ];
73408
- const callouts_regex = `^(${emoji_regex().source}|⚠)(\\s+|$)`;
73409
- const findFirst = (node) => {
73410
- if ('children' in node)
73411
- return findFirst(node.children[0]);
73412
- if (node.type === 'text')
73413
- return node;
73414
- return null;
73415
- };
73416
- const wrapHeading = (node) => {
73417
- const firstChild = node.children[0];
73418
- return {
73419
- type: 'heading',
73420
- depth: 3,
73421
- children: ('children' in firstChild ? firstChild.children : []),
73422
- position: {
73423
- start: firstChild.position.start,
73424
- end: firstChild.position.end,
73425
- },
73426
- };
73427
- };
73392
+ protectedContent += remaining;
73393
+ protectedContent = protectedContent.replace(/`([^`\n]+)`/g, match => {
73394
+ const index = inlineCode.length;
73395
+ inlineCode.push(match);
73396
+ return `___INLINE_CODE_${index}___`;
73397
+ });
73398
+ return { protectedCode: { codeBlocks, inlineCode }, protectedContent };
73399
+ }
73428
73400
  /**
73429
- * Checks if a blockquote matches the expected callout structure:
73430
- * blockquote > paragraph > text node
73401
+ * Restores inline code by replacing placeholders with original content.
73402
+ *
73403
+ * @param content - Content with inline code placeholders
73404
+ * @param protectedCode - The protected code arrays
73405
+ * @returns Content with inline code restored
73431
73406
  */
73432
- const isCalloutStructure = (node) => {
73433
- const firstChild = node.children?.[0];
73434
- if (!firstChild || firstChild.type !== 'paragraph')
73435
- return false;
73436
- if (!('children' in firstChild))
73437
- return false;
73438
- const firstTextChild = firstChild.children?.[0];
73439
- return firstTextChild?.type === 'text';
73440
- };
73407
+ function restoreInlineCode(content, protectedCode) {
73408
+ return content.replace(/___INLINE_CODE_(\d+)___/g, (_m, idx) => {
73409
+ return protectedCode.inlineCode[parseInt(idx, 10)];
73410
+ });
73411
+ }
73441
73412
  /**
73442
- * Finds the first text node containing a newline in a paragraph's children.
73443
- * Returns the index and the newline position within that text node.
73413
+ * Restores fenced code blocks by replacing placeholders with original content.
73414
+ *
73415
+ * @param content - Content with code block placeholders
73416
+ * @param protectedCode - The protected code arrays
73417
+ * @returns Content with code blocks restored
73444
73418
  */
73445
- const findNewlineInParagraph = (paragraph) => {
73446
- for (let i = 0; i < paragraph.children.length; i += 1) {
73447
- const child = paragraph.children[i];
73448
- if (child.type === 'text' && typeof child.value === 'string') {
73449
- const newlineIndex = child.value.indexOf('\n');
73450
- if (newlineIndex !== -1) {
73451
- return { index: i, newlineIndex };
73452
- }
73453
- }
73454
- }
73455
- return null;
73456
- };
73419
+ function restoreFencedCodeBlocks(content, protectedCode) {
73420
+ return content.replace(/___CODE_BLOCK_(\d+)___/g, (_m, idx) => {
73421
+ return protectedCode.codeBlocks[parseInt(idx, 10)];
73422
+ });
73423
+ }
73457
73424
  /**
73458
- * Splits a paragraph at the first newline, separating heading content (before \n)
73459
- * from body content (after \n). Mutates the paragraph to contain only heading children.
73425
+ * Restores all code blocks and inline code by replacing placeholders with original content.
73426
+ *
73427
+ * @param content - Content with code placeholders
73428
+ * @param protectedCode - The protected code arrays
73429
+ * @returns Content with all code blocks and inline code restored
73430
+ * @example
73431
+ * ```typescript
73432
+ * const content = 'Text with ___INLINE_CODE_0___ and ___CODE_BLOCK_0___';
73433
+ * const protectedCode = {
73434
+ * codeBlocks: ['```js\ncode\n```'],
73435
+ * inlineCode: ['`inline`']
73436
+ * };
73437
+ * restoreCodeBlocks(content, protectedCode)
73438
+ * // Returns: 'Text with `inline` and ```js\ncode\n```'
73439
+ * ```
73460
73440
  */
73461
- const splitParagraphAtNewline = (paragraph) => {
73462
- const splitPoint = findNewlineInParagraph(paragraph);
73463
- if (!splitPoint)
73464
- return null;
73465
- const { index, newlineIndex } = splitPoint;
73466
- const originalChildren = paragraph.children;
73467
- const textNode = originalChildren[index];
73468
- const beforeNewline = textNode.value.slice(0, newlineIndex);
73469
- const afterNewline = textNode.value.slice(newlineIndex + 1);
73470
- // Split paragraph: heading = children[0..index-1] + text before newline
73471
- const headingChildren = originalChildren.slice(0, index);
73472
- if (beforeNewline.length > 0 || headingChildren.length === 0) {
73473
- headingChildren.push({ type: 'text', value: beforeNewline });
73441
+ function restoreCodeBlocks(content, protectedCode) {
73442
+ let restored = restoreFencedCodeBlocks(content, protectedCode);
73443
+ restored = restoreInlineCode(restored, protectedCode);
73444
+ return restored;
73445
+ }
73446
+
73447
+ ;// ./processor/transform/mdxish/preprocess-jsx-expressions.ts
73448
+
73449
+ // Base64 encode (Node.js + browser compatible)
73450
+ function base64Encode(str) {
73451
+ if (typeof Buffer !== 'undefined') {
73452
+ return Buffer.from(str, 'utf-8').toString('base64');
73474
73453
  }
73475
- paragraph.children = headingChildren;
73476
- // Body = text after newline + remaining children from original array
73477
- const bodyChildren = [];
73478
- if (afterNewline.length > 0) {
73479
- bodyChildren.push({ type: 'text', value: afterNewline });
73454
+ return btoa(unescape(encodeURIComponent(str)));
73455
+ }
73456
+ // Base64 decode (Node.js + browser compatible)
73457
+ function base64Decode(str) {
73458
+ if (typeof Buffer !== 'undefined') {
73459
+ return Buffer.from(str, 'base64').toString('utf-8');
73480
73460
  }
73481
- bodyChildren.push(...originalChildren.slice(index + 1));
73482
- return bodyChildren.length > 0 ? bodyChildren : null;
73483
- };
73461
+ return decodeURIComponent(escape(atob(str)));
73462
+ }
73463
+ function escapeHtmlAttribute(value) {
73464
+ return value
73465
+ .replace(/&/g, '&amp;')
73466
+ .replace(/"/g, '&quot;')
73467
+ .replace(/</g, '&lt;')
73468
+ .replace(/>/g, '&gt;')
73469
+ .replace(/\n/g, '&#10;');
73470
+ }
73471
+ // Marker prefix for JSON-serialized complex values (arrays/objects)
73472
+ // Using a prefix that won't conflict with regular string values
73473
+ const JSON_VALUE_MARKER = '__MDXISH_JSON__';
73474
+ // Markers for protected HTMLBlock content (HTML comments avoid markdown parsing issues)
73475
+ const HTML_BLOCK_CONTENT_START = '<!--RDMX_HTMLBLOCK:';
73476
+ const HTML_BLOCK_CONTENT_END = ':RDMX_HTMLBLOCK-->';
73484
73477
  /**
73485
- * Removes the icon/match prefix from the first text node in a paragraph.
73486
- * This is needed to clean up the raw AST after we've extracted the icon.
73478
+ * Evaluates a JavaScript expression using context variables.
73479
+ *
73480
+ * @param expression
73481
+ * @param context
73482
+ * @returns The evaluated result
73483
+ * @example
73484
+ * ```typescript
73485
+ * const context = { baseUrl: 'https://example.com', path: '/api' };
73486
+ * evaluateExpression('baseUrl + path', context)
73487
+ * // Returns: 'https://example.com/api'
73488
+ * ```
73487
73489
  */
73488
- const removeIconPrefix = (paragraph, prefixLength) => {
73489
- const firstTextNode = findFirst(paragraph);
73490
- if (firstTextNode && 'value' in firstTextNode && typeof firstTextNode.value === 'string') {
73491
- firstTextNode.value = firstTextNode.value.slice(prefixLength);
73492
- }
73493
- };
73494
- const processBlockquote = (node, index, parent, isMdxish = false) => {
73495
- if (!isCalloutStructure(node)) {
73496
- // Only stringify empty blockquotes (no extractable text content)
73497
- // Preserve blockquotes with actual content (e.g., headings, lists, etc.)
73498
- const content = extractText(node);
73499
- const isEmpty = !content || content.trim() === '';
73500
- if (isEmpty && index !== undefined && parent) {
73501
- const textNode = {
73502
- type: 'text',
73503
- value: '>',
73504
- };
73505
- const paragraphNode = {
73506
- type: 'paragraph',
73507
- children: [textNode],
73508
- position: node.position,
73509
- };
73510
- parent.children.splice(index, 1, paragraphNode);
73511
- }
73512
- return;
73490
+ function evaluateExpression(expression, context) {
73491
+ const contextKeys = Object.keys(context);
73492
+ const contextValues = Object.values(context);
73493
+ // eslint-disable-next-line no-new-func
73494
+ const func = new Function(...contextKeys, `return ${expression}`);
73495
+ return func(...contextValues);
73496
+ }
73497
+ /**
73498
+ * Base64 encodes HTMLBlock template literal content to prevent markdown parser from consuming <script>/<style> tags.
73499
+ *
73500
+ * @param content
73501
+ * @returns Content with HTMLBlock template literals base64 encoded in HTML comments
73502
+ * @example
73503
+ * ```typescript
73504
+ * const input = '<HTMLBlock>{`<script>alert("xss")</script>`}</HTMLBlock>';
73505
+ * protectHTMLBlockContent(input)
73506
+ * // Returns: '<HTMLBlock><!--RDMX_HTMLBLOCK:PHNjcmlwdD5hbGVydCgieHNzIik8L3NjcmlwdD4=:RDMX_HTMLBLOCK--></HTMLBlock>'
73507
+ * ```
73508
+ */
73509
+ function protectHTMLBlockContent(content) {
73510
+ return content.replace(/(<HTMLBlock[^>]*>)\{\s*`((?:[^`\\]|\\.)*)`\s*\}(<\/HTMLBlock>)/g, (_match, openTag, templateContent, closeTag) => {
73511
+ const encoded = base64Encode(templateContent);
73512
+ return `${openTag}${HTML_BLOCK_CONTENT_START}${encoded}${HTML_BLOCK_CONTENT_END}${closeTag}`;
73513
+ });
73514
+ }
73515
+ /**
73516
+ * Removes JSX-style comments (e.g., { /* comment *\/ }) from content.
73517
+ *
73518
+ * @param content
73519
+ * @returns Content with JSX comments removed
73520
+ * @example
73521
+ * ```typescript
73522
+ * removeJSXComments('Text { /* comment *\/ } more text')
73523
+ * // Returns: 'Text more text'
73524
+ * ```
73525
+ */
73526
+ function removeJSXComments(content) {
73527
+ return content.replace(/\{\s*\/\*[^*]*(?:\*(?!\/)[^*]*)*\*\/\s*\}/g, '');
73528
+ }
73529
+ /**
73530
+ * Extracts content between balanced braces, handling nested braces.
73531
+ *
73532
+ * @param content
73533
+ * @param start
73534
+ * @returns Object with extracted content and end position, or null if braces are unbalanced
73535
+ * @example
73536
+ * ```typescript
73537
+ * const input = 'foo{bar{baz}qux}end';
73538
+ * extractBalancedBraces(input, 3) // start at position 3 (after '{')
73539
+ * // Returns: { content: 'bar{baz}qux', end: 16 }
73540
+ * ```
73541
+ */
73542
+ function extractBalancedBraces(content, start) {
73543
+ let depth = 1;
73544
+ let pos = start;
73545
+ while (pos < content.length && depth > 0) {
73546
+ const char = content[pos];
73547
+ if (char === '{')
73548
+ depth += 1;
73549
+ else if (char === '}')
73550
+ depth -= 1;
73551
+ pos += 1;
73513
73552
  }
73514
- // isCalloutStructure ensures node.children[0] is a Paragraph with children
73515
- const firstParagraph = node.children[0];
73516
- const startText = lib_plain(firstParagraph).toString();
73517
- const [match, icon] = startText.match(callouts_regex) || [];
73518
- const firstParagraphOriginalEnd = firstParagraph.position.end;
73519
- if (icon && match) {
73520
- // Handle cases where heading and body are on the same line separated by a newline.
73521
- // Example: "> ⚠️ **Bold heading**\nBody text here"
73522
- const bodyChildren = splitParagraphAtNewline(firstParagraph);
73523
- const didSplit = bodyChildren !== null;
73524
- removeIconPrefix(firstParagraph, match.length);
73525
- const firstText = findFirst(firstParagraph);
73526
- const rawValue = firstText?.value ?? '';
73527
- const hasContent = rawValue.trim().length > 0 || firstParagraph.children.length > 1;
73528
- const empty = !hasContent;
73529
- const theme = themes[icon] || 'default';
73530
- if (hasContent || didSplit) {
73531
- const headingMatch = rawValue.match(/^(#{1,6})\s*/);
73532
- // # heading syntax is handled via direct AST manipulation so we can
73533
- // set the depth while preserving the original inline children (bold, etc.)
73534
- if (headingMatch) {
73535
- firstText.value = rawValue.slice(headingMatch[0].length);
73536
- const heading = wrapHeading(node);
73537
- heading.depth = headingMatch[1].length;
73538
- node.children[0] = heading;
73539
- node.children[0].position.start.offset += match.length;
73540
- node.children[0].position.start.column += match.length;
73553
+ if (depth !== 0)
73554
+ return null;
73555
+ return { content: content.slice(start, pos - 1), end: pos };
73556
+ }
73557
+ /**
73558
+ * Escapes problematic braces in content to prevent MDX expression parsing errors.
73559
+ * Handles three cases:
73560
+ * 1. Unbalanced braces (e.g., `{foo` without closing `}`)
73561
+ * 2. Paragraph-spanning expressions (e.g., `{\n\n}` where blank line splits paragraphs)
73562
+ * 3. Skips HTML elements to prevent backslashes appearing in output
73563
+ *
73564
+ */
73565
+ function escapeProblematicBraces(content) {
73566
+ // Skip HTML elements their content should never be escaped because
73567
+ // rehypeRaw parses them into hast elements, making `\` literal text in output
73568
+ const htmlElements = [];
73569
+ const safe = content.replace(/<([a-z][a-zA-Z0-9]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>/g, match => {
73570
+ const idx = htmlElements.length;
73571
+ htmlElements.push(match);
73572
+ return `___HTML_ELEM_${idx}___`;
73573
+ });
73574
+ const toEscape = new Set();
73575
+ // Convert to array of Unicode code points to handle emojis and multi-byte characters correctly
73576
+ const chars = Array.from(safe);
73577
+ let strDelim = null;
73578
+ let strEscaped = false;
73579
+ // Stack of open braces with their state
73580
+ const openStack = [];
73581
+ // Track position of last newline (outside strings) to detect blank lines
73582
+ let lastNewlinePos = -2; // -2 means no recent newline
73583
+ for (let i = 0; i < chars.length; i += 1) {
73584
+ const ch = chars[i];
73585
+ // Track string delimiters inside expressions to ignore braces within them
73586
+ if (openStack.length > 0) {
73587
+ if (strDelim) {
73588
+ if (strEscaped)
73589
+ strEscaped = false;
73590
+ else if (ch === '\\')
73591
+ strEscaped = true;
73592
+ else if (ch === strDelim)
73593
+ strDelim = null;
73594
+ // eslint-disable-next-line no-continue
73595
+ continue;
73541
73596
  }
73542
- else if (isMdxish) {
73543
- // Block-level title re-parsing is only needed for MDXish where HTML stays
73544
- // as raw nodes. In MDX, remarkMdx has already converted HTML to JSX AST
73545
- // nodes which toMarkdown can't serialize — and MDX doesn't need this
73546
- // block-level title handling anyway.
73547
- const headingText = toMarkdown({ type: 'root', children: [firstParagraph] }, {
73548
- extensions: toMarkdownExtensions,
73549
- })
73550
- .trim()
73551
- .replace(/^\\(?=[>#+\-*])/, '');
73552
- const parsedTitle = titleParser.parse(headingText);
73553
- const parsedFirstChild = parsedTitle.children[0];
73554
- // Block-level syntax ("> quote", "- list") produces non-paragraph nodes;
73555
- // inline text parses as a paragraph and falls through to wrapHeading().
73556
- if (parsedFirstChild && parsedFirstChild.type !== 'paragraph') {
73557
- // Strip positions from re-parsed nodes since they're relative to the heading text, not the original source
73558
- visit(parsedTitle, (n) => {
73559
- delete n.position;
73560
- });
73561
- const heading = wrapHeading(node);
73562
- heading.children = parsedTitle.children;
73563
- delete heading.position;
73564
- node.children[0] = heading;
73597
+ if (ch === '"' || ch === "'" || ch === '`') {
73598
+ strDelim = ch;
73599
+ // eslint-disable-next-line no-continue
73600
+ continue;
73601
+ }
73602
+ // Track newlines to detect blank lines (paragraph boundaries)
73603
+ if (ch === '\n') {
73604
+ // Check if this newline creates a blank line (only whitespace since last newline)
73605
+ if (lastNewlinePos >= 0) {
73606
+ const between = chars.slice(lastNewlinePos + 1, i).join('');
73607
+ if (/^[ \t]*$/.test(between)) {
73608
+ // This is a blank line - mark all open expressions as paragraph-spanning
73609
+ openStack.forEach(entry => {
73610
+ entry.hasBlankLine = true;
73611
+ });
73612
+ }
73565
73613
  }
73566
- else {
73567
- node.children[0] = wrapHeading(node);
73568
- node.children[0].position.start.offset += match.length;
73569
- node.children[0].position.start.column += match.length;
73614
+ lastNewlinePos = i;
73615
+ }
73616
+ }
73617
+ // Skip already-escaped braces (count preceding backslashes)
73618
+ if (ch === '{' || ch === '}') {
73619
+ let bs = 0;
73620
+ for (let j = i - 1; j >= 0 && chars[j] === '\\'; j -= 1)
73621
+ bs += 1;
73622
+ if (bs % 2 === 1) {
73623
+ // eslint-disable-next-line no-continue
73624
+ continue;
73625
+ }
73626
+ }
73627
+ if (ch === '{') {
73628
+ openStack.push({ pos: i, hasBlankLine: false });
73629
+ lastNewlinePos = -2; // Reset newline tracking for new expression
73630
+ }
73631
+ else if (ch === '}') {
73632
+ if (openStack.length > 0) {
73633
+ const entry = openStack.pop();
73634
+ // If expression spans paragraph boundary, escape both braces
73635
+ if (entry.hasBlankLine) {
73636
+ toEscape.add(entry.pos);
73637
+ toEscape.add(i);
73570
73638
  }
73571
73639
  }
73572
73640
  else {
73573
- node.children[0] = wrapHeading(node);
73574
- node.children[0].position.start.offset += match.length;
73575
- node.children[0].position.start.column += match.length;
73641
+ // Unbalanced closing brace (no matching open)
73642
+ toEscape.add(i);
73576
73643
  }
73577
73644
  }
73578
- // Insert body content as a separate paragraph after the heading
73579
- if (bodyChildren) {
73580
- const headingPosition = node.children[0].position;
73581
- node.children.splice(1, 0, {
73582
- type: 'paragraph',
73583
- children: bodyChildren,
73584
- ...(headingPosition && firstParagraphOriginalEnd
73585
- ? {
73586
- position: {
73587
- start: headingPosition.end,
73588
- end: firstParagraphOriginalEnd,
73589
- },
73590
- }
73591
- : {}),
73592
- });
73593
- }
73594
- Object.assign(node, {
73595
- type: NodeTypes.callout,
73596
- data: {
73597
- hName: 'Callout',
73598
- hProperties: {
73599
- icon,
73600
- ...(empty && { empty }),
73601
- theme,
73602
- },
73603
- },
73645
+ }
73646
+ // Any remaining open braces are unbalanced
73647
+ openStack.forEach(entry => toEscape.add(entry.pos));
73648
+ // If there are no problematic braces, return safe content as-is;
73649
+ // otherwise, escape each problematic `{` or `}` so MDX doesn't treat them as expressions.
73650
+ let result = toEscape.size === 0
73651
+ ? safe
73652
+ : chars.map((ch, i) => (toEscape.has(i) ? `\\${ch}` : ch)).join('');
73653
+ // Restore HTML elements
73654
+ if (htmlElements.length > 0) {
73655
+ result = result.replace(/___HTML_ELEM_(\d+)___/g, (_m, idx) => htmlElements[parseInt(idx, 10)]);
73656
+ }
73657
+ return result;
73658
+ }
73659
+ /**
73660
+ * Converts JSX attribute expressions (attribute={expression}) to HTML attributes (attribute="value").
73661
+ * Handles style objects (camelCase → kebab-case), className → class, and JSON stringifies objects.
73662
+ *
73663
+ * @param content
73664
+ * @param context
73665
+ * @returns Content with attribute expressions evaluated and converted to HTML attributes
73666
+ * @example
73667
+ * ```typescript
73668
+ * const context = { baseUrl: 'https://example.com' };
73669
+ * const input = '<a href={baseUrl}>Link</a>';
73670
+ * evaluateAttributeExpressions(input, context)
73671
+ * // Returns: '<a href="https://example.com">Link</a>'
73672
+ * ```
73673
+ */
73674
+ function evaluateAttributeExpressions(content, context, protectedCode) {
73675
+ const attrStartRegex = /(\w+)=\{/g;
73676
+ let result = '';
73677
+ let lastEnd = 0;
73678
+ let match = attrStartRegex.exec(content);
73679
+ while (match !== null) {
73680
+ const attributeName = match[1];
73681
+ const braceStart = match.index + match[0].length;
73682
+ const extracted = extractBalancedBraces(content, braceStart);
73683
+ if (extracted) {
73684
+ // The expression might contain template literals in MDX component tag props
73685
+ // E.g. <Component greeting={`Hello World!`} />
73686
+ // that is marked as inline code. So we need to restore the inline codes
73687
+ // in the expression to evaluate it
73688
+ let expression = extracted.content;
73689
+ if (protectedCode) {
73690
+ expression = restoreInlineCode(expression, protectedCode);
73691
+ }
73692
+ const fullMatchEnd = extracted.end;
73693
+ result += content.slice(lastEnd, match.index);
73694
+ try {
73695
+ const evalResult = evaluateExpression(expression, context);
73696
+ if (typeof evalResult === 'object' && evalResult !== null) {
73697
+ if (attributeName === 'style') {
73698
+ const cssString = Object.entries(evalResult)
73699
+ .map(([key, value]) => {
73700
+ const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
73701
+ return `${cssKey}: ${value}`;
73702
+ })
73703
+ .join('; ');
73704
+ result += `style="${cssString}"`;
73705
+ }
73706
+ else {
73707
+ // These are arrays / objects attribute values
73708
+ // Mark JSON-serialized values with a prefix so they can be parsed back correctly
73709
+ const jsonValue = escapeHtmlAttribute(JSON_VALUE_MARKER + JSON.stringify(evalResult));
73710
+ // Use double quotes so that multi-paragraph values are not split into multiple attributes by the processors
73711
+ result += `${attributeName}="${jsonValue}"`;
73712
+ }
73713
+ }
73714
+ else if (attributeName === 'className') {
73715
+ // Escape special characters so that it doesn't break and split the attribute value to nodes
73716
+ // This will be restored later in the pipeline
73717
+ result += `class="${escapeHtmlAttribute(String(evalResult))}"`;
73718
+ }
73719
+ else {
73720
+ result += `${attributeName}="${escapeHtmlAttribute(String(evalResult))}"`;
73721
+ }
73722
+ }
73723
+ catch (_error) {
73724
+ result += content.slice(match.index, fullMatchEnd);
73725
+ }
73726
+ lastEnd = fullMatchEnd;
73727
+ attrStartRegex.lastIndex = fullMatchEnd;
73728
+ }
73729
+ match = attrStartRegex.exec(content);
73730
+ }
73731
+ result += content.slice(lastEnd);
73732
+ return result;
73733
+ }
73734
+ /**
73735
+ * Preprocesses JSX-like expressions in markdown before parsing.
73736
+ * Inline expressions are handled separately; attribute expressions are processed here.
73737
+ *
73738
+ * @param content
73739
+ * @param context
73740
+ * @returns Preprocessed content ready for markdown parsing
73741
+ */
73742
+ function preprocessJSXExpressions(content, context = {}) {
73743
+ // Step 0: Base64 encode HTMLBlock content
73744
+ let processed = protectHTMLBlockContent(content);
73745
+ // Step 1: Protect code blocks and inline code
73746
+ const { protectedCode, protectedContent } = protectCodeBlocks(processed);
73747
+ // Step 2: Evaluate attribute expressions (JSX attribute syntax: href={baseUrl})
73748
+ // For inline expressions, we use a library to parse the expression & evaluate it later
73749
+ // For attribute expressions, it was difficult to use a library to parse them, so do it manually
73750
+ processed = evaluateAttributeExpressions(protectedContent, context, protectedCode);
73751
+ // Step 3: Escape problematic braces to prevent MDX expression parsing errors
73752
+ // This handles both unbalanced braces and paragraph-spanning expressions in one pass
73753
+ processed = escapeProblematicBraces(processed);
73754
+ // Step 4: Restore protected code blocks
73755
+ processed = restoreCodeBlocks(processed, protectedCode);
73756
+ return processed;
73757
+ }
73758
+
73759
+ ;// ./processor/utils.ts
73760
+
73761
+
73762
+
73763
+ /**
73764
+ * Formats the hProperties of a node as a string, so they can be compiled back into JSX/MDX.
73765
+ * This currently sets all the values to a string since we process/compile the MDX on the fly
73766
+ * through the editor, and it'll throw errors over malformed JSX. TODO: fix this.
73767
+ *
73768
+ * @template T
73769
+ * @param {Node} node
73770
+ * @returns {string} formatted hProperties as JSX attributes
73771
+ */
73772
+ const formatHProps = (node) => {
73773
+ const hProps = getHProps(node);
73774
+ return formatProps(hProps);
73775
+ };
73776
+ /**
73777
+ * Formats an object of props as a string.
73778
+ *
73779
+ * @param {Object} props
73780
+ * @returns {string}
73781
+ */
73782
+ const formatProps = (props) => {
73783
+ const keys = Object.keys(props);
73784
+ return keys.map(key => `${key}="${props[key]}"`).join(' ');
73785
+ };
73786
+ /**
73787
+ * Returns the hProperties of a node.
73788
+ *
73789
+ * @template T
73790
+ * @param {Node} node
73791
+ * @returns {T} hProperties
73792
+ */
73793
+ const getHProps = (node) => {
73794
+ const hProps = node.data?.hProperties || {};
73795
+ return hProps;
73796
+ };
73797
+ /**
73798
+ * Returns array of hProperty keys.
73799
+ *
73800
+ * @template T
73801
+ * @param {Node} node
73802
+ * @returns {Array} array of hProperty keys
73803
+ */
73804
+ const getHPropKeys = (node) => {
73805
+ const hProps = getHProps(node);
73806
+ return Object.keys(hProps) || [];
73807
+ };
73808
+ /**
73809
+ * Gets the attributes of an MDX element and returns them as an object of hProperties.
73810
+ *
73811
+ * @template T
73812
+ * @param {(MdxJsxFlowElement | MdxJsxTextElement)} jsx
73813
+ * @returns {T} object of hProperties
73814
+ */
73815
+ const getAttrs = (jsx) => jsx.attributes.reduce((memo, attr) => {
73816
+ if ('name' in attr) {
73817
+ if (typeof attr.value === 'string') {
73818
+ if (attr.value.startsWith(JSON_VALUE_MARKER)) {
73819
+ try {
73820
+ memo[attr.name] = JSON.parse(attr.value.slice(JSON_VALUE_MARKER.length));
73821
+ }
73822
+ catch {
73823
+ memo[attr.name] = attr.value;
73824
+ }
73825
+ }
73826
+ else {
73827
+ memo[attr.name] = attr.value;
73828
+ }
73829
+ }
73830
+ else if (attr.value === null) {
73831
+ memo[attr.name] = true;
73832
+ }
73833
+ else if (attr.value.value !== 'undefined') {
73834
+ memo[attr.name] = JSON.parse(attr.value.value);
73835
+ }
73836
+ }
73837
+ return memo;
73838
+ }, {});
73839
+ /**
73840
+ * Gets the children of an MDX element and returns them as an array of Text nodes.
73841
+ * Currently only being used by the HTML Block component, which only expects a single text node.
73842
+ *
73843
+ * @template T
73844
+ * @param {(MdxJsxFlowElement | MdxJsxTextElement)} jsx
73845
+ * @returns {Array} array of child text nodes
73846
+ */
73847
+ const utils_getChildren = (jsx) => jsx.children.reduce((memo, child, i) => {
73848
+ memo[i] = {
73849
+ type: 'text',
73850
+ value: child.value,
73851
+ position: child.position,
73852
+ };
73853
+ return memo;
73854
+ }, []);
73855
+ /**
73856
+ * Tests if a node is an MDX element.
73857
+ * TODO: Make this more extensible to all types of nodes. isElement(node, 'type' or ['type1', 'type2']), say
73858
+ *
73859
+ * @param {Node} node
73860
+ * @returns {(node is MdxJsxFlowElement | MdxJsxTextElement | MdxjsEsm)}
73861
+ */
73862
+ const isMDXElement = (node) => {
73863
+ return ['mdxJsxFlowElement', 'mdxJsxTextElement'].includes(node.type);
73864
+ };
73865
+ /**
73866
+ * Tests if a node is an MDX ESM element (i.e. import or export).
73867
+ *
73868
+ * @param {Node} node
73869
+ * @returns {boolean}
73870
+ */
73871
+ const isMDXEsm = (node) => {
73872
+ return node.type === 'mdxjsEsm';
73873
+ };
73874
+ /**
73875
+ * Takes an HTML string and formats it for display in the editor. Removes leading/trailing newlines
73876
+ * and unindents the HTML.
73877
+ *
73878
+ * @param {string} html - HTML content from template literal
73879
+ * @returns {string} processed HTML
73880
+ */
73881
+ function formatHtmlForMdxish(html) {
73882
+ // Remove leading/trailing backticks if present, since they're used to keep the HTML
73883
+ // from being parsed prematurely
73884
+ let processed = html;
73885
+ if (processed.startsWith('`') && processed.endsWith('`')) {
73886
+ processed = processed.slice(1, -1);
73887
+ }
73888
+ // Removes the leading/trailing newlines
73889
+ let cleaned = processed.replace(/^\s*\n|\n\s*$/g, '');
73890
+ // Convert literal \n sequences to actual newlines BEFORE processing backticks
73891
+ // This prevents the backtick unescaping regex from incorrectly matching \n sequences
73892
+ cleaned = cleaned.replace(/\\n/g, '\n');
73893
+ // Unescape backticks: \` -> ` (users escape backticks in template literals)
73894
+ // Handle both cases: \` (adjacent) and \ followed by ` (split by markdown parser)
73895
+ cleaned = cleaned.replace(/\\`/g, '`');
73896
+ // Also handle case where backslash and backtick got separated by markdown parsing
73897
+ // Pattern: backslash followed by any characters (but not \n which we already handled), then a backtick
73898
+ // This handles cases like: \example` -> `example` (replacing \ with ` at start)
73899
+ // Exclude \n sequences to avoid matching them incorrectly
73900
+ cleaned = cleaned.replace(/\\([^`\\n]*?)`/g, '`$1`');
73901
+ // Fix case where markdown parser consumed one backtick from triple backticks
73902
+ // Pattern: `` followed by a word (like ``javascript) should be ```javascript
73903
+ // This handles cases where code fences were parsed and one backtick was lost
73904
+ cleaned = cleaned.replace(/<(\w+[^>]*)>``(\w+)/g, '<$1>```$2');
73905
+ // Unescape dollar signs: \$ -> $ (users escape $ in template literals to prevent interpolation)
73906
+ cleaned = cleaned.replace(/\\\$/g, '$');
73907
+ return cleaned;
73908
+ }
73909
+ /**
73910
+ * Takes an HTML string and formats it for display in the editor. Removes leading/trailing newlines
73911
+ * and unindents the HTML.
73912
+ *
73913
+ * @param {string} html
73914
+ * @returns {string} formatted HTML
73915
+ */
73916
+ const formatHTML = (html) => {
73917
+ // Remove leading/trailing backticks if present, since they're used to keep the HTML
73918
+ // from being parsed prematurely
73919
+ if (html.startsWith('`') && html.endsWith('`')) {
73920
+ // eslint-disable-next-line no-param-reassign
73921
+ html = html.slice(1, -1);
73922
+ }
73923
+ // Removes the leading/trailing newlines
73924
+ const cleaned = html.replace(/^\s*\n|\n\s*$/g, '');
73925
+ // // Get the number of spaces in the first line to determine the tab size
73926
+ // const tab = cleaned.match(/^\s*/)[0].length;
73927
+ // // Remove the first indentation level from each line
73928
+ // const tabRegex = new RegExp(`^\\s{${tab}}`, 'gm');
73929
+ // const unindented = cleaned.replace(tabRegex, '');
73930
+ return cleaned;
73931
+ };
73932
+ /**
73933
+ * Reformat HTML for the markdown/mdx by adding an indentation to each line. This assures that the
73934
+ * HTML is indentend properly within the HTMLBlock component when rendered in the markdown/mdx.
73935
+ *
73936
+ * @param {string} html
73937
+ * @param {number} [indent=2]
73938
+ * @returns {string} re-formatted HTML
73939
+ */
73940
+ const reformatHTML = (html) => {
73941
+ // Remove leading/trailing newlines
73942
+ const cleaned = html.replace(/^\s*\n|\n\s*$/g, '').replaceAll(/(?<!\\(\\\\)*)`/g, '\\`');
73943
+ // // Create a tab/indent with the specified number of spaces
73944
+ // const tab = ' '.repeat(indent);
73945
+ // // Indent each line of the HTML (converts to an array, indents each line, then joins back)
73946
+ // const indented = cleaned.split('\n').map((line: string) => `${tab}${line}`).join('\n');
73947
+ return cleaned;
73948
+ };
73949
+ const toAttributes = (object, keys = []) => {
73950
+ const attributes = [];
73951
+ Object.entries(object).forEach(([name, v]) => {
73952
+ if (keys.length > 0 && !keys.includes(name))
73953
+ return;
73954
+ let value;
73955
+ if (typeof v === 'undefined' || v === null || v === '' || v === false) {
73956
+ return;
73957
+ }
73958
+ else if (typeof v === 'string') {
73959
+ value = v;
73960
+ }
73961
+ else {
73962
+ /* values can be null, undefined, string, or a expression, eg:
73963
+ *
73964
+ * ```
73965
+ * <Image src="..." border={false} size={width - 20} />
73966
+ * ```
73967
+ *
73968
+ * Parsing the expression seems to only be done by the library
73969
+ * `mdast-util-mdx-jsx`, and so the most straight forward way to parse
73970
+ * the expression and get the appropriate AST is with our `mdast`
73971
+ * function.
73972
+ */
73973
+ const proxy = lib_mdast(`{${v}}`);
73974
+ const data = proxy.children[0].data;
73975
+ value = {
73976
+ type: 'mdxJsxAttributeValueExpression',
73977
+ value: v.toString(),
73978
+ data,
73979
+ };
73980
+ }
73981
+ attributes.push({
73982
+ type: 'mdxJsxAttribute',
73983
+ name,
73984
+ value,
73985
+ });
73986
+ });
73987
+ return attributes;
73988
+ };
73989
+ /**
73990
+ * Checks if a named export exists in the MDX tree. Accepts either an mdast or
73991
+ * a hast tree.
73992
+ *
73993
+ * example:
73994
+ * ```
73995
+ * const mdx = `export const Foo = 'bar';`
73996
+ *
73997
+ * hasNamedExport(mdast(mdx), 'Foo') => true
73998
+ * ```
73999
+ *
74000
+ */
74001
+ const hasNamedExport = (tree, name) => {
74002
+ let hasExport = false;
74003
+ // eslint-disable-next-line consistent-return
74004
+ visit(tree, 'mdxjsEsm', node => {
74005
+ if ('declaration' in node.data.estree.body[0] &&
74006
+ node.data.estree.body[0].declaration.type === 'VariableDeclaration') {
74007
+ const { declarations } = node.data.estree.body[0].declaration;
74008
+ hasExport = !!declarations.find(declaration => 'name' in declaration.id && declaration.id.name === name);
74009
+ return hasExport ? EXIT : CONTINUE;
74010
+ }
74011
+ });
74012
+ return hasExport;
74013
+ };
74014
+ /* Example mdast structures to find first export name in a mdxjsEsm node:
74015
+ There are three types of export declarations that we need to consider:
74016
+ 1. VARIABLE DECLARATION
74017
+ "type": "mdxjsEsm",
74018
+ "value": "export const Foo = () => <div>Hello world</div>\nexport const Bar = () => <div>hello darkness my old friend</div>",
74019
+ "data": {
74020
+ "estree": {
74021
+ "type": "Program",
74022
+ "body": [
74023
+ {
74024
+ "type": "ExportNamedDeclaration",
74025
+ "declaration": {
74026
+ "type": "VariableDeclaration",
74027
+ "declarations": [
74028
+ {
74029
+ "type": "VariableDeclarator",
74030
+ "id": {
74031
+ "type": "Identifier",
74032
+ "name": "Foo" // --------> This is the export name
74033
+ },
74034
+ ...
74035
+
74036
+ 2/3. FUNCTION DECLARATION & CLASS DECLARATION
74037
+ "estree": {
74038
+ "type": "Program",
74039
+ "body": [
74040
+ {
74041
+ "type": "ExportNamedDeclaration",
74042
+ "declaration": {
74043
+ "type": "ClassDeclaration" | "FunctionDeclaration",
74044
+ "id": {
74045
+ "type": "Identifier",
74046
+ "name": "Foo" // --------> This is the export name
74047
+ },
74048
+ */
74049
+ const getExports = (tree) => {
74050
+ const set = new Set();
74051
+ visit(tree, isMDXEsm, (node) => {
74052
+ // Once inside an mdxjsEsm node, we need to check for one or more declared exports within data.estree.body
74053
+ // This is because single newlines \n are not considered as a new block, so there may be more than one export statement in a single mdxjsEsm node
74054
+ const body = node.data?.estree.body;
74055
+ if (!body)
74056
+ return;
74057
+ body.forEach(child => {
74058
+ if (child.type === 'ExportNamedDeclaration') {
74059
+ // There are three types of ExportNamedDeclaration that we need to consider: VariableDeclaration, FunctionDeclaration, ClassDeclaration
74060
+ const declaration = child.declaration;
74061
+ // FunctionDeclaration and ClassDeclaration have the same structure
74062
+ if (declaration.type !== 'VariableDeclaration') {
74063
+ // Note: declaration.id.type is always 'Identifier' for FunctionDeclarations and ClassDeclarations
74064
+ set.add(declaration.id.name);
74065
+ }
74066
+ else {
74067
+ declaration.declarations.forEach(dec => {
74068
+ const id = dec.id;
74069
+ if (id.type === 'Identifier') {
74070
+ set.add(id.name);
74071
+ }
74072
+ });
74073
+ }
74074
+ }
74075
+ });
74076
+ });
74077
+ return Array.from(set);
74078
+ };
74079
+
74080
+ ;// ./processor/compile/compatibility.ts
74081
+
74082
+
74083
+
74084
+ /*
74085
+ * Converts a (remark < v9) html node to a JSX string.
74086
+ *
74087
+ * First we replace html comments with the JSX equivalent. Then, we parse that
74088
+ * as html, and serialize it back as xml!
74089
+ *
74090
+ */
74091
+ const compileHtml = (node) => {
74092
+ const string = node.value.replaceAll(/<!--(.*)-->/gms, '{/*$1*/}');
74093
+ return string;
74094
+ };
74095
+ const figureToImageBlock = (node) => {
74096
+ const { align, border, width, src, url, alt, title, ...image } = node.children.find((child) => child.type === 'image');
74097
+ const { className } = image.data.hProperties;
74098
+ const figcaption = node.children.find((child) => child.type === 'figcaption');
74099
+ const caption = figcaption ? toMarkdown(figcaption.children).trim() : null;
74100
+ const attributes = {
74101
+ ...(align && { align }),
74102
+ ...(alt && { alt }),
74103
+ ...(className && { border: className === 'border' }),
74104
+ ...(border && { border }),
74105
+ ...(caption && { caption }),
74106
+ ...(title && { title }),
74107
+ ...(width && { width }),
74108
+ src: src || url,
74109
+ };
74110
+ return `<Image ${formatProps(attributes)} />`;
74111
+ };
74112
+ const embedToEmbedBlock = (node) => {
74113
+ const { html, ...embed } = node.data.hProperties;
74114
+ const attributes = {
74115
+ ...embed,
74116
+ ...(html && { html: encodeURIComponent(html) }),
74117
+ };
74118
+ return `<Embed ${formatProps(attributes)} />`;
74119
+ };
74120
+ const compatibility = (node) => {
74121
+ switch (node.type) {
74122
+ case NodeTypes.glossary: {
74123
+ // Glossary terms will no longer be serialized as special node types in the Editor but we want to ensure that we compile historical
74124
+ // data correctly
74125
+ const term = node.data?.hProperties?.term || node.children[0].value;
74126
+ return `<Glossary>${term}</Glossary>`;
74127
+ }
74128
+ case NodeTypes.reusableContent:
74129
+ return `<${node.tag} />`;
74130
+ case 'html':
74131
+ return compileHtml(node);
74132
+ case 'escape':
74133
+ return `\\${node.value}`;
74134
+ case 'figure':
74135
+ return figureToImageBlock(node);
74136
+ case 'embed':
74137
+ return embedToEmbedBlock(node);
74138
+ case 'i':
74139
+ return `:${node.data.hProperties.className[1]}:`;
74140
+ case 'yaml':
74141
+ return `---\n${node.value}\n---`;
74142
+ default:
74143
+ throw new Error('Unhandled node type!');
74144
+ }
74145
+ };
74146
+ /* harmony default export */ const compile_compatibility = (compatibility);
74147
+
74148
+ ;// ./processor/compile/gemoji.ts
74149
+ const gemoji = (node) => `:${node.name}:`;
74150
+ /* harmony default export */ const compile_gemoji = (gemoji);
74151
+
74152
+ ;// ./processor/compile/variable.ts
74153
+ const variable = (node) => `{user.${node.data?.hProperties?.name || ''}}`;
74154
+ /* harmony default export */ const compile_variable = (variable);
74155
+
74156
+ ;// ./processor/transform/extract-text.ts
74157
+ /**
74158
+ * Extracts text content from a single AST node recursively.
74159
+ * Works with both MDAST and HAST-like node structures.
74160
+ *
74161
+ * Placed this outside of the utils.ts file to avoid circular dependencies.
74162
+ *
74163
+ * @param node - The node to extract text from (can be MDAST Node or HAST-like structure)
74164
+ * @returns The concatenated text content
74165
+ */
74166
+ const extractText = (node) => {
74167
+ if (typeof node.value === 'string') {
74168
+ return node.value;
74169
+ }
74170
+ // When a blockquote contains only an image (no text), treat it as having content
74171
+ // so the blockquote is no longer treated as empty and preserved correctly.
74172
+ if (node.type === 'image') {
74173
+ return typeof node.alt === 'string' && node.alt ? node.alt : '[image]';
74174
+ }
74175
+ if (node.children && Array.isArray(node.children)) {
74176
+ return node.children
74177
+ .map(child => {
74178
+ if (child && typeof child === 'object' && 'type' in child) {
74179
+ return extractText(child);
74180
+ }
74181
+ return '';
74182
+ })
74183
+ .join('');
74184
+ }
74185
+ return '';
74186
+ };
74187
+
74188
+ ;// ./processor/transform/callouts.ts
74189
+
74190
+
74191
+
74192
+
74193
+
74194
+
74195
+
74196
+
74197
+
74198
+
74199
+
74200
+
74201
+
74202
+
74203
+
74204
+ const titleParser = unified().use(remarkParse).use(remarkGfm);
74205
+ // The title paragraph may contain custom AST nodes that `toMarkdown` doesn't
74206
+ // natively understand
74207
+ const toMarkdownExtensions = [
74208
+ gfmToMarkdown(),
74209
+ // For mdx variable syntaxes (e.g., {user.name})
74210
+ mdxExpressionToMarkdown(),
74211
+ // Important: This is required and would crash the parser if there's no variable, gemoji, or fa-icon node handler
74212
+ {
74213
+ handlers: {
74214
+ [NodeTypes.variable]: compile_variable,
74215
+ [NodeTypes.emoji]: compile_gemoji,
74216
+ [NodeTypes.i]: compile_compatibility,
74217
+ },
74218
+ },
74219
+ ];
74220
+ const callouts_regex = `^(${emoji_regex().source}|⚠)(\\s+|$)`;
74221
+ const findFirst = (node) => {
74222
+ if ('children' in node)
74223
+ return findFirst(node.children[0]);
74224
+ if (node.type === 'text')
74225
+ return node;
74226
+ return null;
74227
+ };
74228
+ const wrapHeading = (node) => {
74229
+ const firstChild = node.children[0];
74230
+ return {
74231
+ type: 'heading',
74232
+ depth: 3,
74233
+ children: ('children' in firstChild ? firstChild.children : []),
74234
+ position: {
74235
+ start: firstChild.position.start,
74236
+ end: firstChild.position.end,
74237
+ },
74238
+ };
74239
+ };
74240
+ /**
74241
+ * Checks if a blockquote matches the expected callout structure:
74242
+ * blockquote > paragraph > text node
74243
+ */
74244
+ const isCalloutStructure = (node) => {
74245
+ const firstChild = node.children?.[0];
74246
+ if (!firstChild || firstChild.type !== 'paragraph')
74247
+ return false;
74248
+ if (!('children' in firstChild))
74249
+ return false;
74250
+ const firstTextChild = firstChild.children?.[0];
74251
+ return firstTextChild?.type === 'text';
74252
+ };
74253
+ /**
74254
+ * Finds the first text node containing a newline in a paragraph's children.
74255
+ * Returns the index and the newline position within that text node.
74256
+ */
74257
+ const findNewlineInParagraph = (paragraph) => {
74258
+ for (let i = 0; i < paragraph.children.length; i += 1) {
74259
+ const child = paragraph.children[i];
74260
+ if (child.type === 'text' && typeof child.value === 'string') {
74261
+ const newlineIndex = child.value.indexOf('\n');
74262
+ if (newlineIndex !== -1) {
74263
+ return { index: i, newlineIndex };
74264
+ }
74265
+ }
74266
+ }
74267
+ return null;
74268
+ };
74269
+ /**
74270
+ * Splits a paragraph at the first newline, separating heading content (before \n)
74271
+ * from body content (after \n). Mutates the paragraph to contain only heading children.
74272
+ */
74273
+ const splitParagraphAtNewline = (paragraph) => {
74274
+ const splitPoint = findNewlineInParagraph(paragraph);
74275
+ if (!splitPoint)
74276
+ return null;
74277
+ const { index, newlineIndex } = splitPoint;
74278
+ const originalChildren = paragraph.children;
74279
+ const textNode = originalChildren[index];
74280
+ const beforeNewline = textNode.value.slice(0, newlineIndex);
74281
+ const afterNewline = textNode.value.slice(newlineIndex + 1);
74282
+ // Split paragraph: heading = children[0..index-1] + text before newline
74283
+ const headingChildren = originalChildren.slice(0, index);
74284
+ if (beforeNewline.length > 0 || headingChildren.length === 0) {
74285
+ headingChildren.push({ type: 'text', value: beforeNewline });
74286
+ }
74287
+ paragraph.children = headingChildren;
74288
+ // Body = text after newline + remaining children from original array
74289
+ const bodyChildren = [];
74290
+ if (afterNewline.length > 0) {
74291
+ bodyChildren.push({ type: 'text', value: afterNewline });
74292
+ }
74293
+ bodyChildren.push(...originalChildren.slice(index + 1));
74294
+ return bodyChildren.length > 0 ? bodyChildren : null;
74295
+ };
74296
+ /**
74297
+ * Removes the icon/match prefix from the first text node in a paragraph.
74298
+ * This is needed to clean up the raw AST after we've extracted the icon.
74299
+ */
74300
+ const removeIconPrefix = (paragraph, prefixLength) => {
74301
+ const firstTextNode = findFirst(paragraph);
74302
+ if (firstTextNode && 'value' in firstTextNode && typeof firstTextNode.value === 'string') {
74303
+ firstTextNode.value = firstTextNode.value.slice(prefixLength);
74304
+ }
74305
+ };
74306
+ const processBlockquote = (node, index, parent, isMdxish = false) => {
74307
+ if (!isCalloutStructure(node)) {
74308
+ // Only stringify empty blockquotes (no extractable text content)
74309
+ // Preserve blockquotes with actual content (e.g., headings, lists, etc.)
74310
+ const content = extractText(node);
74311
+ const isEmpty = !content || content.trim() === '';
74312
+ if (isEmpty && index !== undefined && parent) {
74313
+ const textNode = {
74314
+ type: 'text',
74315
+ value: '>',
74316
+ };
74317
+ const paragraphNode = {
74318
+ type: 'paragraph',
74319
+ children: [textNode],
74320
+ position: node.position,
74321
+ };
74322
+ parent.children.splice(index, 1, paragraphNode);
74323
+ }
74324
+ return;
74325
+ }
74326
+ // isCalloutStructure ensures node.children[0] is a Paragraph with children
74327
+ const firstParagraph = node.children[0];
74328
+ const startText = lib_plain(firstParagraph).toString();
74329
+ const [match, icon] = startText.match(callouts_regex) || [];
74330
+ const firstParagraphOriginalEnd = firstParagraph.position.end;
74331
+ if (icon && match) {
74332
+ // Handle cases where heading and body are on the same line separated by a newline.
74333
+ // Example: "> ⚠️ **Bold heading**\nBody text here"
74334
+ const bodyChildren = splitParagraphAtNewline(firstParagraph);
74335
+ const didSplit = bodyChildren !== null;
74336
+ removeIconPrefix(firstParagraph, match.length);
74337
+ const firstText = findFirst(firstParagraph);
74338
+ const rawValue = firstText?.value ?? '';
74339
+ const hasContent = rawValue.trim().length > 0 || firstParagraph.children.length > 1;
74340
+ const empty = !hasContent;
74341
+ const theme = themes[icon] || 'default';
74342
+ if (hasContent || didSplit) {
74343
+ const headingMatch = rawValue.match(/^(#{1,6})\s*/);
74344
+ // # heading syntax is handled via direct AST manipulation so we can
74345
+ // set the depth while preserving the original inline children (bold, etc.)
74346
+ if (headingMatch) {
74347
+ firstText.value = rawValue.slice(headingMatch[0].length);
74348
+ const heading = wrapHeading(node);
74349
+ heading.depth = headingMatch[1].length;
74350
+ node.children[0] = heading;
74351
+ node.children[0].position.start.offset += match.length;
74352
+ node.children[0].position.start.column += match.length;
74353
+ }
74354
+ else if (isMdxish) {
74355
+ // Block-level title re-parsing is only needed for MDXish where HTML stays
74356
+ // as raw nodes. In MDX, remarkMdx has already converted HTML to JSX AST
74357
+ // nodes which toMarkdown can't serialize — and MDX doesn't need this
74358
+ // block-level title handling anyway.
74359
+ const headingText = toMarkdown({ type: 'root', children: [firstParagraph] }, {
74360
+ extensions: toMarkdownExtensions,
74361
+ })
74362
+ .trim()
74363
+ .replace(/^\\(?=[>#+\-*])/, '');
74364
+ const parsedTitle = titleParser.parse(headingText);
74365
+ const parsedFirstChild = parsedTitle.children[0];
74366
+ // Block-level syntax ("> quote", "- list") produces non-paragraph nodes;
74367
+ // inline text parses as a paragraph and falls through to wrapHeading().
74368
+ if (parsedFirstChild && parsedFirstChild.type !== 'paragraph') {
74369
+ // Strip positions from re-parsed nodes since they're relative to the heading text, not the original source
74370
+ visit(parsedTitle, (n) => {
74371
+ delete n.position;
74372
+ });
74373
+ const heading = wrapHeading(node);
74374
+ heading.children = parsedTitle.children;
74375
+ delete heading.position;
74376
+ node.children[0] = heading;
74377
+ }
74378
+ else {
74379
+ node.children[0] = wrapHeading(node);
74380
+ node.children[0].position.start.offset += match.length;
74381
+ node.children[0].position.start.column += match.length;
74382
+ }
74383
+ }
74384
+ else {
74385
+ node.children[0] = wrapHeading(node);
74386
+ node.children[0].position.start.offset += match.length;
74387
+ node.children[0].position.start.column += match.length;
74388
+ }
74389
+ }
74390
+ // Insert body content as a separate paragraph after the heading
74391
+ if (bodyChildren) {
74392
+ const headingPosition = node.children[0].position;
74393
+ node.children.splice(1, 0, {
74394
+ type: 'paragraph',
74395
+ children: bodyChildren,
74396
+ ...(headingPosition && firstParagraphOriginalEnd
74397
+ ? {
74398
+ position: {
74399
+ start: headingPosition.end,
74400
+ end: firstParagraphOriginalEnd,
74401
+ },
74402
+ }
74403
+ : {}),
74404
+ });
74405
+ }
74406
+ Object.assign(node, {
74407
+ type: NodeTypes.callout,
74408
+ data: {
74409
+ hName: 'Callout',
74410
+ hProperties: {
74411
+ icon,
74412
+ ...(empty && { empty }),
74413
+ theme,
74414
+ },
74415
+ },
73604
74416
  });
73605
74417
  }
73606
74418
  };
@@ -90683,857 +91495,121 @@ const emojiToName = {
90683
91495
  '🇾🇪': 'yemen',
90684
91496
  '🇾🇹': 'mayotte',
90685
91497
  '🇿🇦': 'south_africa',
90686
- '🇿🇲': 'zambia',
90687
- '🇿🇼': 'zimbabwe',
90688
- '🏴󠁧󠁢󠁥󠁮󠁧󠁿': 'england',
90689
- '🏴󠁧󠁢󠁳󠁣󠁴󠁿': 'scotland',
90690
- '🏴󠁧󠁢󠁷󠁬󠁳󠁿': 'wales'
90691
- }
90692
-
90693
- ;// ./lib/owlmoji.ts
90694
-
90695
- const owlmoji = [
90696
- {
90697
- emoji: '', // This `emoji` property doesn't get consumed, but is required for type consistency
90698
- names: ['owlbert'],
90699
- tags: ['owlbert'],
90700
- description: 'an owlbert for any occasion',
90701
- category: 'ReadMe',
90702
- },
90703
- {
90704
- emoji: '',
90705
- names: ['owlbert-books'],
90706
- tags: ['owlbert'],
90707
- description: 'owlbert carrying books',
90708
- category: 'ReadMe',
90709
- },
90710
- {
90711
- emoji: '',
90712
- names: ['owlbert-mask'],
90713
- tags: ['owlbert'],
90714
- description: 'owlbert with a respirator',
90715
- category: 'ReadMe',
90716
- },
90717
- {
90718
- emoji: '',
90719
- names: ['owlbert-reading'],
90720
- tags: ['owlbert'],
90721
- description: 'owlbert reading',
90722
- category: 'ReadMe',
90723
- },
90724
- {
90725
- emoji: '',
90726
- names: ['owlbert-thinking'],
90727
- tags: ['owlbert'],
90728
- description: 'owlbert thinking',
90729
- category: 'ReadMe',
90730
- },
90731
- ];
90732
- const owlmojiNames = owlmoji.flatMap(emoji => emoji.names);
90733
- class Owlmoji {
90734
- static kind = (name) => {
90735
- if (name in nameToEmoji)
90736
- return 'gemoji';
90737
- else if (name.match(/^fa-/))
90738
- return 'fontawesome';
90739
- else if (owlmojiNames.includes(name))
90740
- return 'owlmoji';
90741
- return null;
90742
- };
90743
- static nameToEmoji = nameToEmoji;
90744
- static owlmoji = gemoji_gemoji.concat(owlmoji);
90745
- }
90746
-
90747
- ;// ./processor/transform/gemoji+.ts
90748
-
90749
-
90750
-
90751
- const gemoji_regex = /(?<=^|\s):(?<name>\+1|[-\w]+):/g;
90752
- const gemojiReplacer = (_, name) => {
90753
- switch (Owlmoji.kind(name)) {
90754
- case 'gemoji': {
90755
- const node = {
90756
- type: NodeTypes.emoji,
90757
- value: Owlmoji.nameToEmoji[name],
90758
- name,
90759
- };
90760
- return node;
90761
- }
90762
- case 'fontawesome': {
90763
- const node = {
90764
- type: NodeTypes.i,
90765
- value: name,
90766
- data: {
90767
- hName: 'i',
90768
- hProperties: {
90769
- className: ['fa-regular', name],
90770
- },
90771
- },
90772
- };
90773
- return node;
90774
- }
90775
- case 'owlmoji': {
90776
- const node = {
90777
- type: 'image',
90778
- title: `:${name}:`,
90779
- alt: `:${name}:`,
90780
- url: `/public/img/emojis/${name}.png`,
90781
- data: {
90782
- hProperties: {
90783
- className: 'emoji',
90784
- align: 'absmiddle',
90785
- height: '20',
90786
- width: '20',
90787
- },
90788
- },
90789
- };
90790
- return node;
90791
- }
90792
- default:
90793
- return false;
90794
- }
90795
- };
90796
- const gemojiTransformer = () => (tree) => {
90797
- findAndReplace(tree, [gemoji_regex, gemojiReplacer]);
90798
- return tree;
90799
- };
90800
- /* harmony default export */ const gemoji_ = (gemojiTransformer);
90801
-
90802
- ;// ./lib/mdast.ts
90803
-
90804
- const mdast = (text, opts = {}) => {
90805
- const processor = ast_processor(opts);
90806
- const tree = processor.parse(text);
90807
- return processor.runSync(tree);
90808
- };
90809
- /* harmony default export */ const lib_mdast = (mdast);
90810
-
90811
- ;// ./lib/utils/mdxish/protect-code-blocks.ts
90812
- /**
90813
- * Replaces code blocks and inline code with placeholders to protect them from preprocessing.
90814
- *
90815
- * @param content - The markdown content to process
90816
- * @returns Object containing protected content and arrays of original code blocks
90817
- * @example
90818
- * ```typescript
90819
- * const input = 'Text with `inline code` and ```fenced block```';
90820
- * protectCodeBlocks(input)
90821
- * // Returns: {
90822
- * // protectedCode: {
90823
- * // codeBlocks: ['```fenced block```'],
90824
- * // inlineCode: ['`inline code`']
90825
- * // },
90826
- * // protectedContent: 'Text with ___INLINE_CODE_0___ and ___CODE_BLOCK_0___'
90827
- * // }
90828
- * ```
90829
- */
90830
- function protectCodeBlocks(content) {
90831
- const codeBlocks = [];
90832
- const inlineCode = [];
90833
- let protectedContent = '';
90834
- let remaining = content;
90835
- let codeBlockStart = remaining.indexOf('```');
90836
- while (codeBlockStart !== -1) {
90837
- protectedContent += remaining.slice(0, codeBlockStart);
90838
- remaining = remaining.slice(codeBlockStart);
90839
- const codeBlockEnd = remaining.indexOf('```', 3);
90840
- if (codeBlockEnd === -1) {
90841
- break;
90842
- }
90843
- const match = remaining.slice(0, codeBlockEnd + 3);
90844
- const index = codeBlocks.length;
90845
- codeBlocks.push(match);
90846
- protectedContent += `___CODE_BLOCK_${index}___`;
90847
- remaining = remaining.slice(codeBlockEnd + 3);
90848
- codeBlockStart = remaining.indexOf('```');
90849
- }
90850
- protectedContent += remaining;
90851
- protectedContent = protectedContent.replace(/`([^`\n]+)`/g, match => {
90852
- const index = inlineCode.length;
90853
- inlineCode.push(match);
90854
- return `___INLINE_CODE_${index}___`;
90855
- });
90856
- return { protectedCode: { codeBlocks, inlineCode }, protectedContent };
90857
- }
90858
- /**
90859
- * Restores inline code by replacing placeholders with original content.
90860
- *
90861
- * @param content - Content with inline code placeholders
90862
- * @param protectedCode - The protected code arrays
90863
- * @returns Content with inline code restored
90864
- */
90865
- function restoreInlineCode(content, protectedCode) {
90866
- return content.replace(/___INLINE_CODE_(\d+)___/g, (_m, idx) => {
90867
- return protectedCode.inlineCode[parseInt(idx, 10)];
90868
- });
90869
- }
90870
- /**
90871
- * Restores fenced code blocks by replacing placeholders with original content.
90872
- *
90873
- * @param content - Content with code block placeholders
90874
- * @param protectedCode - The protected code arrays
90875
- * @returns Content with code blocks restored
90876
- */
90877
- function restoreFencedCodeBlocks(content, protectedCode) {
90878
- return content.replace(/___CODE_BLOCK_(\d+)___/g, (_m, idx) => {
90879
- return protectedCode.codeBlocks[parseInt(idx, 10)];
90880
- });
90881
- }
90882
- /**
90883
- * Restores all code blocks and inline code by replacing placeholders with original content.
90884
- *
90885
- * @param content - Content with code placeholders
90886
- * @param protectedCode - The protected code arrays
90887
- * @returns Content with all code blocks and inline code restored
90888
- * @example
90889
- * ```typescript
90890
- * const content = 'Text with ___INLINE_CODE_0___ and ___CODE_BLOCK_0___';
90891
- * const protectedCode = {
90892
- * codeBlocks: ['```js\ncode\n```'],
90893
- * inlineCode: ['`inline`']
90894
- * };
90895
- * restoreCodeBlocks(content, protectedCode)
90896
- * // Returns: 'Text with `inline` and ```js\ncode\n```'
90897
- * ```
90898
- */
90899
- function restoreCodeBlocks(content, protectedCode) {
90900
- let restored = restoreFencedCodeBlocks(content, protectedCode);
90901
- restored = restoreInlineCode(restored, protectedCode);
90902
- return restored;
90903
- }
90904
-
90905
- ;// ./processor/transform/mdxish/preprocess-jsx-expressions.ts
90906
-
90907
- // Base64 encode (Node.js + browser compatible)
90908
- function base64Encode(str) {
90909
- if (typeof Buffer !== 'undefined') {
90910
- return Buffer.from(str, 'utf-8').toString('base64');
90911
- }
90912
- return btoa(unescape(encodeURIComponent(str)));
90913
- }
90914
- // Base64 decode (Node.js + browser compatible)
90915
- function base64Decode(str) {
90916
- if (typeof Buffer !== 'undefined') {
90917
- return Buffer.from(str, 'base64').toString('utf-8');
90918
- }
90919
- return decodeURIComponent(escape(atob(str)));
90920
- }
90921
- function escapeHtmlAttribute(value) {
90922
- return value
90923
- .replace(/&/g, '&amp;')
90924
- .replace(/"/g, '&quot;')
90925
- .replace(/</g, '&lt;')
90926
- .replace(/>/g, '&gt;')
90927
- .replace(/\n/g, '&#10;');
90928
- }
90929
- // Marker prefix for JSON-serialized complex values (arrays/objects)
90930
- // Using a prefix that won't conflict with regular string values
90931
- const JSON_VALUE_MARKER = '__MDXISH_JSON__';
90932
- // Markers for protected HTMLBlock content (HTML comments avoid markdown parsing issues)
90933
- const HTML_BLOCK_CONTENT_START = '<!--RDMX_HTMLBLOCK:';
90934
- const HTML_BLOCK_CONTENT_END = ':RDMX_HTMLBLOCK-->';
90935
- /**
90936
- * Evaluates a JavaScript expression using context variables.
90937
- *
90938
- * @param expression
90939
- * @param context
90940
- * @returns The evaluated result
90941
- * @example
90942
- * ```typescript
90943
- * const context = { baseUrl: 'https://example.com', path: '/api' };
90944
- * evaluateExpression('baseUrl + path', context)
90945
- * // Returns: 'https://example.com/api'
90946
- * ```
90947
- */
90948
- function evaluateExpression(expression, context) {
90949
- const contextKeys = Object.keys(context);
90950
- const contextValues = Object.values(context);
90951
- // eslint-disable-next-line no-new-func
90952
- const func = new Function(...contextKeys, `return ${expression}`);
90953
- return func(...contextValues);
90954
- }
90955
- /**
90956
- * Base64 encodes HTMLBlock template literal content to prevent markdown parser from consuming <script>/<style> tags.
90957
- *
90958
- * @param content
90959
- * @returns Content with HTMLBlock template literals base64 encoded in HTML comments
90960
- * @example
90961
- * ```typescript
90962
- * const input = '<HTMLBlock>{`<script>alert("xss")</script>`}</HTMLBlock>';
90963
- * protectHTMLBlockContent(input)
90964
- * // Returns: '<HTMLBlock><!--RDMX_HTMLBLOCK:PHNjcmlwdD5hbGVydCgieHNzIik8L3NjcmlwdD4=:RDMX_HTMLBLOCK--></HTMLBlock>'
90965
- * ```
90966
- */
90967
- function protectHTMLBlockContent(content) {
90968
- return content.replace(/(<HTMLBlock[^>]*>)\{\s*`((?:[^`\\]|\\.)*)`\s*\}(<\/HTMLBlock>)/g, (_match, openTag, templateContent, closeTag) => {
90969
- const encoded = base64Encode(templateContent);
90970
- return `${openTag}${HTML_BLOCK_CONTENT_START}${encoded}${HTML_BLOCK_CONTENT_END}${closeTag}`;
90971
- });
90972
- }
90973
- /**
90974
- * Removes JSX-style comments (e.g., { /* comment *\/ }) from content.
90975
- *
90976
- * @param content
90977
- * @returns Content with JSX comments removed
90978
- * @example
90979
- * ```typescript
90980
- * removeJSXComments('Text { /* comment *\/ } more text')
90981
- * // Returns: 'Text more text'
90982
- * ```
90983
- */
90984
- function removeJSXComments(content) {
90985
- return content.replace(/\{\s*\/\*[^*]*(?:\*(?!\/)[^*]*)*\*\/\s*\}/g, '');
90986
- }
90987
- /**
90988
- * Extracts content between balanced braces, handling nested braces.
90989
- *
90990
- * @param content
90991
- * @param start
90992
- * @returns Object with extracted content and end position, or null if braces are unbalanced
90993
- * @example
90994
- * ```typescript
90995
- * const input = 'foo{bar{baz}qux}end';
90996
- * extractBalancedBraces(input, 3) // start at position 3 (after '{')
90997
- * // Returns: { content: 'bar{baz}qux', end: 16 }
90998
- * ```
90999
- */
91000
- function extractBalancedBraces(content, start) {
91001
- let depth = 1;
91002
- let pos = start;
91003
- while (pos < content.length && depth > 0) {
91004
- const char = content[pos];
91005
- if (char === '{')
91006
- depth += 1;
91007
- else if (char === '}')
91008
- depth -= 1;
91009
- pos += 1;
91010
- }
91011
- if (depth !== 0)
91012
- return null;
91013
- return { content: content.slice(start, pos - 1), end: pos };
91014
- }
91015
- /**
91016
- * Escapes problematic braces in content to prevent MDX expression parsing errors.
91017
- * Handles three cases:
91018
- * 1. Unbalanced braces (e.g., `{foo` without closing `}`)
91019
- * 2. Paragraph-spanning expressions (e.g., `{\n\n}` where blank line splits paragraphs)
91020
- * 3. Skips HTML elements to prevent backslashes appearing in output
91021
- *
91022
- */
91023
- function escapeProblematicBraces(content) {
91024
- // Skip HTML elements — their content should never be escaped because
91025
- // rehypeRaw parses them into hast elements, making `\` literal text in output
91026
- const htmlElements = [];
91027
- const safe = content.replace(/<([a-z][a-zA-Z0-9]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>/g, match => {
91028
- const idx = htmlElements.length;
91029
- htmlElements.push(match);
91030
- return `___HTML_ELEM_${idx}___`;
91031
- });
91032
- const toEscape = new Set();
91033
- // Convert to array of Unicode code points to handle emojis and multi-byte characters correctly
91034
- const chars = Array.from(safe);
91035
- let strDelim = null;
91036
- let strEscaped = false;
91037
- // Stack of open braces with their state
91038
- const openStack = [];
91039
- // Track position of last newline (outside strings) to detect blank lines
91040
- let lastNewlinePos = -2; // -2 means no recent newline
91041
- for (let i = 0; i < chars.length; i += 1) {
91042
- const ch = chars[i];
91043
- // Track string delimiters inside expressions to ignore braces within them
91044
- if (openStack.length > 0) {
91045
- if (strDelim) {
91046
- if (strEscaped)
91047
- strEscaped = false;
91048
- else if (ch === '\\')
91049
- strEscaped = true;
91050
- else if (ch === strDelim)
91051
- strDelim = null;
91052
- // eslint-disable-next-line no-continue
91053
- continue;
91054
- }
91055
- if (ch === '"' || ch === "'" || ch === '`') {
91056
- strDelim = ch;
91057
- // eslint-disable-next-line no-continue
91058
- continue;
91059
- }
91060
- // Track newlines to detect blank lines (paragraph boundaries)
91061
- if (ch === '\n') {
91062
- // Check if this newline creates a blank line (only whitespace since last newline)
91063
- if (lastNewlinePos >= 0) {
91064
- const between = chars.slice(lastNewlinePos + 1, i).join('');
91065
- if (/^[ \t]*$/.test(between)) {
91066
- // This is a blank line - mark all open expressions as paragraph-spanning
91067
- openStack.forEach(entry => {
91068
- entry.hasBlankLine = true;
91069
- });
91070
- }
91071
- }
91072
- lastNewlinePos = i;
91073
- }
91074
- }
91075
- // Skip already-escaped braces (count preceding backslashes)
91076
- if (ch === '{' || ch === '}') {
91077
- let bs = 0;
91078
- for (let j = i - 1; j >= 0 && chars[j] === '\\'; j -= 1)
91079
- bs += 1;
91080
- if (bs % 2 === 1) {
91081
- // eslint-disable-next-line no-continue
91082
- continue;
91083
- }
91084
- }
91085
- if (ch === '{') {
91086
- openStack.push({ pos: i, hasBlankLine: false });
91087
- lastNewlinePos = -2; // Reset newline tracking for new expression
91088
- }
91089
- else if (ch === '}') {
91090
- if (openStack.length > 0) {
91091
- const entry = openStack.pop();
91092
- // If expression spans paragraph boundary, escape both braces
91093
- if (entry.hasBlankLine) {
91094
- toEscape.add(entry.pos);
91095
- toEscape.add(i);
91096
- }
91097
- }
91098
- else {
91099
- // Unbalanced closing brace (no matching open)
91100
- toEscape.add(i);
91101
- }
91102
- }
91103
- }
91104
- // Any remaining open braces are unbalanced
91105
- openStack.forEach(entry => toEscape.add(entry.pos));
91106
- // If there are no problematic braces, return safe content as-is;
91107
- // otherwise, escape each problematic `{` or `}` so MDX doesn't treat them as expressions.
91108
- let result = toEscape.size === 0
91109
- ? safe
91110
- : chars.map((ch, i) => (toEscape.has(i) ? `\\${ch}` : ch)).join('');
91111
- // Restore HTML elements
91112
- if (htmlElements.length > 0) {
91113
- result = result.replace(/___HTML_ELEM_(\d+)___/g, (_m, idx) => htmlElements[parseInt(idx, 10)]);
91114
- }
91115
- return result;
91116
- }
91117
- /**
91118
- * Converts JSX attribute expressions (attribute={expression}) to HTML attributes (attribute="value").
91119
- * Handles style objects (camelCase → kebab-case), className → class, and JSON stringifies objects.
91120
- *
91121
- * @param content
91122
- * @param context
91123
- * @returns Content with attribute expressions evaluated and converted to HTML attributes
91124
- * @example
91125
- * ```typescript
91126
- * const context = { baseUrl: 'https://example.com' };
91127
- * const input = '<a href={baseUrl}>Link</a>';
91128
- * evaluateAttributeExpressions(input, context)
91129
- * // Returns: '<a href="https://example.com">Link</a>'
91130
- * ```
91131
- */
91132
- function evaluateAttributeExpressions(content, context, protectedCode) {
91133
- const attrStartRegex = /(\w+)=\{/g;
91134
- let result = '';
91135
- let lastEnd = 0;
91136
- let match = attrStartRegex.exec(content);
91137
- while (match !== null) {
91138
- const attributeName = match[1];
91139
- const braceStart = match.index + match[0].length;
91140
- const extracted = extractBalancedBraces(content, braceStart);
91141
- if (extracted) {
91142
- // The expression might contain template literals in MDX component tag props
91143
- // E.g. <Component greeting={`Hello World!`} />
91144
- // that is marked as inline code. So we need to restore the inline codes
91145
- // in the expression to evaluate it
91146
- let expression = extracted.content;
91147
- if (protectedCode) {
91148
- expression = restoreInlineCode(expression, protectedCode);
91149
- }
91150
- const fullMatchEnd = extracted.end;
91151
- result += content.slice(lastEnd, match.index);
91152
- try {
91153
- const evalResult = evaluateExpression(expression, context);
91154
- if (typeof evalResult === 'object' && evalResult !== null) {
91155
- if (attributeName === 'style') {
91156
- const cssString = Object.entries(evalResult)
91157
- .map(([key, value]) => {
91158
- const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
91159
- return `${cssKey}: ${value}`;
91160
- })
91161
- .join('; ');
91162
- result += `style="${cssString}"`;
91163
- }
91164
- else {
91165
- // These are arrays / objects attribute values
91166
- // Mark JSON-serialized values with a prefix so they can be parsed back correctly
91167
- const jsonValue = escapeHtmlAttribute(JSON_VALUE_MARKER + JSON.stringify(evalResult));
91168
- // Use double quotes so that multi-paragraph values are not split into multiple attributes by the processors
91169
- result += `${attributeName}="${jsonValue}"`;
91170
- }
91171
- }
91172
- else if (attributeName === 'className') {
91173
- // Escape special characters so that it doesn't break and split the attribute value to nodes
91174
- // This will be restored later in the pipeline
91175
- result += `class="${escapeHtmlAttribute(String(evalResult))}"`;
91176
- }
91177
- else {
91178
- result += `${attributeName}="${escapeHtmlAttribute(String(evalResult))}"`;
91179
- }
91180
- }
91181
- catch (_error) {
91182
- result += content.slice(match.index, fullMatchEnd);
91183
- }
91184
- lastEnd = fullMatchEnd;
91185
- attrStartRegex.lastIndex = fullMatchEnd;
91186
- }
91187
- match = attrStartRegex.exec(content);
91188
- }
91189
- result += content.slice(lastEnd);
91190
- return result;
91498
+ '🇿🇲': 'zambia',
91499
+ '🇿🇼': 'zimbabwe',
91500
+ '🏴󠁧󠁢󠁥󠁮󠁧󠁿': 'england',
91501
+ '🏴󠁧󠁢󠁳󠁣󠁴󠁿': 'scotland',
91502
+ '🏴󠁧󠁢󠁷󠁬󠁳󠁿': 'wales'
91191
91503
  }
91192
- /**
91193
- * Preprocesses JSX-like expressions in markdown before parsing.
91194
- * Inline expressions are handled separately; attribute expressions are processed here.
91195
- *
91196
- * @param content
91197
- * @param context
91198
- * @returns Preprocessed content ready for markdown parsing
91199
- */
91200
- function preprocessJSXExpressions(content, context = {}) {
91201
- // Step 0: Base64 encode HTMLBlock content
91202
- let processed = protectHTMLBlockContent(content);
91203
- // Step 1: Protect code blocks and inline code
91204
- const { protectedCode, protectedContent } = protectCodeBlocks(processed);
91205
- // Step 2: Evaluate attribute expressions (JSX attribute syntax: href={baseUrl})
91206
- // For inline expressions, we use a library to parse the expression & evaluate it later
91207
- // For attribute expressions, it was difficult to use a library to parse them, so do it manually
91208
- processed = evaluateAttributeExpressions(protectedContent, context, protectedCode);
91209
- // Step 3: Escape problematic braces to prevent MDX expression parsing errors
91210
- // This handles both unbalanced braces and paragraph-spanning expressions in one pass
91211
- processed = escapeProblematicBraces(processed);
91212
- // Step 4: Restore protected code blocks
91213
- processed = restoreCodeBlocks(processed, protectedCode);
91214
- return processed;
91504
+
91505
+ ;// ./lib/owlmoji.ts
91506
+
91507
+ const owlmoji = [
91508
+ {
91509
+ emoji: '', // This `emoji` property doesn't get consumed, but is required for type consistency
91510
+ names: ['owlbert'],
91511
+ tags: ['owlbert'],
91512
+ description: 'an owlbert for any occasion',
91513
+ category: 'ReadMe',
91514
+ },
91515
+ {
91516
+ emoji: '',
91517
+ names: ['owlbert-books'],
91518
+ tags: ['owlbert'],
91519
+ description: 'owlbert carrying books',
91520
+ category: 'ReadMe',
91521
+ },
91522
+ {
91523
+ emoji: '',
91524
+ names: ['owlbert-mask'],
91525
+ tags: ['owlbert'],
91526
+ description: 'owlbert with a respirator',
91527
+ category: 'ReadMe',
91528
+ },
91529
+ {
91530
+ emoji: '',
91531
+ names: ['owlbert-reading'],
91532
+ tags: ['owlbert'],
91533
+ description: 'owlbert reading',
91534
+ category: 'ReadMe',
91535
+ },
91536
+ {
91537
+ emoji: '',
91538
+ names: ['owlbert-thinking'],
91539
+ tags: ['owlbert'],
91540
+ description: 'owlbert thinking',
91541
+ category: 'ReadMe',
91542
+ },
91543
+ ];
91544
+ const owlmojiNames = owlmoji.flatMap(emoji => emoji.names);
91545
+ class Owlmoji {
91546
+ static kind = (name) => {
91547
+ if (name in nameToEmoji)
91548
+ return 'gemoji';
91549
+ else if (name.match(/^fa-/))
91550
+ return 'fontawesome';
91551
+ else if (owlmojiNames.includes(name))
91552
+ return 'owlmoji';
91553
+ return null;
91554
+ };
91555
+ static nameToEmoji = nameToEmoji;
91556
+ static owlmoji = gemoji_gemoji.concat(owlmoji);
91215
91557
  }
91216
91558
 
91217
- ;// ./processor/utils.ts
91559
+ ;// ./processor/transform/gemoji+.ts
91218
91560
 
91219
91561
 
91220
91562
 
91221
- /**
91222
- * Formats the hProperties of a node as a string, so they can be compiled back into JSX/MDX.
91223
- * This currently sets all the values to a string since we process/compile the MDX on the fly
91224
- * through the editor, and it'll throw errors over malformed JSX. TODO: fix this.
91225
- *
91226
- * @template T
91227
- * @param {Node} node
91228
- * @returns {string} formatted hProperties as JSX attributes
91229
- */
91230
- const formatHProps = (node) => {
91231
- const hProps = getHProps(node);
91232
- return formatProps(hProps);
91233
- };
91234
- /**
91235
- * Formats an object of props as a string.
91236
- *
91237
- * @param {Object} props
91238
- * @returns {string}
91239
- */
91240
- const formatProps = (props) => {
91241
- const keys = Object.keys(props);
91242
- return keys.map(key => `${key}="${props[key]}"`).join(' ');
91243
- };
91244
- /**
91245
- * Returns the hProperties of a node.
91246
- *
91247
- * @template T
91248
- * @param {Node} node
91249
- * @returns {T} hProperties
91250
- */
91251
- const getHProps = (node) => {
91252
- const hProps = node.data?.hProperties || {};
91253
- return hProps;
91254
- };
91255
- /**
91256
- * Returns array of hProperty keys.
91257
- *
91258
- * @template T
91259
- * @param {Node} node
91260
- * @returns {Array} array of hProperty keys
91261
- */
91262
- const getHPropKeys = (node) => {
91263
- const hProps = getHProps(node);
91264
- return Object.keys(hProps) || [];
91265
- };
91266
- /**
91267
- * Gets the attributes of an MDX element and returns them as an object of hProperties.
91268
- *
91269
- * @template T
91270
- * @param {(MdxJsxFlowElement | MdxJsxTextElement)} jsx
91271
- * @returns {T} object of hProperties
91272
- */
91273
- const getAttrs = (jsx) => jsx.attributes.reduce((memo, attr) => {
91274
- if ('name' in attr) {
91275
- if (typeof attr.value === 'string') {
91276
- if (attr.value.startsWith(JSON_VALUE_MARKER)) {
91277
- try {
91278
- memo[attr.name] = JSON.parse(attr.value.slice(JSON_VALUE_MARKER.length));
91279
- }
91280
- catch {
91281
- memo[attr.name] = attr.value;
91282
- }
91283
- }
91284
- else {
91285
- memo[attr.name] = attr.value;
91286
- }
91287
- }
91288
- else if (attr.value === null) {
91289
- memo[attr.name] = true;
91290
- }
91291
- else if (attr.value.value !== 'undefined') {
91292
- memo[attr.name] = JSON.parse(attr.value.value);
91293
- }
91294
- }
91295
- return memo;
91296
- }, {});
91297
- /**
91298
- * Gets the children of an MDX element and returns them as an array of Text nodes.
91299
- * Currently only being used by the HTML Block component, which only expects a single text node.
91300
- *
91301
- * @template T
91302
- * @param {(MdxJsxFlowElement | MdxJsxTextElement)} jsx
91303
- * @returns {Array} array of child text nodes
91304
- */
91305
- const utils_getChildren = (jsx) => jsx.children.reduce((memo, child, i) => {
91306
- memo[i] = {
91307
- type: 'text',
91308
- value: child.value,
91309
- position: child.position,
91310
- };
91311
- return memo;
91312
- }, []);
91313
- /**
91314
- * Tests if a node is an MDX element.
91315
- * TODO: Make this more extensible to all types of nodes. isElement(node, 'type' or ['type1', 'type2']), say
91316
- *
91317
- * @param {Node} node
91318
- * @returns {(node is MdxJsxFlowElement | MdxJsxTextElement | MdxjsEsm)}
91319
- */
91320
- const isMDXElement = (node) => {
91321
- return ['mdxJsxFlowElement', 'mdxJsxTextElement'].includes(node.type);
91322
- };
91323
- /**
91324
- * Tests if a node is an MDX ESM element (i.e. import or export).
91325
- *
91326
- * @param {Node} node
91327
- * @returns {boolean}
91328
- */
91329
- const isMDXEsm = (node) => {
91330
- return node.type === 'mdxjsEsm';
91331
- };
91332
- /**
91333
- * Takes an HTML string and formats it for display in the editor. Removes leading/trailing newlines
91334
- * and unindents the HTML.
91335
- *
91336
- * @param {string} html - HTML content from template literal
91337
- * @returns {string} processed HTML
91338
- */
91339
- function formatHtmlForMdxish(html) {
91340
- // Remove leading/trailing backticks if present, since they're used to keep the HTML
91341
- // from being parsed prematurely
91342
- let processed = html;
91343
- if (processed.startsWith('`') && processed.endsWith('`')) {
91344
- processed = processed.slice(1, -1);
91345
- }
91346
- // Removes the leading/trailing newlines
91347
- let cleaned = processed.replace(/^\s*\n|\n\s*$/g, '');
91348
- // Convert literal \n sequences to actual newlines BEFORE processing backticks
91349
- // This prevents the backtick unescaping regex from incorrectly matching \n sequences
91350
- cleaned = cleaned.replace(/\\n/g, '\n');
91351
- // Unescape backticks: \` -> ` (users escape backticks in template literals)
91352
- // Handle both cases: \` (adjacent) and \ followed by ` (split by markdown parser)
91353
- cleaned = cleaned.replace(/\\`/g, '`');
91354
- // Also handle case where backslash and backtick got separated by markdown parsing
91355
- // Pattern: backslash followed by any characters (but not \n which we already handled), then a backtick
91356
- // This handles cases like: \example` -> `example` (replacing \ with ` at start)
91357
- // Exclude \n sequences to avoid matching them incorrectly
91358
- cleaned = cleaned.replace(/\\([^`\\n]*?)`/g, '`$1`');
91359
- // Fix case where markdown parser consumed one backtick from triple backticks
91360
- // Pattern: `` followed by a word (like ``javascript) should be ```javascript
91361
- // This handles cases where code fences were parsed and one backtick was lost
91362
- cleaned = cleaned.replace(/<(\w+[^>]*)>``(\w+)/g, '<$1>```$2');
91363
- // Unescape dollar signs: \$ -> $ (users escape $ in template literals to prevent interpolation)
91364
- cleaned = cleaned.replace(/\\\$/g, '$');
91365
- return cleaned;
91366
- }
91367
- /**
91368
- * Takes an HTML string and formats it for display in the editor. Removes leading/trailing newlines
91369
- * and unindents the HTML.
91370
- *
91371
- * @param {string} html
91372
- * @returns {string} formatted HTML
91373
- */
91374
- const formatHTML = (html) => {
91375
- // Remove leading/trailing backticks if present, since they're used to keep the HTML
91376
- // from being parsed prematurely
91377
- if (html.startsWith('`') && html.endsWith('`')) {
91378
- // eslint-disable-next-line no-param-reassign
91379
- html = html.slice(1, -1);
91380
- }
91381
- // Removes the leading/trailing newlines
91382
- const cleaned = html.replace(/^\s*\n|\n\s*$/g, '');
91383
- // // Get the number of spaces in the first line to determine the tab size
91384
- // const tab = cleaned.match(/^\s*/)[0].length;
91385
- // // Remove the first indentation level from each line
91386
- // const tabRegex = new RegExp(`^\\s{${tab}}`, 'gm');
91387
- // const unindented = cleaned.replace(tabRegex, '');
91388
- return cleaned;
91389
- };
91390
- /**
91391
- * Reformat HTML for the markdown/mdx by adding an indentation to each line. This assures that the
91392
- * HTML is indentend properly within the HTMLBlock component when rendered in the markdown/mdx.
91393
- *
91394
- * @param {string} html
91395
- * @param {number} [indent=2]
91396
- * @returns {string} re-formatted HTML
91397
- */
91398
- const reformatHTML = (html) => {
91399
- // Remove leading/trailing newlines
91400
- const cleaned = html.replace(/^\s*\n|\n\s*$/g, '').replaceAll(/(?<!\\(\\\\)*)`/g, '\\`');
91401
- // // Create a tab/indent with the specified number of spaces
91402
- // const tab = ' '.repeat(indent);
91403
- // // Indent each line of the HTML (converts to an array, indents each line, then joins back)
91404
- // const indented = cleaned.split('\n').map((line: string) => `${tab}${line}`).join('\n');
91405
- return cleaned;
91406
- };
91407
- const toAttributes = (object, keys = []) => {
91408
- const attributes = [];
91409
- Object.entries(object).forEach(([name, v]) => {
91410
- if (keys.length > 0 && !keys.includes(name))
91411
- return;
91412
- let value;
91413
- if (typeof v === 'undefined' || v === null || v === '' || v === false) {
91414
- return;
91415
- }
91416
- else if (typeof v === 'string') {
91417
- value = v;
91418
- }
91419
- else {
91420
- /* values can be null, undefined, string, or a expression, eg:
91421
- *
91422
- * ```
91423
- * <Image src="..." border={false} size={width - 20} />
91424
- * ```
91425
- *
91426
- * Parsing the expression seems to only be done by the library
91427
- * `mdast-util-mdx-jsx`, and so the most straight forward way to parse
91428
- * the expression and get the appropriate AST is with our `mdast`
91429
- * function.
91430
- */
91431
- const proxy = lib_mdast(`{${v}}`);
91432
- const data = proxy.children[0].data;
91433
- value = {
91434
- type: 'mdxJsxAttributeValueExpression',
91435
- value: v.toString(),
91436
- data,
91563
+ const gemoji_regex = /(?<=^|\s):(?<name>\+1|[-\w]+):/g;
91564
+ const gemojiReplacer = (_, name) => {
91565
+ switch (Owlmoji.kind(name)) {
91566
+ case 'gemoji': {
91567
+ const node = {
91568
+ type: NodeTypes.emoji,
91569
+ value: Owlmoji.nameToEmoji[name],
91570
+ name,
91437
91571
  };
91572
+ return node;
91438
91573
  }
91439
- attributes.push({
91440
- type: 'mdxJsxAttribute',
91441
- name,
91442
- value,
91443
- });
91444
- });
91445
- return attributes;
91446
- };
91447
- /**
91448
- * Checks if a named export exists in the MDX tree. Accepts either an mdast or
91449
- * a hast tree.
91450
- *
91451
- * example:
91452
- * ```
91453
- * const mdx = `export const Foo = 'bar';`
91454
- *
91455
- * hasNamedExport(mdast(mdx), 'Foo') => true
91456
- * ```
91457
- *
91458
- */
91459
- const hasNamedExport = (tree, name) => {
91460
- let hasExport = false;
91461
- // eslint-disable-next-line consistent-return
91462
- visit(tree, 'mdxjsEsm', node => {
91463
- if ('declaration' in node.data.estree.body[0] &&
91464
- node.data.estree.body[0].declaration.type === 'VariableDeclaration') {
91465
- const { declarations } = node.data.estree.body[0].declaration;
91466
- hasExport = !!declarations.find(declaration => 'name' in declaration.id && declaration.id.name === name);
91467
- return hasExport ? EXIT : CONTINUE;
91574
+ case 'fontawesome': {
91575
+ const node = {
91576
+ type: NodeTypes.i,
91577
+ value: name,
91578
+ data: {
91579
+ hName: 'i',
91580
+ hProperties: {
91581
+ className: ['fa-regular', name],
91582
+ },
91583
+ },
91584
+ };
91585
+ return node;
91468
91586
  }
91469
- });
91470
- return hasExport;
91471
- };
91472
- /* Example mdast structures to find first export name in a mdxjsEsm node:
91473
- There are three types of export declarations that we need to consider:
91474
- 1. VARIABLE DECLARATION
91475
- "type": "mdxjsEsm",
91476
- "value": "export const Foo = () => <div>Hello world</div>\nexport const Bar = () => <div>hello darkness my old friend</div>",
91477
- "data": {
91478
- "estree": {
91479
- "type": "Program",
91480
- "body": [
91481
- {
91482
- "type": "ExportNamedDeclaration",
91483
- "declaration": {
91484
- "type": "VariableDeclaration",
91485
- "declarations": [
91486
- {
91487
- "type": "VariableDeclarator",
91488
- "id": {
91489
- "type": "Identifier",
91490
- "name": "Foo" // --------> This is the export name
91587
+ case 'owlmoji': {
91588
+ const node = {
91589
+ type: 'image',
91590
+ title: `:${name}:`,
91591
+ alt: `:${name}:`,
91592
+ url: `/public/img/emojis/${name}.png`,
91593
+ data: {
91594
+ hProperties: {
91595
+ className: 'emoji',
91596
+ align: 'absmiddle',
91597
+ height: '20',
91598
+ width: '20',
91491
91599
  },
91492
- ...
91493
-
91494
- 2/3. FUNCTION DECLARATION & CLASS DECLARATION
91495
- "estree": {
91496
- "type": "Program",
91497
- "body": [
91498
- {
91499
- "type": "ExportNamedDeclaration",
91500
- "declaration": {
91501
- "type": "ClassDeclaration" | "FunctionDeclaration",
91502
- "id": {
91503
- "type": "Identifier",
91504
- "name": "Foo" // --------> This is the export name
91505
91600
  },
91506
- */
91507
- const getExports = (tree) => {
91508
- const set = new Set();
91509
- visit(tree, isMDXEsm, (node) => {
91510
- // Once inside an mdxjsEsm node, we need to check for one or more declared exports within data.estree.body
91511
- // This is because single newlines \n are not considered as a new block, so there may be more than one export statement in a single mdxjsEsm node
91512
- const body = node.data?.estree.body;
91513
- if (!body)
91514
- return;
91515
- body.forEach(child => {
91516
- if (child.type === 'ExportNamedDeclaration') {
91517
- // There are three types of ExportNamedDeclaration that we need to consider: VariableDeclaration, FunctionDeclaration, ClassDeclaration
91518
- const declaration = child.declaration;
91519
- // FunctionDeclaration and ClassDeclaration have the same structure
91520
- if (declaration.type !== 'VariableDeclaration') {
91521
- // Note: declaration.id.type is always 'Identifier' for FunctionDeclarations and ClassDeclarations
91522
- set.add(declaration.id.name);
91523
- }
91524
- else {
91525
- declaration.declarations.forEach(dec => {
91526
- const id = dec.id;
91527
- if (id.type === 'Identifier') {
91528
- set.add(id.name);
91529
- }
91530
- });
91531
- }
91532
- }
91533
- });
91534
- });
91535
- return Array.from(set);
91601
+ };
91602
+ return node;
91603
+ }
91604
+ default:
91605
+ return false;
91606
+ }
91536
91607
  };
91608
+ const gemojiTransformer = () => (tree) => {
91609
+ findAndReplace(tree, [gemoji_regex, gemojiReplacer]);
91610
+ return tree;
91611
+ };
91612
+ /* harmony default export */ const gemoji_ = (gemojiTransformer);
91537
91613
 
91538
91614
  ;// ./processor/transform/handle-missing-components.ts
91539
91615
 
@@ -92494,6 +92570,23 @@ const mdxishTables = () => tree => {
92494
92570
  return;
92495
92571
  try {
92496
92572
  const parsed = tableNodeProcessor.runSync(tableNodeProcessor.parse(node.value));
92573
+ // since we use a subparser in `tableNodeProcessor` to parse `node.value`,
92574
+ // positions are relative to that substring. shifting them by the base
92575
+ // offset and line number makes them valid in the outer source coordinate space.
92576
+ // otherwise, consumers who directly slice based on position would read and grab the
92577
+ // wrong content
92578
+ const baseOffset = node.position?.start?.offset ?? 0;
92579
+ const baseLine = (node.position?.start?.line ?? 1) - 1;
92580
+ visit(parsed, child => {
92581
+ if (child.position?.start) {
92582
+ child.position.start.offset = (child.position.start.offset ?? 0) + baseOffset;
92583
+ child.position.start.line += baseLine;
92584
+ }
92585
+ if (child.position?.end) {
92586
+ child.position.end.offset = (child.position.end.offset ?? 0) + baseOffset;
92587
+ child.position.end.line += baseLine;
92588
+ }
92589
+ });
92497
92590
  visit(parsed, isMDXElement, (tableNode) => {
92498
92591
  if (tableNode.name === 'Table') {
92499
92592
  processTableNode(tableNode, index, parent, node.position);
@@ -112904,74 +112997,6 @@ const codeTabs = (node, _, state, info) => {
112904
112997
  };
112905
112998
  /* harmony default export */ const compile_code_tabs = (codeTabs);
112906
112999
 
112907
- ;// ./processor/compile/compatibility.ts
112908
-
112909
-
112910
-
112911
- /*
112912
- * Converts a (remark < v9) html node to a JSX string.
112913
- *
112914
- * First we replace html comments with the JSX equivalent. Then, we parse that
112915
- * as html, and serialize it back as xml!
112916
- *
112917
- */
112918
- const compileHtml = (node) => {
112919
- const string = node.value.replaceAll(/<!--(.*)-->/gms, '{/*$1*/}');
112920
- return string;
112921
- };
112922
- const figureToImageBlock = (node) => {
112923
- const { align, border, width, src, url, alt, title, ...image } = node.children.find((child) => child.type === 'image');
112924
- const { className } = image.data.hProperties;
112925
- const figcaption = node.children.find((child) => child.type === 'figcaption');
112926
- const caption = figcaption ? toMarkdown(figcaption.children).trim() : null;
112927
- const attributes = {
112928
- ...(align && { align }),
112929
- ...(alt && { alt }),
112930
- ...(className && { border: className === 'border' }),
112931
- ...(border && { border }),
112932
- ...(caption && { caption }),
112933
- ...(title && { title }),
112934
- ...(width && { width }),
112935
- src: src || url,
112936
- };
112937
- return `<Image ${formatProps(attributes)} />`;
112938
- };
112939
- const embedToEmbedBlock = (node) => {
112940
- const { html, ...embed } = node.data.hProperties;
112941
- const attributes = {
112942
- ...embed,
112943
- ...(html && { html: encodeURIComponent(html) }),
112944
- };
112945
- return `<Embed ${formatProps(attributes)} />`;
112946
- };
112947
- const compatibility = (node) => {
112948
- switch (node.type) {
112949
- case NodeTypes.glossary: {
112950
- // Glossary terms will no longer be serialized as special node types in the Editor but we want to ensure that we compile historical
112951
- // data correctly
112952
- const term = node.data?.hProperties?.term || node.children[0].value;
112953
- return `<Glossary>${term}</Glossary>`;
112954
- }
112955
- case NodeTypes.reusableContent:
112956
- return `<${node.tag} />`;
112957
- case 'html':
112958
- return compileHtml(node);
112959
- case 'escape':
112960
- return `\\${node.value}`;
112961
- case 'figure':
112962
- return figureToImageBlock(node);
112963
- case 'embed':
112964
- return embedToEmbedBlock(node);
112965
- case 'i':
112966
- return `:${node.data.hProperties.className[1]}:`;
112967
- case 'yaml':
112968
- return `---\n${node.value}\n---`;
112969
- default:
112970
- throw new Error('Unhandled node type!');
112971
- }
112972
- };
112973
- /* harmony default export */ const compile_compatibility = (compatibility);
112974
-
112975
113000
  ;// ./processor/compile/embed.ts
112976
113001
 
112977
113002
  const embed_embed = (node) => {
@@ -119118,7 +119143,7 @@ function findJsxTableToken() {
119118
119143
  return undefined;
119119
119144
  }
119120
119145
  function enterJsxTable(token) {
119121
- jsx_table_contextMap.set(token, { chunks: [] });
119146
+ jsx_table_contextMap.set(token, { chunks: [], lastEndLine: token.start.line });
119122
119147
  this.enter({ type: 'html', value: '' }, token);
119123
119148
  }
119124
119149
  function exitJsxTableData(token) {
@@ -119126,14 +119151,20 @@ function exitJsxTableData(token) {
119126
119151
  if (!tableToken)
119127
119152
  return;
119128
119153
  const ctx = jsx_table_contextMap.get(tableToken);
119129
- if (ctx)
119154
+ if (ctx) {
119155
+ const gap = token.start.line - ctx.lastEndLine;
119156
+ if (ctx.chunks.length > 0 && gap > 0) {
119157
+ ctx.chunks.push('\n'.repeat(gap));
119158
+ }
119130
119159
  ctx.chunks.push(this.sliceSerialize(token));
119160
+ ctx.lastEndLine = token.end.line;
119161
+ }
119131
119162
  }
119132
119163
  function exitJsxTable(token) {
119133
119164
  const ctx = jsx_table_contextMap.get(token);
119134
119165
  const node = this.stack[this.stack.length - 1];
119135
119166
  if (ctx) {
119136
- node.value = ctx.chunks.join('\n');
119167
+ node.value = ctx.chunks.join('');
119137
119168
  jsx_table_contextMap.delete(token);
119138
119169
  }
119139
119170
  this.exit(token);