@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.js CHANGED
@@ -11578,7 +11578,8 @@ const Callout = (props) => {
11578
11578
  const children = external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_.Children.toArray(props.children);
11579
11579
  const icon = props.icon;
11580
11580
  const isEmoji = icon && emoji_regex().test(icon);
11581
- const heading = empty ? external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_.createElement("p", { className: 'callout-heading empty' }) : children[0];
11581
+ const hasBody = children.length > 1;
11582
+ const heading = empty ? (hasBody ? external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_.createElement("p", { className: 'callout-heading empty' }) : null) : children[0];
11582
11583
  const theme = props.theme || (icon && themes[icon]) || 'default';
11583
11584
  return (
11584
11585
  // @ts-expect-error -- theme is not a valid attribute
@@ -12118,8 +12119,9 @@ function useScrollHighlight(navRef) {
12118
12119
  const nav = navRef.current;
12119
12120
  if (!nav)
12120
12121
  return;
12121
- const key = Array.from(nav.querySelectorAll('a[href^="#"]'))
12122
- .map(a => a.getAttribute('href'))
12122
+ const key = Array.from(nav.querySelectorAll('a'))
12123
+ .map(a => a.hash)
12124
+ .filter(Boolean)
12123
12125
  .join('\0');
12124
12126
  setTocKey(key);
12125
12127
  });
@@ -12141,9 +12143,11 @@ function useScrollHighlight(navRef) {
12141
12143
  const scrollParent = getScrollParent(headings[0]);
12142
12144
  const isAtBottom = () => {
12143
12145
  if (scrollParent instanceof Window) {
12144
- return window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - SCROLL_BOTTOM_TOLERANCE;
12146
+ return document.documentElement.scrollHeight > window.innerHeight
12147
+ && window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - SCROLL_BOTTOM_TOLERANCE;
12145
12148
  }
12146
- return scrollParent.scrollTop + scrollParent.clientHeight >= scrollParent.scrollHeight - SCROLL_BOTTOM_TOLERANCE;
12149
+ return scrollParent.scrollHeight > scrollParent.clientHeight
12150
+ && scrollParent.scrollTop + scrollParent.clientHeight >= scrollParent.scrollHeight - SCROLL_BOTTOM_TOLERANCE;
12147
12151
  };
12148
12152
  const activate = (id) => {
12149
12153
  if (id === activeId)
@@ -12193,15 +12197,12 @@ function useScrollHighlight(navRef) {
12193
12197
  const onClick = (e) => {
12194
12198
  if (!(e.target instanceof Element))
12195
12199
  return;
12196
- const anchor = e.target.closest('a[href^="#"]');
12197
- if (!(anchor instanceof HTMLAnchorElement))
12200
+ const anchor = e.target.closest('a');
12201
+ if (!(anchor instanceof HTMLAnchorElement) || !anchor.hash)
12198
12202
  return;
12199
12203
  const id = decodeURIComponent(anchor.hash.slice(1));
12200
12204
  if (!linkMap.has(id))
12201
12205
  return;
12202
- if (window.location.hash !== anchor.hash) {
12203
- window.location.hash = anchor.hash;
12204
- }
12205
12206
  activate(id);
12206
12207
  clickLocked = true;
12207
12208
  let unlockTimer = null;
@@ -53146,267 +53147,1078 @@ const plain = (node, opts = {}) => {
53146
53147
  };
53147
53148
  /* harmony default export */ const lib_plain = (plain);
53148
53149
 
53149
- ;// ./processor/compile/gemoji.ts
53150
- const gemoji = (node) => `:${node.name}:`;
53151
- /* harmony default export */ const compile_gemoji = (gemoji);
53150
+ ;// ./lib/mdast.ts
53152
53151
 
53153
- ;// ./processor/compile/variable.ts
53154
- const variable = (node) => `{user.${node.data?.hProperties?.name || ''}}`;
53155
- /* harmony default export */ const compile_variable = (variable);
53152
+ const mdast = (text, opts = {}) => {
53153
+ const processor = ast_processor(opts);
53154
+ const tree = processor.parse(text);
53155
+ return processor.runSync(tree);
53156
+ };
53157
+ /* harmony default export */ const lib_mdast = (mdast);
53156
53158
 
53157
- ;// ./processor/transform/extract-text.ts
53159
+ ;// ./lib/utils/mdxish/protect-code-blocks.ts
53158
53160
  /**
53159
- * Extracts text content from a single AST node recursively.
53160
- * Works with both MDAST and HAST-like node structures.
53161
- *
53162
- * Placed this outside of the utils.ts file to avoid circular dependencies.
53161
+ * Replaces code blocks and inline code with placeholders to protect them from preprocessing.
53163
53162
  *
53164
- * @param node - The node to extract text from (can be MDAST Node or HAST-like structure)
53165
- * @returns The concatenated text content
53163
+ * @param content - The markdown content to process
53164
+ * @returns Object containing protected content and arrays of original code blocks
53165
+ * @example
53166
+ * ```typescript
53167
+ * const input = 'Text with `inline code` and ```fenced block```';
53168
+ * protectCodeBlocks(input)
53169
+ * // Returns: {
53170
+ * // protectedCode: {
53171
+ * // codeBlocks: ['```fenced block```'],
53172
+ * // inlineCode: ['`inline code`']
53173
+ * // },
53174
+ * // protectedContent: 'Text with ___INLINE_CODE_0___ and ___CODE_BLOCK_0___'
53175
+ * // }
53176
+ * ```
53166
53177
  */
53167
- const extractText = (node) => {
53168
- if ((node.type === 'text' || node.type === 'html') && typeof node.value === 'string') {
53169
- return node.value;
53170
- }
53171
- // When a blockquote contains only an image (no text), treat it as having content
53172
- // so the blockquote is no longer treated as empty and preserved correctly.
53173
- if (node.type === 'image') {
53174
- return typeof node.alt === 'string' && node.alt ? node.alt : '[image]';
53175
- }
53176
- if (node.children && Array.isArray(node.children)) {
53177
- return node.children
53178
- .map(child => {
53179
- if (child && typeof child === 'object' && 'type' in child) {
53180
- return extractText(child);
53181
- }
53182
- return '';
53183
- })
53184
- .join('');
53178
+ function protectCodeBlocks(content) {
53179
+ const codeBlocks = [];
53180
+ const inlineCode = [];
53181
+ let protectedContent = '';
53182
+ let remaining = content;
53183
+ let codeBlockStart = remaining.indexOf('```');
53184
+ while (codeBlockStart !== -1) {
53185
+ protectedContent += remaining.slice(0, codeBlockStart);
53186
+ remaining = remaining.slice(codeBlockStart);
53187
+ const codeBlockEnd = remaining.indexOf('```', 3);
53188
+ if (codeBlockEnd === -1) {
53189
+ break;
53190
+ }
53191
+ const match = remaining.slice(0, codeBlockEnd + 3);
53192
+ const index = codeBlocks.length;
53193
+ codeBlocks.push(match);
53194
+ protectedContent += `___CODE_BLOCK_${index}___`;
53195
+ remaining = remaining.slice(codeBlockEnd + 3);
53196
+ codeBlockStart = remaining.indexOf('```');
53185
53197
  }
53186
- return '';
53187
- };
53188
-
53189
- ;// ./processor/transform/callouts.ts
53190
-
53191
-
53192
-
53193
-
53194
-
53195
-
53196
-
53197
-
53198
-
53199
-
53200
-
53201
-
53202
-
53203
-
53204
- const titleParser = unified().use(remarkParse).use(remarkGfm);
53205
- // The title paragraph may contain custom AST nodes that `toMarkdown` doesn't
53206
- // natively understand
53207
- const toMarkdownExtensions = [
53208
- gfmToMarkdown(),
53209
- // For mdx variable syntaxes (e.g., {user.name})
53210
- mdxExpressionToMarkdown(),
53211
- // Important: This is required and would crash the parser if there's no variable and gemoji node handler
53212
- { handlers: { [NodeTypes.variable]: compile_variable, [NodeTypes.emoji]: compile_gemoji } },
53213
- ];
53214
- const callouts_regex = `^(${emoji_regex().source}|⚠)(\\s+|$)`;
53215
- const findFirst = (node) => {
53216
- if ('children' in node)
53217
- return findFirst(node.children[0]);
53218
- if (node.type === 'text')
53219
- return node;
53220
- return null;
53221
- };
53222
- const wrapHeading = (node) => {
53223
- const firstChild = node.children[0];
53224
- return {
53225
- type: 'heading',
53226
- depth: 3,
53227
- children: ('children' in firstChild ? firstChild.children : []),
53228
- position: {
53229
- start: firstChild.position.start,
53230
- end: firstChild.position.end,
53231
- },
53232
- };
53233
- };
53198
+ protectedContent += remaining;
53199
+ protectedContent = protectedContent.replace(/`([^`\n]+)`/g, match => {
53200
+ const index = inlineCode.length;
53201
+ inlineCode.push(match);
53202
+ return `___INLINE_CODE_${index}___`;
53203
+ });
53204
+ return { protectedCode: { codeBlocks, inlineCode }, protectedContent };
53205
+ }
53234
53206
  /**
53235
- * Checks if a blockquote matches the expected callout structure:
53236
- * blockquote > paragraph > text node
53207
+ * Restores inline code by replacing placeholders with original content.
53208
+ *
53209
+ * @param content - Content with inline code placeholders
53210
+ * @param protectedCode - The protected code arrays
53211
+ * @returns Content with inline code restored
53237
53212
  */
53238
- const isCalloutStructure = (node) => {
53239
- const firstChild = node.children?.[0];
53240
- if (!firstChild || firstChild.type !== 'paragraph')
53241
- return false;
53242
- if (!('children' in firstChild))
53243
- return false;
53244
- const firstTextChild = firstChild.children?.[0];
53245
- return firstTextChild?.type === 'text';
53246
- };
53213
+ function restoreInlineCode(content, protectedCode) {
53214
+ return content.replace(/___INLINE_CODE_(\d+)___/g, (_m, idx) => {
53215
+ return protectedCode.inlineCode[parseInt(idx, 10)];
53216
+ });
53217
+ }
53247
53218
  /**
53248
- * Finds the first text node containing a newline in a paragraph's children.
53249
- * Returns the index and the newline position within that text node.
53219
+ * Restores fenced code blocks by replacing placeholders with original content.
53220
+ *
53221
+ * @param content - Content with code block placeholders
53222
+ * @param protectedCode - The protected code arrays
53223
+ * @returns Content with code blocks restored
53250
53224
  */
53251
- const findNewlineInParagraph = (paragraph) => {
53252
- for (let i = 0; i < paragraph.children.length; i += 1) {
53253
- const child = paragraph.children[i];
53254
- if (child.type === 'text' && typeof child.value === 'string') {
53255
- const newlineIndex = child.value.indexOf('\n');
53256
- if (newlineIndex !== -1) {
53257
- return { index: i, newlineIndex };
53258
- }
53259
- }
53260
- }
53261
- return null;
53262
- };
53225
+ function restoreFencedCodeBlocks(content, protectedCode) {
53226
+ return content.replace(/___CODE_BLOCK_(\d+)___/g, (_m, idx) => {
53227
+ return protectedCode.codeBlocks[parseInt(idx, 10)];
53228
+ });
53229
+ }
53263
53230
  /**
53264
- * Splits a paragraph at the first newline, separating heading content (before \n)
53265
- * from body content (after \n). Mutates the paragraph to contain only heading children.
53231
+ * Restores all code blocks and inline code by replacing placeholders with original content.
53232
+ *
53233
+ * @param content - Content with code placeholders
53234
+ * @param protectedCode - The protected code arrays
53235
+ * @returns Content with all code blocks and inline code restored
53236
+ * @example
53237
+ * ```typescript
53238
+ * const content = 'Text with ___INLINE_CODE_0___ and ___CODE_BLOCK_0___';
53239
+ * const protectedCode = {
53240
+ * codeBlocks: ['```js\ncode\n```'],
53241
+ * inlineCode: ['`inline`']
53242
+ * };
53243
+ * restoreCodeBlocks(content, protectedCode)
53244
+ * // Returns: 'Text with `inline` and ```js\ncode\n```'
53245
+ * ```
53266
53246
  */
53267
- const splitParagraphAtNewline = (paragraph) => {
53268
- const splitPoint = findNewlineInParagraph(paragraph);
53269
- if (!splitPoint)
53270
- return null;
53271
- const { index, newlineIndex } = splitPoint;
53272
- const originalChildren = paragraph.children;
53273
- const textNode = originalChildren[index];
53274
- const beforeNewline = textNode.value.slice(0, newlineIndex);
53275
- const afterNewline = textNode.value.slice(newlineIndex + 1);
53276
- // Split paragraph: heading = children[0..index-1] + text before newline
53277
- const headingChildren = originalChildren.slice(0, index);
53278
- if (beforeNewline.length > 0 || headingChildren.length === 0) {
53279
- headingChildren.push({ type: 'text', value: beforeNewline });
53247
+ function restoreCodeBlocks(content, protectedCode) {
53248
+ let restored = restoreFencedCodeBlocks(content, protectedCode);
53249
+ restored = restoreInlineCode(restored, protectedCode);
53250
+ return restored;
53251
+ }
53252
+
53253
+ ;// ./processor/transform/mdxish/preprocess-jsx-expressions.ts
53254
+
53255
+ // Base64 encode (Node.js + browser compatible)
53256
+ function base64Encode(str) {
53257
+ if (typeof Buffer !== 'undefined') {
53258
+ return Buffer.from(str, 'utf-8').toString('base64');
53280
53259
  }
53281
- paragraph.children = headingChildren;
53282
- // Body = text after newline + remaining children from original array
53283
- const bodyChildren = [];
53284
- if (afterNewline.length > 0) {
53285
- bodyChildren.push({ type: 'text', value: afterNewline });
53260
+ return btoa(unescape(encodeURIComponent(str)));
53261
+ }
53262
+ // Base64 decode (Node.js + browser compatible)
53263
+ function base64Decode(str) {
53264
+ if (typeof Buffer !== 'undefined') {
53265
+ return Buffer.from(str, 'base64').toString('utf-8');
53286
53266
  }
53287
- bodyChildren.push(...originalChildren.slice(index + 1));
53288
- return bodyChildren.length > 0 ? bodyChildren : null;
53289
- };
53267
+ return decodeURIComponent(escape(atob(str)));
53268
+ }
53269
+ function escapeHtmlAttribute(value) {
53270
+ return value
53271
+ .replace(/&/g, '&amp;')
53272
+ .replace(/"/g, '&quot;')
53273
+ .replace(/</g, '&lt;')
53274
+ .replace(/>/g, '&gt;')
53275
+ .replace(/\n/g, '&#10;');
53276
+ }
53277
+ // Marker prefix for JSON-serialized complex values (arrays/objects)
53278
+ // Using a prefix that won't conflict with regular string values
53279
+ const JSON_VALUE_MARKER = '__MDXISH_JSON__';
53280
+ // Markers for protected HTMLBlock content (HTML comments avoid markdown parsing issues)
53281
+ const HTML_BLOCK_CONTENT_START = '<!--RDMX_HTMLBLOCK:';
53282
+ const HTML_BLOCK_CONTENT_END = ':RDMX_HTMLBLOCK-->';
53290
53283
  /**
53291
- * Removes the icon/match prefix from the first text node in a paragraph.
53292
- * This is needed to clean up the raw AST after we've extracted the icon.
53284
+ * Evaluates a JavaScript expression using context variables.
53285
+ *
53286
+ * @param expression
53287
+ * @param context
53288
+ * @returns The evaluated result
53289
+ * @example
53290
+ * ```typescript
53291
+ * const context = { baseUrl: 'https://example.com', path: '/api' };
53292
+ * evaluateExpression('baseUrl + path', context)
53293
+ * // Returns: 'https://example.com/api'
53294
+ * ```
53293
53295
  */
53294
- const removeIconPrefix = (paragraph, prefixLength) => {
53295
- const firstTextNode = findFirst(paragraph);
53296
- if (firstTextNode && 'value' in firstTextNode && typeof firstTextNode.value === 'string') {
53297
- firstTextNode.value = firstTextNode.value.slice(prefixLength);
53298
- }
53299
- };
53300
- const processBlockquote = (node, index, parent, isMdxish = false) => {
53301
- if (!isCalloutStructure(node)) {
53302
- // Only stringify empty blockquotes (no extractable text content)
53303
- // Preserve blockquotes with actual content (e.g., headings, lists, etc.)
53304
- const content = extractText(node);
53305
- const isEmpty = !content || content.trim() === '';
53306
- if (isEmpty && index !== undefined && parent) {
53307
- const textNode = {
53308
- type: 'text',
53309
- value: '>',
53310
- };
53311
- const paragraphNode = {
53312
- type: 'paragraph',
53313
- children: [textNode],
53314
- position: node.position,
53315
- };
53316
- parent.children.splice(index, 1, paragraphNode);
53317
- }
53318
- return;
53296
+ function evaluateExpression(expression, context) {
53297
+ const contextKeys = Object.keys(context);
53298
+ const contextValues = Object.values(context);
53299
+ // eslint-disable-next-line no-new-func
53300
+ const func = new Function(...contextKeys, `return ${expression}`);
53301
+ return func(...contextValues);
53302
+ }
53303
+ /**
53304
+ * Base64 encodes HTMLBlock template literal content to prevent markdown parser from consuming <script>/<style> tags.
53305
+ *
53306
+ * @param content
53307
+ * @returns Content with HTMLBlock template literals base64 encoded in HTML comments
53308
+ * @example
53309
+ * ```typescript
53310
+ * const input = '<HTMLBlock>{`<script>alert("xss")</script>`}</HTMLBlock>';
53311
+ * protectHTMLBlockContent(input)
53312
+ * // Returns: '<HTMLBlock><!--RDMX_HTMLBLOCK:PHNjcmlwdD5hbGVydCgieHNzIik8L3NjcmlwdD4=:RDMX_HTMLBLOCK--></HTMLBlock>'
53313
+ * ```
53314
+ */
53315
+ function protectHTMLBlockContent(content) {
53316
+ return content.replace(/(<HTMLBlock[^>]*>)\{\s*`((?:[^`\\]|\\.)*)`\s*\}(<\/HTMLBlock>)/g, (_match, openTag, templateContent, closeTag) => {
53317
+ const encoded = base64Encode(templateContent);
53318
+ return `${openTag}${HTML_BLOCK_CONTENT_START}${encoded}${HTML_BLOCK_CONTENT_END}${closeTag}`;
53319
+ });
53320
+ }
53321
+ /**
53322
+ * Removes JSX-style comments (e.g., { /* comment *\/ }) from content.
53323
+ *
53324
+ * @param content
53325
+ * @returns Content with JSX comments removed
53326
+ * @example
53327
+ * ```typescript
53328
+ * removeJSXComments('Text { /* comment *\/ } more text')
53329
+ * // Returns: 'Text more text'
53330
+ * ```
53331
+ */
53332
+ function removeJSXComments(content) {
53333
+ return content.replace(/\{\s*\/\*[^*]*(?:\*(?!\/)[^*]*)*\*\/\s*\}/g, '');
53334
+ }
53335
+ /**
53336
+ * Extracts content between balanced braces, handling nested braces.
53337
+ *
53338
+ * @param content
53339
+ * @param start
53340
+ * @returns Object with extracted content and end position, or null if braces are unbalanced
53341
+ * @example
53342
+ * ```typescript
53343
+ * const input = 'foo{bar{baz}qux}end';
53344
+ * extractBalancedBraces(input, 3) // start at position 3 (after '{')
53345
+ * // Returns: { content: 'bar{baz}qux', end: 16 }
53346
+ * ```
53347
+ */
53348
+ function extractBalancedBraces(content, start) {
53349
+ let depth = 1;
53350
+ let pos = start;
53351
+ while (pos < content.length && depth > 0) {
53352
+ const char = content[pos];
53353
+ if (char === '{')
53354
+ depth += 1;
53355
+ else if (char === '}')
53356
+ depth -= 1;
53357
+ pos += 1;
53319
53358
  }
53320
- // isCalloutStructure ensures node.children[0] is a Paragraph with children
53321
- const firstParagraph = node.children[0];
53322
- const startText = lib_plain(firstParagraph).toString();
53323
- const [match, icon] = startText.match(callouts_regex) || [];
53324
- const firstParagraphOriginalEnd = firstParagraph.position.end;
53325
- if (icon && match) {
53326
- // Handle cases where heading and body are on the same line separated by a newline.
53327
- // Example: "> ⚠️ **Bold heading**\nBody text here"
53328
- const bodyChildren = splitParagraphAtNewline(firstParagraph);
53329
- const didSplit = bodyChildren !== null;
53330
- removeIconPrefix(firstParagraph, match.length);
53331
- const firstText = findFirst(firstParagraph);
53332
- const rawValue = firstText?.value ?? '';
53333
- const hasContent = rawValue.trim().length > 0 || firstParagraph.children.length > 1;
53334
- const empty = !hasContent;
53335
- const theme = themes[icon] || 'default';
53336
- if (hasContent || didSplit) {
53337
- const headingMatch = rawValue.match(/^(#{1,6})\s*/);
53338
- // # heading syntax is handled via direct AST manipulation so we can
53339
- // set the depth while preserving the original inline children (bold, etc.)
53340
- if (headingMatch) {
53341
- firstText.value = rawValue.slice(headingMatch[0].length);
53342
- const heading = wrapHeading(node);
53343
- heading.depth = headingMatch[1].length;
53344
- node.children[0] = heading;
53345
- node.children[0].position.start.offset += match.length;
53346
- node.children[0].position.start.column += match.length;
53359
+ if (depth !== 0)
53360
+ return null;
53361
+ return { content: content.slice(start, pos - 1), end: pos };
53362
+ }
53363
+ /**
53364
+ * Escapes problematic braces in content to prevent MDX expression parsing errors.
53365
+ * Handles three cases:
53366
+ * 1. Unbalanced braces (e.g., `{foo` without closing `}`)
53367
+ * 2. Paragraph-spanning expressions (e.g., `{\n\n}` where blank line splits paragraphs)
53368
+ * 3. Skips HTML elements to prevent backslashes appearing in output
53369
+ *
53370
+ */
53371
+ function escapeProblematicBraces(content) {
53372
+ // Skip HTML elements their content should never be escaped because
53373
+ // rehypeRaw parses them into hast elements, making `\` literal text in output
53374
+ const htmlElements = [];
53375
+ const safe = content.replace(/<([a-z][a-zA-Z0-9]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>/g, match => {
53376
+ const idx = htmlElements.length;
53377
+ htmlElements.push(match);
53378
+ return `___HTML_ELEM_${idx}___`;
53379
+ });
53380
+ const toEscape = new Set();
53381
+ // Convert to array of Unicode code points to handle emojis and multi-byte characters correctly
53382
+ const chars = Array.from(safe);
53383
+ let strDelim = null;
53384
+ let strEscaped = false;
53385
+ // Stack of open braces with their state
53386
+ const openStack = [];
53387
+ // Track position of last newline (outside strings) to detect blank lines
53388
+ let lastNewlinePos = -2; // -2 means no recent newline
53389
+ for (let i = 0; i < chars.length; i += 1) {
53390
+ const ch = chars[i];
53391
+ // Track string delimiters inside expressions to ignore braces within them
53392
+ if (openStack.length > 0) {
53393
+ if (strDelim) {
53394
+ if (strEscaped)
53395
+ strEscaped = false;
53396
+ else if (ch === '\\')
53397
+ strEscaped = true;
53398
+ else if (ch === strDelim)
53399
+ strDelim = null;
53400
+ // eslint-disable-next-line no-continue
53401
+ continue;
53347
53402
  }
53348
- else if (isMdxish) {
53349
- // Block-level title re-parsing is only needed for MDXish where HTML stays
53350
- // as raw nodes. In MDX, remarkMdx has already converted HTML to JSX AST
53351
- // nodes which toMarkdown can't serialize — and MDX doesn't need this
53352
- // block-level title handling anyway.
53353
- const headingText = toMarkdown({ type: 'root', children: [firstParagraph] }, {
53354
- extensions: toMarkdownExtensions,
53355
- })
53356
- .trim()
53357
- .replace(/^\\(?=[>#+\-*])/, '');
53358
- const parsedTitle = titleParser.parse(headingText);
53359
- const parsedFirstChild = parsedTitle.children[0];
53360
- // Block-level syntax ("> quote", "- list") produces non-paragraph nodes;
53361
- // inline text parses as a paragraph and falls through to wrapHeading().
53362
- if (parsedFirstChild && parsedFirstChild.type !== 'paragraph') {
53363
- // Strip positions from re-parsed nodes since they're relative to the heading text, not the original source
53364
- visit(parsedTitle, (n) => {
53365
- delete n.position;
53366
- });
53367
- const heading = wrapHeading(node);
53368
- heading.children = parsedTitle.children;
53369
- delete heading.position;
53370
- node.children[0] = heading;
53403
+ if (ch === '"' || ch === "'" || ch === '`') {
53404
+ strDelim = ch;
53405
+ // eslint-disable-next-line no-continue
53406
+ continue;
53407
+ }
53408
+ // Track newlines to detect blank lines (paragraph boundaries)
53409
+ if (ch === '\n') {
53410
+ // Check if this newline creates a blank line (only whitespace since last newline)
53411
+ if (lastNewlinePos >= 0) {
53412
+ const between = chars.slice(lastNewlinePos + 1, i).join('');
53413
+ if (/^[ \t]*$/.test(between)) {
53414
+ // This is a blank line - mark all open expressions as paragraph-spanning
53415
+ openStack.forEach(entry => {
53416
+ entry.hasBlankLine = true;
53417
+ });
53418
+ }
53371
53419
  }
53372
- else {
53373
- node.children[0] = wrapHeading(node);
53374
- node.children[0].position.start.offset += match.length;
53375
- node.children[0].position.start.column += match.length;
53420
+ lastNewlinePos = i;
53421
+ }
53422
+ }
53423
+ // Skip already-escaped braces (count preceding backslashes)
53424
+ if (ch === '{' || ch === '}') {
53425
+ let bs = 0;
53426
+ for (let j = i - 1; j >= 0 && chars[j] === '\\'; j -= 1)
53427
+ bs += 1;
53428
+ if (bs % 2 === 1) {
53429
+ // eslint-disable-next-line no-continue
53430
+ continue;
53431
+ }
53432
+ }
53433
+ if (ch === '{') {
53434
+ openStack.push({ pos: i, hasBlankLine: false });
53435
+ lastNewlinePos = -2; // Reset newline tracking for new expression
53436
+ }
53437
+ else if (ch === '}') {
53438
+ if (openStack.length > 0) {
53439
+ const entry = openStack.pop();
53440
+ // If expression spans paragraph boundary, escape both braces
53441
+ if (entry.hasBlankLine) {
53442
+ toEscape.add(entry.pos);
53443
+ toEscape.add(i);
53376
53444
  }
53377
53445
  }
53378
53446
  else {
53379
- node.children[0] = wrapHeading(node);
53380
- node.children[0].position.start.offset += match.length;
53381
- node.children[0].position.start.column += match.length;
53447
+ // Unbalanced closing brace (no matching open)
53448
+ toEscape.add(i);
53382
53449
  }
53383
53450
  }
53384
- // Insert body content as a separate paragraph after the heading
53385
- if (bodyChildren) {
53386
- const headingPosition = node.children[0].position;
53387
- node.children.splice(1, 0, {
53388
- type: 'paragraph',
53389
- children: bodyChildren,
53390
- ...(headingPosition && firstParagraphOriginalEnd
53391
- ? {
53392
- position: {
53393
- start: headingPosition.end,
53394
- end: firstParagraphOriginalEnd,
53395
- },
53396
- }
53397
- : {}),
53398
- });
53399
- }
53400
- Object.assign(node, {
53401
- type: NodeTypes.callout,
53402
- data: {
53403
- hName: 'Callout',
53404
- hProperties: {
53405
- icon,
53406
- ...(empty && { empty }),
53407
- theme,
53408
- },
53409
- },
53451
+ }
53452
+ // Any remaining open braces are unbalanced
53453
+ openStack.forEach(entry => toEscape.add(entry.pos));
53454
+ // If there are no problematic braces, return safe content as-is;
53455
+ // otherwise, escape each problematic `{` or `}` so MDX doesn't treat them as expressions.
53456
+ let result = toEscape.size === 0
53457
+ ? safe
53458
+ : chars.map((ch, i) => (toEscape.has(i) ? `\\${ch}` : ch)).join('');
53459
+ // Restore HTML elements
53460
+ if (htmlElements.length > 0) {
53461
+ result = result.replace(/___HTML_ELEM_(\d+)___/g, (_m, idx) => htmlElements[parseInt(idx, 10)]);
53462
+ }
53463
+ return result;
53464
+ }
53465
+ /**
53466
+ * Converts JSX attribute expressions (attribute={expression}) to HTML attributes (attribute="value").
53467
+ * Handles style objects (camelCase → kebab-case), className → class, and JSON stringifies objects.
53468
+ *
53469
+ * @param content
53470
+ * @param context
53471
+ * @returns Content with attribute expressions evaluated and converted to HTML attributes
53472
+ * @example
53473
+ * ```typescript
53474
+ * const context = { baseUrl: 'https://example.com' };
53475
+ * const input = '<a href={baseUrl}>Link</a>';
53476
+ * evaluateAttributeExpressions(input, context)
53477
+ * // Returns: '<a href="https://example.com">Link</a>'
53478
+ * ```
53479
+ */
53480
+ function evaluateAttributeExpressions(content, context, protectedCode) {
53481
+ const attrStartRegex = /(\w+)=\{/g;
53482
+ let result = '';
53483
+ let lastEnd = 0;
53484
+ let match = attrStartRegex.exec(content);
53485
+ while (match !== null) {
53486
+ const attributeName = match[1];
53487
+ const braceStart = match.index + match[0].length;
53488
+ const extracted = extractBalancedBraces(content, braceStart);
53489
+ if (extracted) {
53490
+ // The expression might contain template literals in MDX component tag props
53491
+ // E.g. <Component greeting={`Hello World!`} />
53492
+ // that is marked as inline code. So we need to restore the inline codes
53493
+ // in the expression to evaluate it
53494
+ let expression = extracted.content;
53495
+ if (protectedCode) {
53496
+ expression = restoreInlineCode(expression, protectedCode);
53497
+ }
53498
+ const fullMatchEnd = extracted.end;
53499
+ result += content.slice(lastEnd, match.index);
53500
+ try {
53501
+ const evalResult = evaluateExpression(expression, context);
53502
+ if (typeof evalResult === 'object' && evalResult !== null) {
53503
+ if (attributeName === 'style') {
53504
+ const cssString = Object.entries(evalResult)
53505
+ .map(([key, value]) => {
53506
+ const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
53507
+ return `${cssKey}: ${value}`;
53508
+ })
53509
+ .join('; ');
53510
+ result += `style="${cssString}"`;
53511
+ }
53512
+ else {
53513
+ // These are arrays / objects attribute values
53514
+ // Mark JSON-serialized values with a prefix so they can be parsed back correctly
53515
+ const jsonValue = escapeHtmlAttribute(JSON_VALUE_MARKER + JSON.stringify(evalResult));
53516
+ // Use double quotes so that multi-paragraph values are not split into multiple attributes by the processors
53517
+ result += `${attributeName}="${jsonValue}"`;
53518
+ }
53519
+ }
53520
+ else if (attributeName === 'className') {
53521
+ // Escape special characters so that it doesn't break and split the attribute value to nodes
53522
+ // This will be restored later in the pipeline
53523
+ result += `class="${escapeHtmlAttribute(String(evalResult))}"`;
53524
+ }
53525
+ else {
53526
+ result += `${attributeName}="${escapeHtmlAttribute(String(evalResult))}"`;
53527
+ }
53528
+ }
53529
+ catch (_error) {
53530
+ result += content.slice(match.index, fullMatchEnd);
53531
+ }
53532
+ lastEnd = fullMatchEnd;
53533
+ attrStartRegex.lastIndex = fullMatchEnd;
53534
+ }
53535
+ match = attrStartRegex.exec(content);
53536
+ }
53537
+ result += content.slice(lastEnd);
53538
+ return result;
53539
+ }
53540
+ /**
53541
+ * Preprocesses JSX-like expressions in markdown before parsing.
53542
+ * Inline expressions are handled separately; attribute expressions are processed here.
53543
+ *
53544
+ * @param content
53545
+ * @param context
53546
+ * @returns Preprocessed content ready for markdown parsing
53547
+ */
53548
+ function preprocessJSXExpressions(content, context = {}) {
53549
+ // Step 0: Base64 encode HTMLBlock content
53550
+ let processed = protectHTMLBlockContent(content);
53551
+ // Step 1: Protect code blocks and inline code
53552
+ const { protectedCode, protectedContent } = protectCodeBlocks(processed);
53553
+ // Step 2: Evaluate attribute expressions (JSX attribute syntax: href={baseUrl})
53554
+ // For inline expressions, we use a library to parse the expression & evaluate it later
53555
+ // For attribute expressions, it was difficult to use a library to parse them, so do it manually
53556
+ processed = evaluateAttributeExpressions(protectedContent, context, protectedCode);
53557
+ // Step 3: Escape problematic braces to prevent MDX expression parsing errors
53558
+ // This handles both unbalanced braces and paragraph-spanning expressions in one pass
53559
+ processed = escapeProblematicBraces(processed);
53560
+ // Step 4: Restore protected code blocks
53561
+ processed = restoreCodeBlocks(processed, protectedCode);
53562
+ return processed;
53563
+ }
53564
+
53565
+ ;// ./processor/utils.ts
53566
+
53567
+
53568
+
53569
+ /**
53570
+ * Formats the hProperties of a node as a string, so they can be compiled back into JSX/MDX.
53571
+ * This currently sets all the values to a string since we process/compile the MDX on the fly
53572
+ * through the editor, and it'll throw errors over malformed JSX. TODO: fix this.
53573
+ *
53574
+ * @template T
53575
+ * @param {Node} node
53576
+ * @returns {string} formatted hProperties as JSX attributes
53577
+ */
53578
+ const formatHProps = (node) => {
53579
+ const hProps = getHProps(node);
53580
+ return formatProps(hProps);
53581
+ };
53582
+ /**
53583
+ * Formats an object of props as a string.
53584
+ *
53585
+ * @param {Object} props
53586
+ * @returns {string}
53587
+ */
53588
+ const formatProps = (props) => {
53589
+ const keys = Object.keys(props);
53590
+ return keys.map(key => `${key}="${props[key]}"`).join(' ');
53591
+ };
53592
+ /**
53593
+ * Returns the hProperties of a node.
53594
+ *
53595
+ * @template T
53596
+ * @param {Node} node
53597
+ * @returns {T} hProperties
53598
+ */
53599
+ const getHProps = (node) => {
53600
+ const hProps = node.data?.hProperties || {};
53601
+ return hProps;
53602
+ };
53603
+ /**
53604
+ * Returns array of hProperty keys.
53605
+ *
53606
+ * @template T
53607
+ * @param {Node} node
53608
+ * @returns {Array} array of hProperty keys
53609
+ */
53610
+ const getHPropKeys = (node) => {
53611
+ const hProps = getHProps(node);
53612
+ return Object.keys(hProps) || [];
53613
+ };
53614
+ /**
53615
+ * Gets the attributes of an MDX element and returns them as an object of hProperties.
53616
+ *
53617
+ * @template T
53618
+ * @param {(MdxJsxFlowElement | MdxJsxTextElement)} jsx
53619
+ * @returns {T} object of hProperties
53620
+ */
53621
+ const getAttrs = (jsx) => jsx.attributes.reduce((memo, attr) => {
53622
+ if ('name' in attr) {
53623
+ if (typeof attr.value === 'string') {
53624
+ if (attr.value.startsWith(JSON_VALUE_MARKER)) {
53625
+ try {
53626
+ memo[attr.name] = JSON.parse(attr.value.slice(JSON_VALUE_MARKER.length));
53627
+ }
53628
+ catch {
53629
+ memo[attr.name] = attr.value;
53630
+ }
53631
+ }
53632
+ else {
53633
+ memo[attr.name] = attr.value;
53634
+ }
53635
+ }
53636
+ else if (attr.value === null) {
53637
+ memo[attr.name] = true;
53638
+ }
53639
+ else if (attr.value.value !== 'undefined') {
53640
+ memo[attr.name] = JSON.parse(attr.value.value);
53641
+ }
53642
+ }
53643
+ return memo;
53644
+ }, {});
53645
+ /**
53646
+ * Gets the children of an MDX element and returns them as an array of Text nodes.
53647
+ * Currently only being used by the HTML Block component, which only expects a single text node.
53648
+ *
53649
+ * @template T
53650
+ * @param {(MdxJsxFlowElement | MdxJsxTextElement)} jsx
53651
+ * @returns {Array} array of child text nodes
53652
+ */
53653
+ const getChildren = (jsx) => jsx.children.reduce((memo, child, i) => {
53654
+ memo[i] = {
53655
+ type: 'text',
53656
+ value: child.value,
53657
+ position: child.position,
53658
+ };
53659
+ return memo;
53660
+ }, []);
53661
+ /**
53662
+ * Tests if a node is an MDX element.
53663
+ * TODO: Make this more extensible to all types of nodes. isElement(node, 'type' or ['type1', 'type2']), say
53664
+ *
53665
+ * @param {Node} node
53666
+ * @returns {(node is MdxJsxFlowElement | MdxJsxTextElement | MdxjsEsm)}
53667
+ */
53668
+ const isMDXElement = (node) => {
53669
+ return ['mdxJsxFlowElement', 'mdxJsxTextElement'].includes(node.type);
53670
+ };
53671
+ /**
53672
+ * Tests if a node is an MDX ESM element (i.e. import or export).
53673
+ *
53674
+ * @param {Node} node
53675
+ * @returns {boolean}
53676
+ */
53677
+ const isMDXEsm = (node) => {
53678
+ return node.type === 'mdxjsEsm';
53679
+ };
53680
+ /**
53681
+ * Takes an HTML string and formats it for display in the editor. Removes leading/trailing newlines
53682
+ * and unindents the HTML.
53683
+ *
53684
+ * @param {string} html - HTML content from template literal
53685
+ * @returns {string} processed HTML
53686
+ */
53687
+ function formatHtmlForMdxish(html) {
53688
+ // Remove leading/trailing backticks if present, since they're used to keep the HTML
53689
+ // from being parsed prematurely
53690
+ let processed = html;
53691
+ if (processed.startsWith('`') && processed.endsWith('`')) {
53692
+ processed = processed.slice(1, -1);
53693
+ }
53694
+ // Removes the leading/trailing newlines
53695
+ let cleaned = processed.replace(/^\s*\n|\n\s*$/g, '');
53696
+ // Convert literal \n sequences to actual newlines BEFORE processing backticks
53697
+ // This prevents the backtick unescaping regex from incorrectly matching \n sequences
53698
+ cleaned = cleaned.replace(/\\n/g, '\n');
53699
+ // Unescape backticks: \` -> ` (users escape backticks in template literals)
53700
+ // Handle both cases: \` (adjacent) and \ followed by ` (split by markdown parser)
53701
+ cleaned = cleaned.replace(/\\`/g, '`');
53702
+ // Also handle case where backslash and backtick got separated by markdown parsing
53703
+ // Pattern: backslash followed by any characters (but not \n which we already handled), then a backtick
53704
+ // This handles cases like: \example` -> `example` (replacing \ with ` at start)
53705
+ // Exclude \n sequences to avoid matching them incorrectly
53706
+ cleaned = cleaned.replace(/\\([^`\\n]*?)`/g, '`$1`');
53707
+ // Fix case where markdown parser consumed one backtick from triple backticks
53708
+ // Pattern: `` followed by a word (like ``javascript) should be ```javascript
53709
+ // This handles cases where code fences were parsed and one backtick was lost
53710
+ cleaned = cleaned.replace(/<(\w+[^>]*)>``(\w+)/g, '<$1>```$2');
53711
+ // Unescape dollar signs: \$ -> $ (users escape $ in template literals to prevent interpolation)
53712
+ cleaned = cleaned.replace(/\\\$/g, '$');
53713
+ return cleaned;
53714
+ }
53715
+ /**
53716
+ * Takes an HTML string and formats it for display in the editor. Removes leading/trailing newlines
53717
+ * and unindents the HTML.
53718
+ *
53719
+ * @param {string} html
53720
+ * @returns {string} formatted HTML
53721
+ */
53722
+ const formatHTML = (html) => {
53723
+ // Remove leading/trailing backticks if present, since they're used to keep the HTML
53724
+ // from being parsed prematurely
53725
+ if (html.startsWith('`') && html.endsWith('`')) {
53726
+ // eslint-disable-next-line no-param-reassign
53727
+ html = html.slice(1, -1);
53728
+ }
53729
+ // Removes the leading/trailing newlines
53730
+ const cleaned = html.replace(/^\s*\n|\n\s*$/g, '');
53731
+ // // Get the number of spaces in the first line to determine the tab size
53732
+ // const tab = cleaned.match(/^\s*/)[0].length;
53733
+ // // Remove the first indentation level from each line
53734
+ // const tabRegex = new RegExp(`^\\s{${tab}}`, 'gm');
53735
+ // const unindented = cleaned.replace(tabRegex, '');
53736
+ return cleaned;
53737
+ };
53738
+ /**
53739
+ * Reformat HTML for the markdown/mdx by adding an indentation to each line. This assures that the
53740
+ * HTML is indentend properly within the HTMLBlock component when rendered in the markdown/mdx.
53741
+ *
53742
+ * @param {string} html
53743
+ * @param {number} [indent=2]
53744
+ * @returns {string} re-formatted HTML
53745
+ */
53746
+ const reformatHTML = (html) => {
53747
+ // Remove leading/trailing newlines
53748
+ const cleaned = html.replace(/^\s*\n|\n\s*$/g, '').replaceAll(/(?<!\\(\\\\)*)`/g, '\\`');
53749
+ // // Create a tab/indent with the specified number of spaces
53750
+ // const tab = ' '.repeat(indent);
53751
+ // // Indent each line of the HTML (converts to an array, indents each line, then joins back)
53752
+ // const indented = cleaned.split('\n').map((line: string) => `${tab}${line}`).join('\n');
53753
+ return cleaned;
53754
+ };
53755
+ const toAttributes = (object, keys = []) => {
53756
+ const attributes = [];
53757
+ Object.entries(object).forEach(([name, v]) => {
53758
+ if (keys.length > 0 && !keys.includes(name))
53759
+ return;
53760
+ let value;
53761
+ if (typeof v === 'undefined' || v === null || v === '' || v === false) {
53762
+ return;
53763
+ }
53764
+ else if (typeof v === 'string') {
53765
+ value = v;
53766
+ }
53767
+ else {
53768
+ /* values can be null, undefined, string, or a expression, eg:
53769
+ *
53770
+ * ```
53771
+ * <Image src="..." border={false} size={width - 20} />
53772
+ * ```
53773
+ *
53774
+ * Parsing the expression seems to only be done by the library
53775
+ * `mdast-util-mdx-jsx`, and so the most straight forward way to parse
53776
+ * the expression and get the appropriate AST is with our `mdast`
53777
+ * function.
53778
+ */
53779
+ const proxy = lib_mdast(`{${v}}`);
53780
+ const data = proxy.children[0].data;
53781
+ value = {
53782
+ type: 'mdxJsxAttributeValueExpression',
53783
+ value: v.toString(),
53784
+ data,
53785
+ };
53786
+ }
53787
+ attributes.push({
53788
+ type: 'mdxJsxAttribute',
53789
+ name,
53790
+ value,
53791
+ });
53792
+ });
53793
+ return attributes;
53794
+ };
53795
+ /**
53796
+ * Checks if a named export exists in the MDX tree. Accepts either an mdast or
53797
+ * a hast tree.
53798
+ *
53799
+ * example:
53800
+ * ```
53801
+ * const mdx = `export const Foo = 'bar';`
53802
+ *
53803
+ * hasNamedExport(mdast(mdx), 'Foo') => true
53804
+ * ```
53805
+ *
53806
+ */
53807
+ const hasNamedExport = (tree, name) => {
53808
+ let hasExport = false;
53809
+ // eslint-disable-next-line consistent-return
53810
+ visit(tree, 'mdxjsEsm', node => {
53811
+ if ('declaration' in node.data.estree.body[0] &&
53812
+ node.data.estree.body[0].declaration.type === 'VariableDeclaration') {
53813
+ const { declarations } = node.data.estree.body[0].declaration;
53814
+ hasExport = !!declarations.find(declaration => 'name' in declaration.id && declaration.id.name === name);
53815
+ return hasExport ? EXIT : CONTINUE;
53816
+ }
53817
+ });
53818
+ return hasExport;
53819
+ };
53820
+ /* Example mdast structures to find first export name in a mdxjsEsm node:
53821
+ There are three types of export declarations that we need to consider:
53822
+ 1. VARIABLE DECLARATION
53823
+ "type": "mdxjsEsm",
53824
+ "value": "export const Foo = () => <div>Hello world</div>\nexport const Bar = () => <div>hello darkness my old friend</div>",
53825
+ "data": {
53826
+ "estree": {
53827
+ "type": "Program",
53828
+ "body": [
53829
+ {
53830
+ "type": "ExportNamedDeclaration",
53831
+ "declaration": {
53832
+ "type": "VariableDeclaration",
53833
+ "declarations": [
53834
+ {
53835
+ "type": "VariableDeclarator",
53836
+ "id": {
53837
+ "type": "Identifier",
53838
+ "name": "Foo" // --------> This is the export name
53839
+ },
53840
+ ...
53841
+
53842
+ 2/3. FUNCTION DECLARATION & CLASS DECLARATION
53843
+ "estree": {
53844
+ "type": "Program",
53845
+ "body": [
53846
+ {
53847
+ "type": "ExportNamedDeclaration",
53848
+ "declaration": {
53849
+ "type": "ClassDeclaration" | "FunctionDeclaration",
53850
+ "id": {
53851
+ "type": "Identifier",
53852
+ "name": "Foo" // --------> This is the export name
53853
+ },
53854
+ */
53855
+ const getExports = (tree) => {
53856
+ const set = new Set();
53857
+ visit(tree, isMDXEsm, (node) => {
53858
+ // Once inside an mdxjsEsm node, we need to check for one or more declared exports within data.estree.body
53859
+ // 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
53860
+ const body = node.data?.estree.body;
53861
+ if (!body)
53862
+ return;
53863
+ body.forEach(child => {
53864
+ if (child.type === 'ExportNamedDeclaration') {
53865
+ // There are three types of ExportNamedDeclaration that we need to consider: VariableDeclaration, FunctionDeclaration, ClassDeclaration
53866
+ const declaration = child.declaration;
53867
+ // FunctionDeclaration and ClassDeclaration have the same structure
53868
+ if (declaration.type !== 'VariableDeclaration') {
53869
+ // Note: declaration.id.type is always 'Identifier' for FunctionDeclarations and ClassDeclarations
53870
+ set.add(declaration.id.name);
53871
+ }
53872
+ else {
53873
+ declaration.declarations.forEach(dec => {
53874
+ const id = dec.id;
53875
+ if (id.type === 'Identifier') {
53876
+ set.add(id.name);
53877
+ }
53878
+ });
53879
+ }
53880
+ }
53881
+ });
53882
+ });
53883
+ return Array.from(set);
53884
+ };
53885
+
53886
+ ;// ./processor/compile/compatibility.ts
53887
+
53888
+
53889
+
53890
+ /*
53891
+ * Converts a (remark < v9) html node to a JSX string.
53892
+ *
53893
+ * First we replace html comments with the JSX equivalent. Then, we parse that
53894
+ * as html, and serialize it back as xml!
53895
+ *
53896
+ */
53897
+ const compileHtml = (node) => {
53898
+ const string = node.value.replaceAll(/<!--(.*)-->/gms, '{/*$1*/}');
53899
+ return string;
53900
+ };
53901
+ const figureToImageBlock = (node) => {
53902
+ const { align, border, width, src, url, alt, title, ...image } = node.children.find((child) => child.type === 'image');
53903
+ const { className } = image.data.hProperties;
53904
+ const figcaption = node.children.find((child) => child.type === 'figcaption');
53905
+ const caption = figcaption ? toMarkdown(figcaption.children).trim() : null;
53906
+ const attributes = {
53907
+ ...(align && { align }),
53908
+ ...(alt && { alt }),
53909
+ ...(className && { border: className === 'border' }),
53910
+ ...(border && { border }),
53911
+ ...(caption && { caption }),
53912
+ ...(title && { title }),
53913
+ ...(width && { width }),
53914
+ src: src || url,
53915
+ };
53916
+ return `<Image ${formatProps(attributes)} />`;
53917
+ };
53918
+ const embedToEmbedBlock = (node) => {
53919
+ const { html, ...embed } = node.data.hProperties;
53920
+ const attributes = {
53921
+ ...embed,
53922
+ ...(html && { html: encodeURIComponent(html) }),
53923
+ };
53924
+ return `<Embed ${formatProps(attributes)} />`;
53925
+ };
53926
+ const compatibility = (node) => {
53927
+ switch (node.type) {
53928
+ case NodeTypes.glossary: {
53929
+ // Glossary terms will no longer be serialized as special node types in the Editor but we want to ensure that we compile historical
53930
+ // data correctly
53931
+ const term = node.data?.hProperties?.term || node.children[0].value;
53932
+ return `<Glossary>${term}</Glossary>`;
53933
+ }
53934
+ case NodeTypes.reusableContent:
53935
+ return `<${node.tag} />`;
53936
+ case 'html':
53937
+ return compileHtml(node);
53938
+ case 'escape':
53939
+ return `\\${node.value}`;
53940
+ case 'figure':
53941
+ return figureToImageBlock(node);
53942
+ case 'embed':
53943
+ return embedToEmbedBlock(node);
53944
+ case 'i':
53945
+ return `:${node.data.hProperties.className[1]}:`;
53946
+ case 'yaml':
53947
+ return `---\n${node.value}\n---`;
53948
+ default:
53949
+ throw new Error('Unhandled node type!');
53950
+ }
53951
+ };
53952
+ /* harmony default export */ const compile_compatibility = (compatibility);
53953
+
53954
+ ;// ./processor/compile/gemoji.ts
53955
+ const gemoji = (node) => `:${node.name}:`;
53956
+ /* harmony default export */ const compile_gemoji = (gemoji);
53957
+
53958
+ ;// ./processor/compile/variable.ts
53959
+ const variable = (node) => `{user.${node.data?.hProperties?.name || ''}}`;
53960
+ /* harmony default export */ const compile_variable = (variable);
53961
+
53962
+ ;// ./processor/transform/extract-text.ts
53963
+ /**
53964
+ * Extracts text content from a single AST node recursively.
53965
+ * Works with both MDAST and HAST-like node structures.
53966
+ *
53967
+ * Placed this outside of the utils.ts file to avoid circular dependencies.
53968
+ *
53969
+ * @param node - The node to extract text from (can be MDAST Node or HAST-like structure)
53970
+ * @returns The concatenated text content
53971
+ */
53972
+ const extractText = (node) => {
53973
+ if (typeof node.value === 'string') {
53974
+ return node.value;
53975
+ }
53976
+ // When a blockquote contains only an image (no text), treat it as having content
53977
+ // so the blockquote is no longer treated as empty and preserved correctly.
53978
+ if (node.type === 'image') {
53979
+ return typeof node.alt === 'string' && node.alt ? node.alt : '[image]';
53980
+ }
53981
+ if (node.children && Array.isArray(node.children)) {
53982
+ return node.children
53983
+ .map(child => {
53984
+ if (child && typeof child === 'object' && 'type' in child) {
53985
+ return extractText(child);
53986
+ }
53987
+ return '';
53988
+ })
53989
+ .join('');
53990
+ }
53991
+ return '';
53992
+ };
53993
+
53994
+ ;// ./processor/transform/callouts.ts
53995
+
53996
+
53997
+
53998
+
53999
+
54000
+
54001
+
54002
+
54003
+
54004
+
54005
+
54006
+
54007
+
54008
+
54009
+
54010
+ const titleParser = unified().use(remarkParse).use(remarkGfm);
54011
+ // The title paragraph may contain custom AST nodes that `toMarkdown` doesn't
54012
+ // natively understand
54013
+ const toMarkdownExtensions = [
54014
+ gfmToMarkdown(),
54015
+ // For mdx variable syntaxes (e.g., {user.name})
54016
+ mdxExpressionToMarkdown(),
54017
+ // Important: This is required and would crash the parser if there's no variable, gemoji, or fa-icon node handler
54018
+ {
54019
+ handlers: {
54020
+ [NodeTypes.variable]: compile_variable,
54021
+ [NodeTypes.emoji]: compile_gemoji,
54022
+ [NodeTypes.i]: compile_compatibility,
54023
+ },
54024
+ },
54025
+ ];
54026
+ const callouts_regex = `^(${emoji_regex().source}|⚠)(\\s+|$)`;
54027
+ const findFirst = (node) => {
54028
+ if ('children' in node)
54029
+ return findFirst(node.children[0]);
54030
+ if (node.type === 'text')
54031
+ return node;
54032
+ return null;
54033
+ };
54034
+ const wrapHeading = (node) => {
54035
+ const firstChild = node.children[0];
54036
+ return {
54037
+ type: 'heading',
54038
+ depth: 3,
54039
+ children: ('children' in firstChild ? firstChild.children : []),
54040
+ position: {
54041
+ start: firstChild.position.start,
54042
+ end: firstChild.position.end,
54043
+ },
54044
+ };
54045
+ };
54046
+ /**
54047
+ * Checks if a blockquote matches the expected callout structure:
54048
+ * blockquote > paragraph > text node
54049
+ */
54050
+ const isCalloutStructure = (node) => {
54051
+ const firstChild = node.children?.[0];
54052
+ if (!firstChild || firstChild.type !== 'paragraph')
54053
+ return false;
54054
+ if (!('children' in firstChild))
54055
+ return false;
54056
+ const firstTextChild = firstChild.children?.[0];
54057
+ return firstTextChild?.type === 'text';
54058
+ };
54059
+ /**
54060
+ * Finds the first text node containing a newline in a paragraph's children.
54061
+ * Returns the index and the newline position within that text node.
54062
+ */
54063
+ const findNewlineInParagraph = (paragraph) => {
54064
+ for (let i = 0; i < paragraph.children.length; i += 1) {
54065
+ const child = paragraph.children[i];
54066
+ if (child.type === 'text' && typeof child.value === 'string') {
54067
+ const newlineIndex = child.value.indexOf('\n');
54068
+ if (newlineIndex !== -1) {
54069
+ return { index: i, newlineIndex };
54070
+ }
54071
+ }
54072
+ }
54073
+ return null;
54074
+ };
54075
+ /**
54076
+ * Splits a paragraph at the first newline, separating heading content (before \n)
54077
+ * from body content (after \n). Mutates the paragraph to contain only heading children.
54078
+ */
54079
+ const splitParagraphAtNewline = (paragraph) => {
54080
+ const splitPoint = findNewlineInParagraph(paragraph);
54081
+ if (!splitPoint)
54082
+ return null;
54083
+ const { index, newlineIndex } = splitPoint;
54084
+ const originalChildren = paragraph.children;
54085
+ const textNode = originalChildren[index];
54086
+ const beforeNewline = textNode.value.slice(0, newlineIndex);
54087
+ const afterNewline = textNode.value.slice(newlineIndex + 1);
54088
+ // Split paragraph: heading = children[0..index-1] + text before newline
54089
+ const headingChildren = originalChildren.slice(0, index);
54090
+ if (beforeNewline.length > 0 || headingChildren.length === 0) {
54091
+ headingChildren.push({ type: 'text', value: beforeNewline });
54092
+ }
54093
+ paragraph.children = headingChildren;
54094
+ // Body = text after newline + remaining children from original array
54095
+ const bodyChildren = [];
54096
+ if (afterNewline.length > 0) {
54097
+ bodyChildren.push({ type: 'text', value: afterNewline });
54098
+ }
54099
+ bodyChildren.push(...originalChildren.slice(index + 1));
54100
+ return bodyChildren.length > 0 ? bodyChildren : null;
54101
+ };
54102
+ /**
54103
+ * Removes the icon/match prefix from the first text node in a paragraph.
54104
+ * This is needed to clean up the raw AST after we've extracted the icon.
54105
+ */
54106
+ const removeIconPrefix = (paragraph, prefixLength) => {
54107
+ const firstTextNode = findFirst(paragraph);
54108
+ if (firstTextNode && 'value' in firstTextNode && typeof firstTextNode.value === 'string') {
54109
+ firstTextNode.value = firstTextNode.value.slice(prefixLength);
54110
+ }
54111
+ };
54112
+ const processBlockquote = (node, index, parent, isMdxish = false) => {
54113
+ if (!isCalloutStructure(node)) {
54114
+ // Only stringify empty blockquotes (no extractable text content)
54115
+ // Preserve blockquotes with actual content (e.g., headings, lists, etc.)
54116
+ const content = extractText(node);
54117
+ const isEmpty = !content || content.trim() === '';
54118
+ if (isEmpty && index !== undefined && parent) {
54119
+ const textNode = {
54120
+ type: 'text',
54121
+ value: '>',
54122
+ };
54123
+ const paragraphNode = {
54124
+ type: 'paragraph',
54125
+ children: [textNode],
54126
+ position: node.position,
54127
+ };
54128
+ parent.children.splice(index, 1, paragraphNode);
54129
+ }
54130
+ return;
54131
+ }
54132
+ // isCalloutStructure ensures node.children[0] is a Paragraph with children
54133
+ const firstParagraph = node.children[0];
54134
+ const startText = lib_plain(firstParagraph).toString();
54135
+ const [match, icon] = startText.match(callouts_regex) || [];
54136
+ const firstParagraphOriginalEnd = firstParagraph.position.end;
54137
+ if (icon && match) {
54138
+ // Handle cases where heading and body are on the same line separated by a newline.
54139
+ // Example: "> ⚠️ **Bold heading**\nBody text here"
54140
+ const bodyChildren = splitParagraphAtNewline(firstParagraph);
54141
+ const didSplit = bodyChildren !== null;
54142
+ removeIconPrefix(firstParagraph, match.length);
54143
+ const firstText = findFirst(firstParagraph);
54144
+ const rawValue = firstText?.value ?? '';
54145
+ const hasContent = rawValue.trim().length > 0 || firstParagraph.children.length > 1;
54146
+ const empty = !hasContent;
54147
+ const theme = themes[icon] || 'default';
54148
+ if (hasContent || didSplit) {
54149
+ const headingMatch = rawValue.match(/^(#{1,6})\s*/);
54150
+ // # heading syntax is handled via direct AST manipulation so we can
54151
+ // set the depth while preserving the original inline children (bold, etc.)
54152
+ if (headingMatch) {
54153
+ firstText.value = rawValue.slice(headingMatch[0].length);
54154
+ const heading = wrapHeading(node);
54155
+ heading.depth = headingMatch[1].length;
54156
+ node.children[0] = heading;
54157
+ node.children[0].position.start.offset += match.length;
54158
+ node.children[0].position.start.column += match.length;
54159
+ }
54160
+ else if (isMdxish) {
54161
+ // Block-level title re-parsing is only needed for MDXish where HTML stays
54162
+ // as raw nodes. In MDX, remarkMdx has already converted HTML to JSX AST
54163
+ // nodes which toMarkdown can't serialize — and MDX doesn't need this
54164
+ // block-level title handling anyway.
54165
+ const headingText = toMarkdown({ type: 'root', children: [firstParagraph] }, {
54166
+ extensions: toMarkdownExtensions,
54167
+ })
54168
+ .trim()
54169
+ .replace(/^\\(?=[>#+\-*])/, '');
54170
+ const parsedTitle = titleParser.parse(headingText);
54171
+ const parsedFirstChild = parsedTitle.children[0];
54172
+ // Block-level syntax ("> quote", "- list") produces non-paragraph nodes;
54173
+ // inline text parses as a paragraph and falls through to wrapHeading().
54174
+ if (parsedFirstChild && parsedFirstChild.type !== 'paragraph') {
54175
+ // Strip positions from re-parsed nodes since they're relative to the heading text, not the original source
54176
+ visit(parsedTitle, (n) => {
54177
+ delete n.position;
54178
+ });
54179
+ const heading = wrapHeading(node);
54180
+ heading.children = parsedTitle.children;
54181
+ delete heading.position;
54182
+ node.children[0] = heading;
54183
+ }
54184
+ else {
54185
+ node.children[0] = wrapHeading(node);
54186
+ node.children[0].position.start.offset += match.length;
54187
+ node.children[0].position.start.column += match.length;
54188
+ }
54189
+ }
54190
+ else {
54191
+ node.children[0] = wrapHeading(node);
54192
+ node.children[0].position.start.offset += match.length;
54193
+ node.children[0].position.start.column += match.length;
54194
+ }
54195
+ }
54196
+ // Insert body content as a separate paragraph after the heading
54197
+ if (bodyChildren) {
54198
+ const headingPosition = node.children[0].position;
54199
+ node.children.splice(1, 0, {
54200
+ type: 'paragraph',
54201
+ children: bodyChildren,
54202
+ ...(headingPosition && firstParagraphOriginalEnd
54203
+ ? {
54204
+ position: {
54205
+ start: headingPosition.end,
54206
+ end: firstParagraphOriginalEnd,
54207
+ },
54208
+ }
54209
+ : {}),
54210
+ });
54211
+ }
54212
+ Object.assign(node, {
54213
+ type: NodeTypes.callout,
54214
+ data: {
54215
+ hName: 'Callout',
54216
+ hProperties: {
54217
+ icon,
54218
+ ...(empty && { empty }),
54219
+ theme,
54220
+ },
54221
+ },
53410
54222
  });
53411
54223
  }
53412
54224
  };
@@ -70489,857 +71301,121 @@ const emojiToName = {
70489
71301
  '🇾🇪': 'yemen',
70490
71302
  '🇾🇹': 'mayotte',
70491
71303
  '🇿🇦': 'south_africa',
70492
- '🇿🇲': 'zambia',
70493
- '🇿🇼': 'zimbabwe',
70494
- '🏴󠁧󠁢󠁥󠁮󠁧󠁿': 'england',
70495
- '🏴󠁧󠁢󠁳󠁣󠁴󠁿': 'scotland',
70496
- '🏴󠁧󠁢󠁷󠁬󠁳󠁿': 'wales'
70497
- }
70498
-
70499
- ;// ./lib/owlmoji.ts
70500
-
70501
- const owlmoji = [
70502
- {
70503
- emoji: '', // This `emoji` property doesn't get consumed, but is required for type consistency
70504
- names: ['owlbert'],
70505
- tags: ['owlbert'],
70506
- description: 'an owlbert for any occasion',
70507
- category: 'ReadMe',
70508
- },
70509
- {
70510
- emoji: '',
70511
- names: ['owlbert-books'],
70512
- tags: ['owlbert'],
70513
- description: 'owlbert carrying books',
70514
- category: 'ReadMe',
70515
- },
70516
- {
70517
- emoji: '',
70518
- names: ['owlbert-mask'],
70519
- tags: ['owlbert'],
70520
- description: 'owlbert with a respirator',
70521
- category: 'ReadMe',
70522
- },
70523
- {
70524
- emoji: '',
70525
- names: ['owlbert-reading'],
70526
- tags: ['owlbert'],
70527
- description: 'owlbert reading',
70528
- category: 'ReadMe',
70529
- },
70530
- {
70531
- emoji: '',
70532
- names: ['owlbert-thinking'],
70533
- tags: ['owlbert'],
70534
- description: 'owlbert thinking',
70535
- category: 'ReadMe',
70536
- },
70537
- ];
70538
- const owlmojiNames = owlmoji.flatMap(emoji => emoji.names);
70539
- class Owlmoji {
70540
- static kind = (name) => {
70541
- if (name in nameToEmoji)
70542
- return 'gemoji';
70543
- else if (name.match(/^fa-/))
70544
- return 'fontawesome';
70545
- else if (owlmojiNames.includes(name))
70546
- return 'owlmoji';
70547
- return null;
70548
- };
70549
- static nameToEmoji = nameToEmoji;
70550
- static owlmoji = gemoji_gemoji.concat(owlmoji);
70551
- }
70552
-
70553
- ;// ./processor/transform/gemoji+.ts
70554
-
70555
-
70556
-
70557
- const gemoji_regex = /(?<=^|\s):(?<name>\+1|[-\w]+):/g;
70558
- const gemojiReplacer = (_, name) => {
70559
- switch (Owlmoji.kind(name)) {
70560
- case 'gemoji': {
70561
- const node = {
70562
- type: NodeTypes.emoji,
70563
- value: Owlmoji.nameToEmoji[name],
70564
- name,
70565
- };
70566
- return node;
70567
- }
70568
- case 'fontawesome': {
70569
- const node = {
70570
- type: NodeTypes.i,
70571
- value: name,
70572
- data: {
70573
- hName: 'i',
70574
- hProperties: {
70575
- className: ['fa-regular', name],
70576
- },
70577
- },
70578
- };
70579
- return node;
70580
- }
70581
- case 'owlmoji': {
70582
- const node = {
70583
- type: 'image',
70584
- title: `:${name}:`,
70585
- alt: `:${name}:`,
70586
- url: `/public/img/emojis/${name}.png`,
70587
- data: {
70588
- hProperties: {
70589
- className: 'emoji',
70590
- align: 'absmiddle',
70591
- height: '20',
70592
- width: '20',
70593
- },
70594
- },
70595
- };
70596
- return node;
70597
- }
70598
- default:
70599
- return false;
70600
- }
70601
- };
70602
- const gemojiTransformer = () => (tree) => {
70603
- findAndReplace(tree, [gemoji_regex, gemojiReplacer]);
70604
- return tree;
70605
- };
70606
- /* harmony default export */ const gemoji_ = (gemojiTransformer);
70607
-
70608
- ;// ./lib/mdast.ts
70609
-
70610
- const mdast = (text, opts = {}) => {
70611
- const processor = ast_processor(opts);
70612
- const tree = processor.parse(text);
70613
- return processor.runSync(tree);
70614
- };
70615
- /* harmony default export */ const lib_mdast = (mdast);
70616
-
70617
- ;// ./lib/utils/mdxish/protect-code-blocks.ts
70618
- /**
70619
- * Replaces code blocks and inline code with placeholders to protect them from preprocessing.
70620
- *
70621
- * @param content - The markdown content to process
70622
- * @returns Object containing protected content and arrays of original code blocks
70623
- * @example
70624
- * ```typescript
70625
- * const input = 'Text with `inline code` and ```fenced block```';
70626
- * protectCodeBlocks(input)
70627
- * // Returns: {
70628
- * // protectedCode: {
70629
- * // codeBlocks: ['```fenced block```'],
70630
- * // inlineCode: ['`inline code`']
70631
- * // },
70632
- * // protectedContent: 'Text with ___INLINE_CODE_0___ and ___CODE_BLOCK_0___'
70633
- * // }
70634
- * ```
70635
- */
70636
- function protectCodeBlocks(content) {
70637
- const codeBlocks = [];
70638
- const inlineCode = [];
70639
- let protectedContent = '';
70640
- let remaining = content;
70641
- let codeBlockStart = remaining.indexOf('```');
70642
- while (codeBlockStart !== -1) {
70643
- protectedContent += remaining.slice(0, codeBlockStart);
70644
- remaining = remaining.slice(codeBlockStart);
70645
- const codeBlockEnd = remaining.indexOf('```', 3);
70646
- if (codeBlockEnd === -1) {
70647
- break;
70648
- }
70649
- const match = remaining.slice(0, codeBlockEnd + 3);
70650
- const index = codeBlocks.length;
70651
- codeBlocks.push(match);
70652
- protectedContent += `___CODE_BLOCK_${index}___`;
70653
- remaining = remaining.slice(codeBlockEnd + 3);
70654
- codeBlockStart = remaining.indexOf('```');
70655
- }
70656
- protectedContent += remaining;
70657
- protectedContent = protectedContent.replace(/`([^`\n]+)`/g, match => {
70658
- const index = inlineCode.length;
70659
- inlineCode.push(match);
70660
- return `___INLINE_CODE_${index}___`;
70661
- });
70662
- return { protectedCode: { codeBlocks, inlineCode }, protectedContent };
70663
- }
70664
- /**
70665
- * Restores inline code by replacing placeholders with original content.
70666
- *
70667
- * @param content - Content with inline code placeholders
70668
- * @param protectedCode - The protected code arrays
70669
- * @returns Content with inline code restored
70670
- */
70671
- function restoreInlineCode(content, protectedCode) {
70672
- return content.replace(/___INLINE_CODE_(\d+)___/g, (_m, idx) => {
70673
- return protectedCode.inlineCode[parseInt(idx, 10)];
70674
- });
70675
- }
70676
- /**
70677
- * Restores fenced code blocks by replacing placeholders with original content.
70678
- *
70679
- * @param content - Content with code block placeholders
70680
- * @param protectedCode - The protected code arrays
70681
- * @returns Content with code blocks restored
70682
- */
70683
- function restoreFencedCodeBlocks(content, protectedCode) {
70684
- return content.replace(/___CODE_BLOCK_(\d+)___/g, (_m, idx) => {
70685
- return protectedCode.codeBlocks[parseInt(idx, 10)];
70686
- });
70687
- }
70688
- /**
70689
- * Restores all code blocks and inline code by replacing placeholders with original content.
70690
- *
70691
- * @param content - Content with code placeholders
70692
- * @param protectedCode - The protected code arrays
70693
- * @returns Content with all code blocks and inline code restored
70694
- * @example
70695
- * ```typescript
70696
- * const content = 'Text with ___INLINE_CODE_0___ and ___CODE_BLOCK_0___';
70697
- * const protectedCode = {
70698
- * codeBlocks: ['```js\ncode\n```'],
70699
- * inlineCode: ['`inline`']
70700
- * };
70701
- * restoreCodeBlocks(content, protectedCode)
70702
- * // Returns: 'Text with `inline` and ```js\ncode\n```'
70703
- * ```
70704
- */
70705
- function restoreCodeBlocks(content, protectedCode) {
70706
- let restored = restoreFencedCodeBlocks(content, protectedCode);
70707
- restored = restoreInlineCode(restored, protectedCode);
70708
- return restored;
70709
- }
70710
-
70711
- ;// ./processor/transform/mdxish/preprocess-jsx-expressions.ts
70712
-
70713
- // Base64 encode (Node.js + browser compatible)
70714
- function base64Encode(str) {
70715
- if (typeof Buffer !== 'undefined') {
70716
- return Buffer.from(str, 'utf-8').toString('base64');
70717
- }
70718
- return btoa(unescape(encodeURIComponent(str)));
70719
- }
70720
- // Base64 decode (Node.js + browser compatible)
70721
- function base64Decode(str) {
70722
- if (typeof Buffer !== 'undefined') {
70723
- return Buffer.from(str, 'base64').toString('utf-8');
70724
- }
70725
- return decodeURIComponent(escape(atob(str)));
70726
- }
70727
- function escapeHtmlAttribute(value) {
70728
- return value
70729
- .replace(/&/g, '&amp;')
70730
- .replace(/"/g, '&quot;')
70731
- .replace(/</g, '&lt;')
70732
- .replace(/>/g, '&gt;')
70733
- .replace(/\n/g, '&#10;');
70734
- }
70735
- // Marker prefix for JSON-serialized complex values (arrays/objects)
70736
- // Using a prefix that won't conflict with regular string values
70737
- const JSON_VALUE_MARKER = '__MDXISH_JSON__';
70738
- // Markers for protected HTMLBlock content (HTML comments avoid markdown parsing issues)
70739
- const HTML_BLOCK_CONTENT_START = '<!--RDMX_HTMLBLOCK:';
70740
- const HTML_BLOCK_CONTENT_END = ':RDMX_HTMLBLOCK-->';
70741
- /**
70742
- * Evaluates a JavaScript expression using context variables.
70743
- *
70744
- * @param expression
70745
- * @param context
70746
- * @returns The evaluated result
70747
- * @example
70748
- * ```typescript
70749
- * const context = { baseUrl: 'https://example.com', path: '/api' };
70750
- * evaluateExpression('baseUrl + path', context)
70751
- * // Returns: 'https://example.com/api'
70752
- * ```
70753
- */
70754
- function evaluateExpression(expression, context) {
70755
- const contextKeys = Object.keys(context);
70756
- const contextValues = Object.values(context);
70757
- // eslint-disable-next-line no-new-func
70758
- const func = new Function(...contextKeys, `return ${expression}`);
70759
- return func(...contextValues);
70760
- }
70761
- /**
70762
- * Base64 encodes HTMLBlock template literal content to prevent markdown parser from consuming <script>/<style> tags.
70763
- *
70764
- * @param content
70765
- * @returns Content with HTMLBlock template literals base64 encoded in HTML comments
70766
- * @example
70767
- * ```typescript
70768
- * const input = '<HTMLBlock>{`<script>alert("xss")</script>`}</HTMLBlock>';
70769
- * protectHTMLBlockContent(input)
70770
- * // Returns: '<HTMLBlock><!--RDMX_HTMLBLOCK:PHNjcmlwdD5hbGVydCgieHNzIik8L3NjcmlwdD4=:RDMX_HTMLBLOCK--></HTMLBlock>'
70771
- * ```
70772
- */
70773
- function protectHTMLBlockContent(content) {
70774
- return content.replace(/(<HTMLBlock[^>]*>)\{\s*`((?:[^`\\]|\\.)*)`\s*\}(<\/HTMLBlock>)/g, (_match, openTag, templateContent, closeTag) => {
70775
- const encoded = base64Encode(templateContent);
70776
- return `${openTag}${HTML_BLOCK_CONTENT_START}${encoded}${HTML_BLOCK_CONTENT_END}${closeTag}`;
70777
- });
70778
- }
70779
- /**
70780
- * Removes JSX-style comments (e.g., { /* comment *\/ }) from content.
70781
- *
70782
- * @param content
70783
- * @returns Content with JSX comments removed
70784
- * @example
70785
- * ```typescript
70786
- * removeJSXComments('Text { /* comment *\/ } more text')
70787
- * // Returns: 'Text more text'
70788
- * ```
70789
- */
70790
- function removeJSXComments(content) {
70791
- return content.replace(/\{\s*\/\*[^*]*(?:\*(?!\/)[^*]*)*\*\/\s*\}/g, '');
70792
- }
70793
- /**
70794
- * Extracts content between balanced braces, handling nested braces.
70795
- *
70796
- * @param content
70797
- * @param start
70798
- * @returns Object with extracted content and end position, or null if braces are unbalanced
70799
- * @example
70800
- * ```typescript
70801
- * const input = 'foo{bar{baz}qux}end';
70802
- * extractBalancedBraces(input, 3) // start at position 3 (after '{')
70803
- * // Returns: { content: 'bar{baz}qux', end: 16 }
70804
- * ```
70805
- */
70806
- function extractBalancedBraces(content, start) {
70807
- let depth = 1;
70808
- let pos = start;
70809
- while (pos < content.length && depth > 0) {
70810
- const char = content[pos];
70811
- if (char === '{')
70812
- depth += 1;
70813
- else if (char === '}')
70814
- depth -= 1;
70815
- pos += 1;
70816
- }
70817
- if (depth !== 0)
70818
- return null;
70819
- return { content: content.slice(start, pos - 1), end: pos };
70820
- }
70821
- /**
70822
- * Escapes problematic braces in content to prevent MDX expression parsing errors.
70823
- * Handles three cases:
70824
- * 1. Unbalanced braces (e.g., `{foo` without closing `}`)
70825
- * 2. Paragraph-spanning expressions (e.g., `{\n\n}` where blank line splits paragraphs)
70826
- * 3. Skips HTML elements to prevent backslashes appearing in output
70827
- *
70828
- */
70829
- function escapeProblematicBraces(content) {
70830
- // Skip HTML elements — their content should never be escaped because
70831
- // rehypeRaw parses them into hast elements, making `\` literal text in output
70832
- const htmlElements = [];
70833
- const safe = content.replace(/<([a-z][a-zA-Z0-9]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>/g, match => {
70834
- const idx = htmlElements.length;
70835
- htmlElements.push(match);
70836
- return `___HTML_ELEM_${idx}___`;
70837
- });
70838
- const toEscape = new Set();
70839
- // Convert to array of Unicode code points to handle emojis and multi-byte characters correctly
70840
- const chars = Array.from(safe);
70841
- let strDelim = null;
70842
- let strEscaped = false;
70843
- // Stack of open braces with their state
70844
- const openStack = [];
70845
- // Track position of last newline (outside strings) to detect blank lines
70846
- let lastNewlinePos = -2; // -2 means no recent newline
70847
- for (let i = 0; i < chars.length; i += 1) {
70848
- const ch = chars[i];
70849
- // Track string delimiters inside expressions to ignore braces within them
70850
- if (openStack.length > 0) {
70851
- if (strDelim) {
70852
- if (strEscaped)
70853
- strEscaped = false;
70854
- else if (ch === '\\')
70855
- strEscaped = true;
70856
- else if (ch === strDelim)
70857
- strDelim = null;
70858
- // eslint-disable-next-line no-continue
70859
- continue;
70860
- }
70861
- if (ch === '"' || ch === "'" || ch === '`') {
70862
- strDelim = ch;
70863
- // eslint-disable-next-line no-continue
70864
- continue;
70865
- }
70866
- // Track newlines to detect blank lines (paragraph boundaries)
70867
- if (ch === '\n') {
70868
- // Check if this newline creates a blank line (only whitespace since last newline)
70869
- if (lastNewlinePos >= 0) {
70870
- const between = chars.slice(lastNewlinePos + 1, i).join('');
70871
- if (/^[ \t]*$/.test(between)) {
70872
- // This is a blank line - mark all open expressions as paragraph-spanning
70873
- openStack.forEach(entry => {
70874
- entry.hasBlankLine = true;
70875
- });
70876
- }
70877
- }
70878
- lastNewlinePos = i;
70879
- }
70880
- }
70881
- // Skip already-escaped braces (count preceding backslashes)
70882
- if (ch === '{' || ch === '}') {
70883
- let bs = 0;
70884
- for (let j = i - 1; j >= 0 && chars[j] === '\\'; j -= 1)
70885
- bs += 1;
70886
- if (bs % 2 === 1) {
70887
- // eslint-disable-next-line no-continue
70888
- continue;
70889
- }
70890
- }
70891
- if (ch === '{') {
70892
- openStack.push({ pos: i, hasBlankLine: false });
70893
- lastNewlinePos = -2; // Reset newline tracking for new expression
70894
- }
70895
- else if (ch === '}') {
70896
- if (openStack.length > 0) {
70897
- const entry = openStack.pop();
70898
- // If expression spans paragraph boundary, escape both braces
70899
- if (entry.hasBlankLine) {
70900
- toEscape.add(entry.pos);
70901
- toEscape.add(i);
70902
- }
70903
- }
70904
- else {
70905
- // Unbalanced closing brace (no matching open)
70906
- toEscape.add(i);
70907
- }
70908
- }
70909
- }
70910
- // Any remaining open braces are unbalanced
70911
- openStack.forEach(entry => toEscape.add(entry.pos));
70912
- // If there are no problematic braces, return safe content as-is;
70913
- // otherwise, escape each problematic `{` or `}` so MDX doesn't treat them as expressions.
70914
- let result = toEscape.size === 0
70915
- ? safe
70916
- : chars.map((ch, i) => (toEscape.has(i) ? `\\${ch}` : ch)).join('');
70917
- // Restore HTML elements
70918
- if (htmlElements.length > 0) {
70919
- result = result.replace(/___HTML_ELEM_(\d+)___/g, (_m, idx) => htmlElements[parseInt(idx, 10)]);
70920
- }
70921
- return result;
70922
- }
70923
- /**
70924
- * Converts JSX attribute expressions (attribute={expression}) to HTML attributes (attribute="value").
70925
- * Handles style objects (camelCase → kebab-case), className → class, and JSON stringifies objects.
70926
- *
70927
- * @param content
70928
- * @param context
70929
- * @returns Content with attribute expressions evaluated and converted to HTML attributes
70930
- * @example
70931
- * ```typescript
70932
- * const context = { baseUrl: 'https://example.com' };
70933
- * const input = '<a href={baseUrl}>Link</a>';
70934
- * evaluateAttributeExpressions(input, context)
70935
- * // Returns: '<a href="https://example.com">Link</a>'
70936
- * ```
70937
- */
70938
- function evaluateAttributeExpressions(content, context, protectedCode) {
70939
- const attrStartRegex = /(\w+)=\{/g;
70940
- let result = '';
70941
- let lastEnd = 0;
70942
- let match = attrStartRegex.exec(content);
70943
- while (match !== null) {
70944
- const attributeName = match[1];
70945
- const braceStart = match.index + match[0].length;
70946
- const extracted = extractBalancedBraces(content, braceStart);
70947
- if (extracted) {
70948
- // The expression might contain template literals in MDX component tag props
70949
- // E.g. <Component greeting={`Hello World!`} />
70950
- // that is marked as inline code. So we need to restore the inline codes
70951
- // in the expression to evaluate it
70952
- let expression = extracted.content;
70953
- if (protectedCode) {
70954
- expression = restoreInlineCode(expression, protectedCode);
70955
- }
70956
- const fullMatchEnd = extracted.end;
70957
- result += content.slice(lastEnd, match.index);
70958
- try {
70959
- const evalResult = evaluateExpression(expression, context);
70960
- if (typeof evalResult === 'object' && evalResult !== null) {
70961
- if (attributeName === 'style') {
70962
- const cssString = Object.entries(evalResult)
70963
- .map(([key, value]) => {
70964
- const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
70965
- return `${cssKey}: ${value}`;
70966
- })
70967
- .join('; ');
70968
- result += `style="${cssString}"`;
70969
- }
70970
- else {
70971
- // These are arrays / objects attribute values
70972
- // Mark JSON-serialized values with a prefix so they can be parsed back correctly
70973
- const jsonValue = escapeHtmlAttribute(JSON_VALUE_MARKER + JSON.stringify(evalResult));
70974
- // Use double quotes so that multi-paragraph values are not split into multiple attributes by the processors
70975
- result += `${attributeName}="${jsonValue}"`;
70976
- }
70977
- }
70978
- else if (attributeName === 'className') {
70979
- // Escape special characters so that it doesn't break and split the attribute value to nodes
70980
- // This will be restored later in the pipeline
70981
- result += `class="${escapeHtmlAttribute(String(evalResult))}"`;
70982
- }
70983
- else {
70984
- result += `${attributeName}="${escapeHtmlAttribute(String(evalResult))}"`;
70985
- }
70986
- }
70987
- catch (_error) {
70988
- result += content.slice(match.index, fullMatchEnd);
70989
- }
70990
- lastEnd = fullMatchEnd;
70991
- attrStartRegex.lastIndex = fullMatchEnd;
70992
- }
70993
- match = attrStartRegex.exec(content);
70994
- }
70995
- result += content.slice(lastEnd);
70996
- return result;
71304
+ '🇿🇲': 'zambia',
71305
+ '🇿🇼': 'zimbabwe',
71306
+ '🏴󠁧󠁢󠁥󠁮󠁧󠁿': 'england',
71307
+ '🏴󠁧󠁢󠁳󠁣󠁴󠁿': 'scotland',
71308
+ '🏴󠁧󠁢󠁷󠁬󠁳󠁿': 'wales'
70997
71309
  }
70998
- /**
70999
- * Preprocesses JSX-like expressions in markdown before parsing.
71000
- * Inline expressions are handled separately; attribute expressions are processed here.
71001
- *
71002
- * @param content
71003
- * @param context
71004
- * @returns Preprocessed content ready for markdown parsing
71005
- */
71006
- function preprocessJSXExpressions(content, context = {}) {
71007
- // Step 0: Base64 encode HTMLBlock content
71008
- let processed = protectHTMLBlockContent(content);
71009
- // Step 1: Protect code blocks and inline code
71010
- const { protectedCode, protectedContent } = protectCodeBlocks(processed);
71011
- // Step 2: Evaluate attribute expressions (JSX attribute syntax: href={baseUrl})
71012
- // For inline expressions, we use a library to parse the expression & evaluate it later
71013
- // For attribute expressions, it was difficult to use a library to parse them, so do it manually
71014
- processed = evaluateAttributeExpressions(protectedContent, context, protectedCode);
71015
- // Step 3: Escape problematic braces to prevent MDX expression parsing errors
71016
- // This handles both unbalanced braces and paragraph-spanning expressions in one pass
71017
- processed = escapeProblematicBraces(processed);
71018
- // Step 4: Restore protected code blocks
71019
- processed = restoreCodeBlocks(processed, protectedCode);
71020
- return processed;
71310
+
71311
+ ;// ./lib/owlmoji.ts
71312
+
71313
+ const owlmoji = [
71314
+ {
71315
+ emoji: '', // This `emoji` property doesn't get consumed, but is required for type consistency
71316
+ names: ['owlbert'],
71317
+ tags: ['owlbert'],
71318
+ description: 'an owlbert for any occasion',
71319
+ category: 'ReadMe',
71320
+ },
71321
+ {
71322
+ emoji: '',
71323
+ names: ['owlbert-books'],
71324
+ tags: ['owlbert'],
71325
+ description: 'owlbert carrying books',
71326
+ category: 'ReadMe',
71327
+ },
71328
+ {
71329
+ emoji: '',
71330
+ names: ['owlbert-mask'],
71331
+ tags: ['owlbert'],
71332
+ description: 'owlbert with a respirator',
71333
+ category: 'ReadMe',
71334
+ },
71335
+ {
71336
+ emoji: '',
71337
+ names: ['owlbert-reading'],
71338
+ tags: ['owlbert'],
71339
+ description: 'owlbert reading',
71340
+ category: 'ReadMe',
71341
+ },
71342
+ {
71343
+ emoji: '',
71344
+ names: ['owlbert-thinking'],
71345
+ tags: ['owlbert'],
71346
+ description: 'owlbert thinking',
71347
+ category: 'ReadMe',
71348
+ },
71349
+ ];
71350
+ const owlmojiNames = owlmoji.flatMap(emoji => emoji.names);
71351
+ class Owlmoji {
71352
+ static kind = (name) => {
71353
+ if (name in nameToEmoji)
71354
+ return 'gemoji';
71355
+ else if (name.match(/^fa-/))
71356
+ return 'fontawesome';
71357
+ else if (owlmojiNames.includes(name))
71358
+ return 'owlmoji';
71359
+ return null;
71360
+ };
71361
+ static nameToEmoji = nameToEmoji;
71362
+ static owlmoji = gemoji_gemoji.concat(owlmoji);
71021
71363
  }
71022
71364
 
71023
- ;// ./processor/utils.ts
71365
+ ;// ./processor/transform/gemoji+.ts
71024
71366
 
71025
71367
 
71026
71368
 
71027
- /**
71028
- * Formats the hProperties of a node as a string, so they can be compiled back into JSX/MDX.
71029
- * This currently sets all the values to a string since we process/compile the MDX on the fly
71030
- * through the editor, and it'll throw errors over malformed JSX. TODO: fix this.
71031
- *
71032
- * @template T
71033
- * @param {Node} node
71034
- * @returns {string} formatted hProperties as JSX attributes
71035
- */
71036
- const formatHProps = (node) => {
71037
- const hProps = getHProps(node);
71038
- return formatProps(hProps);
71039
- };
71040
- /**
71041
- * Formats an object of props as a string.
71042
- *
71043
- * @param {Object} props
71044
- * @returns {string}
71045
- */
71046
- const formatProps = (props) => {
71047
- const keys = Object.keys(props);
71048
- return keys.map(key => `${key}="${props[key]}"`).join(' ');
71049
- };
71050
- /**
71051
- * Returns the hProperties of a node.
71052
- *
71053
- * @template T
71054
- * @param {Node} node
71055
- * @returns {T} hProperties
71056
- */
71057
- const getHProps = (node) => {
71058
- const hProps = node.data?.hProperties || {};
71059
- return hProps;
71060
- };
71061
- /**
71062
- * Returns array of hProperty keys.
71063
- *
71064
- * @template T
71065
- * @param {Node} node
71066
- * @returns {Array} array of hProperty keys
71067
- */
71068
- const getHPropKeys = (node) => {
71069
- const hProps = getHProps(node);
71070
- return Object.keys(hProps) || [];
71071
- };
71072
- /**
71073
- * Gets the attributes of an MDX element and returns them as an object of hProperties.
71074
- *
71075
- * @template T
71076
- * @param {(MdxJsxFlowElement | MdxJsxTextElement)} jsx
71077
- * @returns {T} object of hProperties
71078
- */
71079
- const getAttrs = (jsx) => jsx.attributes.reduce((memo, attr) => {
71080
- if ('name' in attr) {
71081
- if (typeof attr.value === 'string') {
71082
- if (attr.value.startsWith(JSON_VALUE_MARKER)) {
71083
- try {
71084
- memo[attr.name] = JSON.parse(attr.value.slice(JSON_VALUE_MARKER.length));
71085
- }
71086
- catch {
71087
- memo[attr.name] = attr.value;
71088
- }
71089
- }
71090
- else {
71091
- memo[attr.name] = attr.value;
71092
- }
71093
- }
71094
- else if (attr.value === null) {
71095
- memo[attr.name] = true;
71096
- }
71097
- else if (attr.value.value !== 'undefined') {
71098
- memo[attr.name] = JSON.parse(attr.value.value);
71099
- }
71100
- }
71101
- return memo;
71102
- }, {});
71103
- /**
71104
- * Gets the children of an MDX element and returns them as an array of Text nodes.
71105
- * Currently only being used by the HTML Block component, which only expects a single text node.
71106
- *
71107
- * @template T
71108
- * @param {(MdxJsxFlowElement | MdxJsxTextElement)} jsx
71109
- * @returns {Array} array of child text nodes
71110
- */
71111
- const getChildren = (jsx) => jsx.children.reduce((memo, child, i) => {
71112
- memo[i] = {
71113
- type: 'text',
71114
- value: child.value,
71115
- position: child.position,
71116
- };
71117
- return memo;
71118
- }, []);
71119
- /**
71120
- * Tests if a node is an MDX element.
71121
- * TODO: Make this more extensible to all types of nodes. isElement(node, 'type' or ['type1', 'type2']), say
71122
- *
71123
- * @param {Node} node
71124
- * @returns {(node is MdxJsxFlowElement | MdxJsxTextElement | MdxjsEsm)}
71125
- */
71126
- const isMDXElement = (node) => {
71127
- return ['mdxJsxFlowElement', 'mdxJsxTextElement'].includes(node.type);
71128
- };
71129
- /**
71130
- * Tests if a node is an MDX ESM element (i.e. import or export).
71131
- *
71132
- * @param {Node} node
71133
- * @returns {boolean}
71134
- */
71135
- const isMDXEsm = (node) => {
71136
- return node.type === 'mdxjsEsm';
71137
- };
71138
- /**
71139
- * Takes an HTML string and formats it for display in the editor. Removes leading/trailing newlines
71140
- * and unindents the HTML.
71141
- *
71142
- * @param {string} html - HTML content from template literal
71143
- * @returns {string} processed HTML
71144
- */
71145
- function formatHtmlForMdxish(html) {
71146
- // Remove leading/trailing backticks if present, since they're used to keep the HTML
71147
- // from being parsed prematurely
71148
- let processed = html;
71149
- if (processed.startsWith('`') && processed.endsWith('`')) {
71150
- processed = processed.slice(1, -1);
71151
- }
71152
- // Removes the leading/trailing newlines
71153
- let cleaned = processed.replace(/^\s*\n|\n\s*$/g, '');
71154
- // Convert literal \n sequences to actual newlines BEFORE processing backticks
71155
- // This prevents the backtick unescaping regex from incorrectly matching \n sequences
71156
- cleaned = cleaned.replace(/\\n/g, '\n');
71157
- // Unescape backticks: \` -> ` (users escape backticks in template literals)
71158
- // Handle both cases: \` (adjacent) and \ followed by ` (split by markdown parser)
71159
- cleaned = cleaned.replace(/\\`/g, '`');
71160
- // Also handle case where backslash and backtick got separated by markdown parsing
71161
- // Pattern: backslash followed by any characters (but not \n which we already handled), then a backtick
71162
- // This handles cases like: \example` -> `example` (replacing \ with ` at start)
71163
- // Exclude \n sequences to avoid matching them incorrectly
71164
- cleaned = cleaned.replace(/\\([^`\\n]*?)`/g, '`$1`');
71165
- // Fix case where markdown parser consumed one backtick from triple backticks
71166
- // Pattern: `` followed by a word (like ``javascript) should be ```javascript
71167
- // This handles cases where code fences were parsed and one backtick was lost
71168
- cleaned = cleaned.replace(/<(\w+[^>]*)>``(\w+)/g, '<$1>```$2');
71169
- // Unescape dollar signs: \$ -> $ (users escape $ in template literals to prevent interpolation)
71170
- cleaned = cleaned.replace(/\\\$/g, '$');
71171
- return cleaned;
71172
- }
71173
- /**
71174
- * Takes an HTML string and formats it for display in the editor. Removes leading/trailing newlines
71175
- * and unindents the HTML.
71176
- *
71177
- * @param {string} html
71178
- * @returns {string} formatted HTML
71179
- */
71180
- const formatHTML = (html) => {
71181
- // Remove leading/trailing backticks if present, since they're used to keep the HTML
71182
- // from being parsed prematurely
71183
- if (html.startsWith('`') && html.endsWith('`')) {
71184
- // eslint-disable-next-line no-param-reassign
71185
- html = html.slice(1, -1);
71186
- }
71187
- // Removes the leading/trailing newlines
71188
- const cleaned = html.replace(/^\s*\n|\n\s*$/g, '');
71189
- // // Get the number of spaces in the first line to determine the tab size
71190
- // const tab = cleaned.match(/^\s*/)[0].length;
71191
- // // Remove the first indentation level from each line
71192
- // const tabRegex = new RegExp(`^\\s{${tab}}`, 'gm');
71193
- // const unindented = cleaned.replace(tabRegex, '');
71194
- return cleaned;
71195
- };
71196
- /**
71197
- * Reformat HTML for the markdown/mdx by adding an indentation to each line. This assures that the
71198
- * HTML is indentend properly within the HTMLBlock component when rendered in the markdown/mdx.
71199
- *
71200
- * @param {string} html
71201
- * @param {number} [indent=2]
71202
- * @returns {string} re-formatted HTML
71203
- */
71204
- const reformatHTML = (html) => {
71205
- // Remove leading/trailing newlines
71206
- const cleaned = html.replace(/^\s*\n|\n\s*$/g, '').replaceAll(/(?<!\\(\\\\)*)`/g, '\\`');
71207
- // // Create a tab/indent with the specified number of spaces
71208
- // const tab = ' '.repeat(indent);
71209
- // // Indent each line of the HTML (converts to an array, indents each line, then joins back)
71210
- // const indented = cleaned.split('\n').map((line: string) => `${tab}${line}`).join('\n');
71211
- return cleaned;
71212
- };
71213
- const toAttributes = (object, keys = []) => {
71214
- const attributes = [];
71215
- Object.entries(object).forEach(([name, v]) => {
71216
- if (keys.length > 0 && !keys.includes(name))
71217
- return;
71218
- let value;
71219
- if (typeof v === 'undefined' || v === null || v === '' || v === false) {
71220
- return;
71221
- }
71222
- else if (typeof v === 'string') {
71223
- value = v;
71224
- }
71225
- else {
71226
- /* values can be null, undefined, string, or a expression, eg:
71227
- *
71228
- * ```
71229
- * <Image src="..." border={false} size={width - 20} />
71230
- * ```
71231
- *
71232
- * Parsing the expression seems to only be done by the library
71233
- * `mdast-util-mdx-jsx`, and so the most straight forward way to parse
71234
- * the expression and get the appropriate AST is with our `mdast`
71235
- * function.
71236
- */
71237
- const proxy = lib_mdast(`{${v}}`);
71238
- const data = proxy.children[0].data;
71239
- value = {
71240
- type: 'mdxJsxAttributeValueExpression',
71241
- value: v.toString(),
71242
- data,
71369
+ const gemoji_regex = /(?<=^|\s):(?<name>\+1|[-\w]+):/g;
71370
+ const gemojiReplacer = (_, name) => {
71371
+ switch (Owlmoji.kind(name)) {
71372
+ case 'gemoji': {
71373
+ const node = {
71374
+ type: NodeTypes.emoji,
71375
+ value: Owlmoji.nameToEmoji[name],
71376
+ name,
71243
71377
  };
71378
+ return node;
71244
71379
  }
71245
- attributes.push({
71246
- type: 'mdxJsxAttribute',
71247
- name,
71248
- value,
71249
- });
71250
- });
71251
- return attributes;
71252
- };
71253
- /**
71254
- * Checks if a named export exists in the MDX tree. Accepts either an mdast or
71255
- * a hast tree.
71256
- *
71257
- * example:
71258
- * ```
71259
- * const mdx = `export const Foo = 'bar';`
71260
- *
71261
- * hasNamedExport(mdast(mdx), 'Foo') => true
71262
- * ```
71263
- *
71264
- */
71265
- const hasNamedExport = (tree, name) => {
71266
- let hasExport = false;
71267
- // eslint-disable-next-line consistent-return
71268
- visit(tree, 'mdxjsEsm', node => {
71269
- if ('declaration' in node.data.estree.body[0] &&
71270
- node.data.estree.body[0].declaration.type === 'VariableDeclaration') {
71271
- const { declarations } = node.data.estree.body[0].declaration;
71272
- hasExport = !!declarations.find(declaration => 'name' in declaration.id && declaration.id.name === name);
71273
- return hasExport ? EXIT : CONTINUE;
71380
+ case 'fontawesome': {
71381
+ const node = {
71382
+ type: NodeTypes.i,
71383
+ value: name,
71384
+ data: {
71385
+ hName: 'i',
71386
+ hProperties: {
71387
+ className: ['fa-regular', name],
71388
+ },
71389
+ },
71390
+ };
71391
+ return node;
71274
71392
  }
71275
- });
71276
- return hasExport;
71277
- };
71278
- /* Example mdast structures to find first export name in a mdxjsEsm node:
71279
- There are three types of export declarations that we need to consider:
71280
- 1. VARIABLE DECLARATION
71281
- "type": "mdxjsEsm",
71282
- "value": "export const Foo = () => <div>Hello world</div>\nexport const Bar = () => <div>hello darkness my old friend</div>",
71283
- "data": {
71284
- "estree": {
71285
- "type": "Program",
71286
- "body": [
71287
- {
71288
- "type": "ExportNamedDeclaration",
71289
- "declaration": {
71290
- "type": "VariableDeclaration",
71291
- "declarations": [
71292
- {
71293
- "type": "VariableDeclarator",
71294
- "id": {
71295
- "type": "Identifier",
71296
- "name": "Foo" // --------> This is the export name
71393
+ case 'owlmoji': {
71394
+ const node = {
71395
+ type: 'image',
71396
+ title: `:${name}:`,
71397
+ alt: `:${name}:`,
71398
+ url: `/public/img/emojis/${name}.png`,
71399
+ data: {
71400
+ hProperties: {
71401
+ className: 'emoji',
71402
+ align: 'absmiddle',
71403
+ height: '20',
71404
+ width: '20',
71297
71405
  },
71298
- ...
71299
-
71300
- 2/3. FUNCTION DECLARATION & CLASS DECLARATION
71301
- "estree": {
71302
- "type": "Program",
71303
- "body": [
71304
- {
71305
- "type": "ExportNamedDeclaration",
71306
- "declaration": {
71307
- "type": "ClassDeclaration" | "FunctionDeclaration",
71308
- "id": {
71309
- "type": "Identifier",
71310
- "name": "Foo" // --------> This is the export name
71311
71406
  },
71312
- */
71313
- const getExports = (tree) => {
71314
- const set = new Set();
71315
- visit(tree, isMDXEsm, (node) => {
71316
- // Once inside an mdxjsEsm node, we need to check for one or more declared exports within data.estree.body
71317
- // 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
71318
- const body = node.data?.estree.body;
71319
- if (!body)
71320
- return;
71321
- body.forEach(child => {
71322
- if (child.type === 'ExportNamedDeclaration') {
71323
- // There are three types of ExportNamedDeclaration that we need to consider: VariableDeclaration, FunctionDeclaration, ClassDeclaration
71324
- const declaration = child.declaration;
71325
- // FunctionDeclaration and ClassDeclaration have the same structure
71326
- if (declaration.type !== 'VariableDeclaration') {
71327
- // Note: declaration.id.type is always 'Identifier' for FunctionDeclarations and ClassDeclarations
71328
- set.add(declaration.id.name);
71329
- }
71330
- else {
71331
- declaration.declarations.forEach(dec => {
71332
- const id = dec.id;
71333
- if (id.type === 'Identifier') {
71334
- set.add(id.name);
71335
- }
71336
- });
71337
- }
71338
- }
71339
- });
71340
- });
71341
- return Array.from(set);
71407
+ };
71408
+ return node;
71409
+ }
71410
+ default:
71411
+ return false;
71412
+ }
71342
71413
  };
71414
+ const gemojiTransformer = () => (tree) => {
71415
+ findAndReplace(tree, [gemoji_regex, gemojiReplacer]);
71416
+ return tree;
71417
+ };
71418
+ /* harmony default export */ const gemoji_ = (gemojiTransformer);
71343
71419
 
71344
71420
  ;// ./processor/transform/handle-missing-components.ts
71345
71421
 
@@ -72300,6 +72376,23 @@ const mdxishTables = () => tree => {
72300
72376
  return;
72301
72377
  try {
72302
72378
  const parsed = tableNodeProcessor.runSync(tableNodeProcessor.parse(node.value));
72379
+ // since we use a subparser in `tableNodeProcessor` to parse `node.value`,
72380
+ // positions are relative to that substring. shifting them by the base
72381
+ // offset and line number makes them valid in the outer source coordinate space.
72382
+ // otherwise, consumers who directly slice based on position would read and grab the
72383
+ // wrong content
72384
+ const baseOffset = node.position?.start?.offset ?? 0;
72385
+ const baseLine = (node.position?.start?.line ?? 1) - 1;
72386
+ visit(parsed, child => {
72387
+ if (child.position?.start) {
72388
+ child.position.start.offset = (child.position.start.offset ?? 0) + baseOffset;
72389
+ child.position.start.line += baseLine;
72390
+ }
72391
+ if (child.position?.end) {
72392
+ child.position.end.offset = (child.position.end.offset ?? 0) + baseOffset;
72393
+ child.position.end.line += baseLine;
72394
+ }
72395
+ });
72303
72396
  visit(parsed, isMDXElement, (tableNode) => {
72304
72397
  if (tableNode.name === 'Table') {
72305
72398
  processTableNode(tableNode, index, parent, node.position);
@@ -92710,74 +92803,6 @@ const codeTabs = (node, _, state, info) => {
92710
92803
  };
92711
92804
  /* harmony default export */ const compile_code_tabs = (codeTabs);
92712
92805
 
92713
- ;// ./processor/compile/compatibility.ts
92714
-
92715
-
92716
-
92717
- /*
92718
- * Converts a (remark < v9) html node to a JSX string.
92719
- *
92720
- * First we replace html comments with the JSX equivalent. Then, we parse that
92721
- * as html, and serialize it back as xml!
92722
- *
92723
- */
92724
- const compileHtml = (node) => {
92725
- const string = node.value.replaceAll(/<!--(.*)-->/gms, '{/*$1*/}');
92726
- return string;
92727
- };
92728
- const figureToImageBlock = (node) => {
92729
- const { align, border, width, src, url, alt, title, ...image } = node.children.find((child) => child.type === 'image');
92730
- const { className } = image.data.hProperties;
92731
- const figcaption = node.children.find((child) => child.type === 'figcaption');
92732
- const caption = figcaption ? toMarkdown(figcaption.children).trim() : null;
92733
- const attributes = {
92734
- ...(align && { align }),
92735
- ...(alt && { alt }),
92736
- ...(className && { border: className === 'border' }),
92737
- ...(border && { border }),
92738
- ...(caption && { caption }),
92739
- ...(title && { title }),
92740
- ...(width && { width }),
92741
- src: src || url,
92742
- };
92743
- return `<Image ${formatProps(attributes)} />`;
92744
- };
92745
- const embedToEmbedBlock = (node) => {
92746
- const { html, ...embed } = node.data.hProperties;
92747
- const attributes = {
92748
- ...embed,
92749
- ...(html && { html: encodeURIComponent(html) }),
92750
- };
92751
- return `<Embed ${formatProps(attributes)} />`;
92752
- };
92753
- const compatibility = (node) => {
92754
- switch (node.type) {
92755
- case NodeTypes.glossary: {
92756
- // Glossary terms will no longer be serialized as special node types in the Editor but we want to ensure that we compile historical
92757
- // data correctly
92758
- const term = node.data?.hProperties?.term || node.children[0].value;
92759
- return `<Glossary>${term}</Glossary>`;
92760
- }
92761
- case NodeTypes.reusableContent:
92762
- return `<${node.tag} />`;
92763
- case 'html':
92764
- return compileHtml(node);
92765
- case 'escape':
92766
- return `\\${node.value}`;
92767
- case 'figure':
92768
- return figureToImageBlock(node);
92769
- case 'embed':
92770
- return embedToEmbedBlock(node);
92771
- case 'i':
92772
- return `:${node.data.hProperties.className[1]}:`;
92773
- case 'yaml':
92774
- return `---\n${node.value}\n---`;
92775
- default:
92776
- throw new Error('Unhandled node type!');
92777
- }
92778
- };
92779
- /* harmony default export */ const compile_compatibility = (compatibility);
92780
-
92781
92806
  ;// ./processor/compile/embed.ts
92782
92807
 
92783
92808
  const embed_embed = (node) => {
@@ -98924,7 +98949,7 @@ function findJsxTableToken() {
98924
98949
  return undefined;
98925
98950
  }
98926
98951
  function enterJsxTable(token) {
98927
- jsx_table_contextMap.set(token, { chunks: [] });
98952
+ jsx_table_contextMap.set(token, { chunks: [], lastEndLine: token.start.line });
98928
98953
  this.enter({ type: 'html', value: '' }, token);
98929
98954
  }
98930
98955
  function exitJsxTableData(token) {
@@ -98932,14 +98957,20 @@ function exitJsxTableData(token) {
98932
98957
  if (!tableToken)
98933
98958
  return;
98934
98959
  const ctx = jsx_table_contextMap.get(tableToken);
98935
- if (ctx)
98960
+ if (ctx) {
98961
+ const gap = token.start.line - ctx.lastEndLine;
98962
+ if (ctx.chunks.length > 0 && gap > 0) {
98963
+ ctx.chunks.push('\n'.repeat(gap));
98964
+ }
98936
98965
  ctx.chunks.push(this.sliceSerialize(token));
98966
+ ctx.lastEndLine = token.end.line;
98967
+ }
98937
98968
  }
98938
98969
  function exitJsxTable(token) {
98939
98970
  const ctx = jsx_table_contextMap.get(token);
98940
98971
  const node = this.stack[this.stack.length - 1];
98941
98972
  if (ctx) {
98942
- node.value = ctx.chunks.join('\n');
98973
+ node.value = ctx.chunks.join('');
98943
98974
  jsx_table_contextMap.delete(token);
98944
98975
  }
98945
98976
  this.exit(token);