@readme/markdown 14.5.0 → 14.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.node.js CHANGED
@@ -24835,10 +24835,12 @@ const LightboxPortal = ({ children }) => {
24835
24835
  return (0,external_react_dom_namespaceObject.createPortal)(children, document.body);
24836
24836
  };
24837
24837
  const Image = (Props) => {
24838
- const { align = '', alt = '', border: borderProp = false, caption, className = '', framed: framedProp = false, height = 'auto', src, title = '', width = 'auto', lazy = true, children, } = Props;
24838
+ const { align = '', alt = '', border: borderProp = false, caption, className = '', framed: framedProp = false, height = 'auto', src, title = '', width = 'auto', lazy = true, children, wrap: wrapProp, } = Props;
24839
24839
  // Normalize border/framed: MDXish passes {false} as the string "false", not a boolean
24840
24840
  const border = borderProp === true || borderProp === 'true';
24841
24841
  const framed = framedProp === true || framedProp === 'true';
24842
+ // Default (undefined) keeps legacy behavior: left/right images float and wrap text.
24843
+ const noWrap = (align === 'left' || align === 'right') && (wrapProp === false || wrapProp === 'false');
24842
24844
  const [lightbox, setLightbox] = external_react_.useState(false);
24843
24845
  if (className === 'emoji') {
24844
24846
  return external_react_.createElement("img", { alt: alt, height: height, loading: lazy ? 'lazy' : 'eager', src: src, title: title, width: width });
@@ -24868,7 +24870,8 @@ const Image = (Props) => {
24868
24870
  setLightbox(!lightbox);
24869
24871
  };
24870
24872
  // Framed images center the <img> itself; outer wrapper handles left/right alignment via text-align.
24871
- const imgElement = (external_react_.createElement("img", { alt: alt, className: `img ${caption || children || framed ? 'img-align-center' : align ? `img-align-${align}` : ''} ${border ? 'border' : ''}`, height: height, loading: lazy ? 'lazy' : 'eager', src: src, title: title, width: width }));
24873
+ const imgClass = `img ${caption || children || framed ? 'img-align-center' : align ? `img-align-${align}` : ''} ${border ? 'border' : ''}${noWrap ? ' img-no-wrap' : ''}`;
24874
+ const imgElement = (external_react_.createElement("img", { alt: alt, className: imgClass, height: height, loading: lazy ? 'lazy' : 'eager', src: src, title: title, width: width }));
24872
24875
  const closedLightbox = (ariaLabel, content) => (external_react_.createElement("span", { "aria-label": ariaLabel, className: "img lightbox closed", onClick: toggle, onKeyDown: handleKeyDown, role: 'button', tabIndex: 0 },
24873
24876
  external_react_.createElement("span", { className: "lightbox-inner" }, content)));
24874
24877
  const lightboxOverlay = lightbox ? (external_react_.createElement(LightboxPortal, null,
@@ -24880,10 +24883,10 @@ const Image = (Props) => {
24880
24883
  external_react_.createElement("button", { "aria-label": "Minimize image", className: "lightbox-close", onClick: toggle, type: "button" },
24881
24884
  external_react_.createElement("i", { "aria-hidden": "true", className: "fa-solid fa-xmark" }))))) : null;
24882
24885
  if (framed) {
24883
- const frameClass = `img-frame img-frame-${align || 'center'}`;
24884
- // Left/right frames shrink to fit, so percentage widths can't resolve
24885
- // against the parent, hoist onto the wrapper. Center frames are full-width.
24886
- const isClamped = align === 'left' || align === 'right';
24886
+ const frameClass = `img-frame img-frame-${align || 'center'}${noWrap ? ' img-no-wrap' : ''}`;
24887
+ // Only left/right wrapping frames shrink to fit; for those, hoist percentage
24888
+ // widths onto the wrapper since they can't resolve against a shrink-to-fit parent.
24889
+ const isClamped = (align === 'left' || align === 'right') && !noWrap;
24887
24890
  const frameStyle = isClamped && typeof width === 'string' && width.endsWith('%') ? { width } : undefined;
24888
24891
  if (children || caption) {
24889
24892
  return (external_react_.createElement("figure", { className: frameClass, style: frameStyle },
@@ -24896,11 +24899,20 @@ const Image = (Props) => {
24896
24899
  lightboxOverlay));
24897
24900
  }
24898
24901
  if (children || caption) {
24899
- return (external_react_.createElement("figure", null,
24900
- closedLightbox(alt || 'Expand image', external_react_.createElement(external_react_.Fragment, null,
24901
- imgElement,
24902
- external_react_.createElement("figcaption", null, children || caption))),
24903
- lightboxOverlay));
24902
+ // Mirrors the framed pattern: left/right captioned figures float and shrink
24903
+ // to fit so a long caption doesn't widen the float past the image.
24904
+ const isFloating = (align === 'left' || align === 'right') && !noWrap;
24905
+ const figureClass = [
24906
+ (align === 'left' || align === 'right') && `img-figure-${align}`,
24907
+ noWrap && 'img-no-wrap',
24908
+ ]
24909
+ .filter(Boolean)
24910
+ .join(' ');
24911
+ const figureStyle = isFloating && typeof width === 'string' && width.endsWith('%') ? { width } : undefined;
24912
+ return (external_react_.createElement("figure", { className: figureClass || undefined, style: figureStyle },
24913
+ closedLightbox(alt || 'Expand image', imgElement),
24914
+ lightboxOverlay,
24915
+ external_react_.createElement("figcaption", null, children || caption)));
24904
24916
  }
24905
24917
  return (external_react_.createElement(external_react_.Fragment, null,
24906
24918
  closedLightbox('Expand image', imgElement),
@@ -25679,6 +25691,7 @@ const INLINE_COMPONENT_TAGS = new Set(['Anchor', 'Glossary']);
25679
25691
  /**
25680
25692
  * PascalCase tags that have their own dedicated tokenizer / transformer
25681
25693
  * and must not be claimed by the generic `mdxComponent` construct.
25694
+ * Subject to change as we add more dedicated tokenizers.
25682
25695
  */
25683
25696
  const DEDICATED_COMPONENT_TAGS = ['HTMLBlock', 'Table'];
25684
25697
  /**
@@ -25689,6 +25702,14 @@ const GENERIC_MDX_COMPONENT_EXCLUDED_TAGS = new Set([
25689
25702
  ...DEDICATED_COMPONENT_TAGS,
25690
25703
  ...INLINE_COMPONENT_TAGS,
25691
25704
  ]);
25705
+ /**
25706
+ * Tags the micromark `mdxComponent` tokenizer must not claim, which
25707
+ * are inline components and those that have their own dedicated tokenizer
25708
+ */
25709
+ const TOKENIZER_MDX_COMPONENT_EXCLUDED_TAGS = new Set([
25710
+ 'Table',
25711
+ ...INLINE_COMPONENT_TAGS,
25712
+ ]);
25692
25713
  /**
25693
25714
  * Lowercased variant of {@link INLINE_COMPONENT_TAGS} for consumers that
25694
25715
  * run after rehype (where hast `tagName` is normalized to lowercase).
@@ -74734,18 +74755,28 @@ const isMDXEsm = (node) => {
74734
74755
  * Takes an HTML string and formats it for display in the editor. Removes leading/trailing newlines
74735
74756
  * and unindents the HTML.
74736
74757
  *
74737
- * @param {string} html - HTML content from template literal
74758
+ * @param {string} html - cooked HTML payload (callers strip any template-literal backticks first)
74759
+ * @param {number} [openingTagIndent=0] - column the `<HTMLBlock>` opening tag sits at, used to
74760
+ * dedent each content line so its indentation reads relative to the tag, not the line start
74738
74761
  * @returns {string} processed HTML
74739
74762
  */
74740
- function formatHtmlForMdxish(html) {
74741
- // Remove leading/trailing backticks if present, since they're used to keep the HTML
74742
- // from being parsed prematurely
74743
- let processed = html;
74744
- if (processed.startsWith('`') && processed.endsWith('`')) {
74745
- processed = processed.slice(1, -1);
74746
- }
74763
+ function formatHtmlForMdxish(html, openingTagIndent = 0) {
74747
74764
  // Removes the leading/trailing newlines
74748
- let cleaned = processed.replace(/^\s*\n|\n\s*$/g, '');
74765
+ let cleaned = html.replace(/^\s*\n|\n\s*$/g, '');
74766
+ // Strip / deindent the lines in the HTML string so that the indents are relative
74767
+ // to the opening HTMLBlock tag, not the literal line start
74768
+ // Keep any deeper indent
74769
+ if (openingTagIndent > 0) {
74770
+ cleaned = cleaned
74771
+ .split('\n')
74772
+ .map(line => {
74773
+ let i = 0;
74774
+ while (i < openingTagIndent && (line[i] === ' ' || line[i] === '\t'))
74775
+ i += 1;
74776
+ return line.slice(i);
74777
+ })
74778
+ .join('\n');
74779
+ }
74749
74780
  // Convert literal \n sequences to actual newlines only inside <pre> and <code>.
74750
74781
  // Because <pre> needs to respect the newline visual and
74751
74782
  // escape characters should be processed in the <code> tag.
@@ -95902,11 +95933,15 @@ const symmetrizePair = (html, { openStart, openEnd, closeStart, closeEnd }) => {
95902
95933
  const postCloser = html.slice(closeEnd, closeLine.end);
95903
95934
  const openerHasExtras = preOpener.trim().length > 0 || postOpener.trim().length > 0;
95904
95935
  const closerHasExtras = preCloser.trim().length > 0 || postCloser.trim().length > 0;
95905
- // Both match (both bare or both attached) — mdxjs parses this fine.
95906
- if (openerHasExtras === closerHasExtras)
95936
+ // A blank line splits opener/closer into separate paragraphs.
95937
+ // If either tag is attached to surrounding text, mdxjs can fail to match.
95938
+ const spansBlankLine = /\n[^\S\n]*\n/.test(html.slice(openEnd, closeStart));
95939
+ // If both sides are already symmetric, keep as-is.
95940
+ // Exception: attached + blank-line-split pairs still need normalization.
95941
+ if (openerHasExtras === closerHasExtras && !(openerHasExtras && spansBlankLine))
95907
95942
  return [];
95908
- // Asymmetric. Push non-tag content on the offending side to its own line,
95909
- // visually aligning the inserted line with the existing whitespace prefix.
95943
+ // For asymmetric (or attached-but-split) pairs, move side content to its own
95944
+ // line so opener/closer become line-symmetric.
95910
95945
  const indentFor = (linePrefix) => linePrefix.match(/^\s*/)?.[0] ?? '';
95911
95946
  const inserts = [];
95912
95947
  if (openerHasExtras) {
@@ -96352,6 +96387,7 @@ const repairUnclosedTags = (html) => {
96352
96387
 
96353
96388
 
96354
96389
 
96390
+
96355
96391
 
96356
96392
 
96357
96393
  const isTableCell = (node) => isMDXElement(node) && ['th', 'td'].includes(node.name);
@@ -96452,6 +96488,15 @@ const processTableNode = (node, index, parent, documentPosition) => {
96452
96488
  const { align: alignAttr } = getAttrs(node);
96453
96489
  const align = Array.isArray(alignAttr) ? alignAttr : null;
96454
96490
  let tableHasFlowContent = false;
96491
+ // An `<HTMLBlock>` (still a JSX element here; converted to `html-block` by
96492
+ // `mdxishHtmlBlocks` after this transformer) is block-level content that a
96493
+ // markdown table cell can't represent, so keep the table as a JSX `<Table>`.
96494
+ visit(node, candidate => candidate.type === NodeTypes.htmlBlock ||
96495
+ ((candidate.type === 'mdxJsxFlowElement' || candidate.type === 'mdxJsxTextElement') &&
96496
+ candidate.name === 'HTMLBlock'), () => {
96497
+ tableHasFlowContent = true;
96498
+ return EXIT;
96499
+ });
96455
96500
  // Re-parse text-only cells through markdown and detect flow content
96456
96501
  visit(node, isTableCell, (cell) => {
96457
96502
  if (!isTextOnly(cell.children))
@@ -121092,7 +121137,7 @@ function createTokenize(mode) {
121092
121137
  return tagNameRest;
121093
121138
  }
121094
121139
  // Tag name complete — check exclusions
121095
- if (GENERIC_MDX_COMPONENT_EXCLUDED_TAGS.has(tagName)) {
121140
+ if (TOKENIZER_MDX_COMPONENT_EXCLUDED_TAGS.has(tagName)) {
121096
121141
  return nok(code);
121097
121142
  }
121098
121143
  depth = 1;
@@ -123183,557 +123228,158 @@ const magicBlockTransformer = (options = {}) => tree => {
123183
123228
  };
123184
123229
  /* harmony default export */ const magic_block_transformer = (magicBlockTransformer);
123185
123230
 
123186
- ;// ./lib/micromark/jsx-comment/pattern.ts
123187
- /**
123188
- * Matches a JSX comment: `{/*`, content, `*\/}` — no whitespace tolerated
123189
- * between the braces and the comment markers.
123190
- *
123191
- * This grammar is mirrored by the flow tokenizer in ./syntax.ts. Any change
123192
- * here needs a mirror change in the state machine; the parity test in
123193
- * __tests__/lib/micromark/jsx-comment-pattern-parity.test.ts locks the two
123194
- * together so they can't silently drift.
123195
- */
123196
- const JSX_COMMENT_REGEX = /\{\/\*[^*]*(?:\*(?!\/)[^*]*)*\*\/\}/g;
123231
+ ;// ./processor/transform/mdxish/mdxish-html-blocks.ts
123197
123232
 
123198
- ;// ./processor/transform/mdxish/preprocess-jsx-expressions.ts
123199
123233
 
123200
123234
 
123201
- // Base64 encode (Node.js + browser compatible)
123202
- function base64Encode(str) {
123203
- if (typeof Buffer !== 'undefined') {
123204
- return Buffer.from(str, 'utf-8').toString('base64');
123205
- }
123206
- return btoa(unescape(encodeURIComponent(str)));
123207
- }
123208
- // Base64 decode (Node.js + browser compatible)
123209
- function base64Decode(str) {
123210
- if (typeof Buffer !== 'undefined') {
123211
- return Buffer.from(str, 'base64').toString('utf-8');
123212
- }
123213
- return decodeURIComponent(escape(atob(str)));
123214
- }
123215
- // Markers for protected HTMLBlock content (HTML comments avoid markdown parsing issues)
123216
- const HTML_BLOCK_CONTENT_START = '<!--RDMX_HTMLBLOCK:';
123217
- const HTML_BLOCK_CONTENT_END = ':RDMX_HTMLBLOCK-->';
123235
+ // `<HTMLBlock …>{`…`}</HTMLBlock>` embedded inside a raw HTML block (e.g. a
123236
+ // single-line `<div>…</div>`). CommonMark slurps the whole div as one `html`
123237
+ // node, so the tokenizer never sees the HTMLBlock — we recover it here.
123238
+ const RAW_HTML_BLOCK_RE = /<HTMLBlock\b([^>]*)>\s*\{\s*`((?:[^`\\]|\\.)*)`\s*\}\s*<\/HTMLBlock>/g;
123239
+ // Opening `<HTMLBlock …>` as its own `html` node — produced inside a paragraph
123240
+ // when an HTMLBlock appears inline alongside text.
123241
+ const HTML_BLOCK_OPEN_RE = /^<HTMLBlock\b([^>]*)>$/;
123218
123242
  /**
123219
- * Base64 encodes HTMLBlock template literal content to prevent markdown parser from consuming <script>/<style> tags.
123220
- *
123221
- * @param content
123222
- * @returns Content with HTMLBlock template literals base64 encoded in HTML comments
123223
- * @example
123224
- * ```typescript
123225
- * const input = '<HTMLBlock>{`<script>alert("xss")</script>`}</HTMLBlock>';
123226
- * protectHTMLBlockContent(input)
123227
- * // Returns: '<HTMLBlock><!--RDMX_HTMLBLOCK:PHNjcmlwdD5hbGVydCgieHNzIik8L3NjcmlwdD4=:RDMX_HTMLBLOCK--></HTMLBlock>'
123228
- * ```
123243
+ * Builds the canonical `html-block` MDAST node the renderer expects.
123229
123244
  */
123230
- function protectHTMLBlockContent(content) {
123231
- return content.replace(/(<HTMLBlock[^>]*>)\{\s*`((?:[^`\\]|\\.)*)`\s*\}(<\/HTMLBlock>)/g, (_match, openTag, templateContent, closeTag) => {
123232
- const encoded = base64Encode(templateContent);
123233
- return `${openTag}${HTML_BLOCK_CONTENT_START}${encoded}${HTML_BLOCK_CONTENT_END}${closeTag}`;
123234
- });
123235
- }
123245
+ const createHtmlBlockNode = (html, position, runScripts, safeMode) => ({
123246
+ position,
123247
+ children: [{ type: 'text', value: html }],
123248
+ type: NodeTypes.htmlBlock,
123249
+ data: {
123250
+ hName: 'html-block',
123251
+ hProperties: {
123252
+ html,
123253
+ ...(runScripts !== undefined && { runScripts }),
123254
+ ...(safeMode !== undefined && { safeMode }),
123255
+ },
123256
+ },
123257
+ });
123236
123258
  /**
123237
- * Removes JSX-style comments (e.g., { /* comment *\/ }) from content.
123238
- *
123239
- * @param content
123240
- * @returns Content with JSX comments removed
123241
- * @example
123242
- * ```typescript
123243
- * removeJSXComments('Text { /* comment *\/ } more text')
123244
- * // Returns: 'Text more text'
123245
- * ```
123259
+ * Reads the cooked string out of a brace expression wrapping a single template
123260
+ * literal (`` `<p>n</p>` `` → `<p>n</p>`).
123246
123261
  */
123247
- function removeJSXComments(content) {
123248
- return content.replace(JSX_COMMENT_REGEX, '');
123249
- }
123250
- const HTML_ELEM_PLACEHOLDER_PREFIX = '___MDXISH_HTML_ELEM_';
123251
- const HTML_ELEM_PLACEHOLDER = new RegExp(`${HTML_ELEM_PLACEHOLDER_PREFIX}(\\d+)___`, 'g');
123252
- // Matches an HTML element that starts at a line boundary and ends at a line boundary.
123253
- // Allows optional leading indentation and lazily matches until the same closing tag.
123254
- const BLOCK_HTML_RE = /(?<=^|\n)[ \t]*<([a-z][a-zA-Z0-9]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>[ \t]*(?=\n|$)/g;
123262
+ const extractTemplateLiteral = (value) => {
123263
+ if (!value)
123264
+ return '';
123265
+ const match = value.trim().match(/^`([\s\S]*)`$/);
123266
+ // Non-template-literal bodies (e.g. `{someVar}`) are malformed mdxish input;
123267
+ // returning '' beats shipping JS identifier source as an HTML payload.
123268
+ return match ? match[1] : '';
123269
+ };
123270
+ const toRunScripts = (raw) => raw === 'true' ? true : raw === 'false' ? false : raw;
123271
+ /** Reads an attribute from a raw `<HTMLBlock …>` attribute string. */
123272
+ const rawAttr = (attrs, name) => {
123273
+ const quoted = attrs.match(new RegExp(`\\b${name}\\s*=\\s*"([^"]*)"`));
123274
+ if (quoted)
123275
+ return quoted[1];
123276
+ const expr = attrs.match(new RegExp(`\\b${name}\\s*=\\s*\\{(true|false)\\}`));
123277
+ if (expr)
123278
+ return expr[1];
123279
+ return new RegExp(`\\b${name}\\b`).test(attrs) ? 'true' : undefined;
123280
+ };
123281
+ /** Reads an attribute from a parsed `<HTMLBlock>` JSX element. */
123282
+ const jsxAttr = (element, name) => {
123283
+ const attr = element.attributes.find(a => a.type === 'mdxJsxAttribute' && a.name === name);
123284
+ if (!attr || attr.type !== 'mdxJsxAttribute')
123285
+ return undefined;
123286
+ if (typeof attr.value === 'string')
123287
+ return attr.value;
123288
+ if (attr.value && typeof attr.value === 'object' && 'value' in attr.value)
123289
+ return attr.value.value;
123290
+ return 'true'; // bare boolean attribute, e.g. <HTMLBlock runScripts />
123291
+ };
123292
+ /** Builds an `html-block` from a raw attribute string and (unparsed) body. */
123293
+ const htmlBlockFromRaw = (attrs, html, position, openingTagIndent = 0) => createHtmlBlockNode(formatHtmlForMdxish(html, openingTagIndent), position, toRunScripts(rawAttr(attrs, 'runScripts')), rawAttr(attrs, 'safeMode'));
123255
123294
  /**
123256
- * Hides line-anchored HTML elements from the brace-escaping pass so we don't leak `\{`
123257
- * into rendered output (rehypeRaw renders the `\` literally, e.g. `<div>{foo</div>`).
123295
+ * Splits a raw `html` node that embeds one or more `<HTMLBlock>`s into
123296
+ * `[html before, html-block, html after, …]`. Returns null when there is none.
123258
123297
  *
123259
- * One carve-out: if an interior line at column 0 has bare text containing `{`, mdxish
123260
- * parses that line as a paragraph and the mdxExpression step would throw without an
123261
- * escape — so we leave that case to the brace balancer.
123262
- */
123263
- function protectHTMLElements(content) {
123264
- const htmlElements = [];
123265
- const protectedContent = content.replace(BLOCK_HTML_RE, match => {
123266
- // Look at the lines between the open and close tags. If any of them starts
123267
- // at column 0 with bare text (not whitespace, not another tag) and contains
123268
- // `{`, mdxish will parse that line as a paragraph and the brace as an MDX
123269
- // expression, which would throw an error. So we let the brace balancer escape it.
123270
- // Otherwise, we need to extract the sequence to protect it from the brace escaping.
123271
- const interior = match.split('\n').slice(1, -1);
123272
- const hazard = interior.some(line => line.length > 0 && line[0] !== ' ' && line[0] !== '\t' && line[0] !== '<' && line.includes('{'));
123273
- if (hazard)
123274
- return match;
123275
- htmlElements.push(match);
123276
- return `${HTML_ELEM_PLACEHOLDER_PREFIX}${htmlElements.length - 1}___`;
123277
- });
123278
- return { htmlElements, protectedContent };
123279
- }
123280
- function restoreHTMLElements(content, htmlElements) {
123281
- if (htmlElements.length === 0)
123282
- return content;
123283
- return content.replace(HTML_ELEM_PLACEHOLDER, (_m, idx) => htmlElements[parseInt(idx, 10)]);
123284
- }
123285
- /**
123286
- * Escapes unbalanced and paragraph-spanning braces so MDX doesn't trip on them.
123298
+ * `String.split` on a regex with capture groups interleaves the captures into
123299
+ * the result, so segments arrive as `[text, attrs, body, text, attrs, body, …]`.
123287
123300
  */
123288
- function escapeProblematicBraces(content) {
123289
- const { htmlElements, protectedContent } = protectHTMLElements(content);
123290
- let strDelim = null;
123291
- let strEscaped = false;
123292
- // Track position of last newline (outside strings) to detect blank lines
123293
- // -2 means no recent newline
123294
- let lastNewlinePos = -2;
123295
- // Character state machine trackers
123296
- const toEscape = new Set();
123297
- // Convert to array of Unicode code points so that emojis and multi-byte characters are correctly tracked
123298
- const chars = Array.from(protectedContent);
123299
- const openStack = [];
123300
- for (let i = 0; i < chars.length; i += 1) {
123301
- const ch = chars[i];
123302
- // Track string delimiters inside expressions to ignore braces within them
123303
- if (openStack.length > 0) {
123304
- if (strDelim) {
123305
- if (strEscaped)
123306
- strEscaped = false;
123307
- else if (ch === '\\')
123308
- strEscaped = true;
123309
- else if (ch === strDelim)
123310
- strDelim = null;
123311
- // eslint-disable-next-line no-continue
123312
- continue;
123313
- }
123314
- if (ch === '"' || ch === "'" || ch === '`') {
123315
- strDelim = ch;
123316
- // eslint-disable-next-line no-continue
123317
- continue;
123318
- }
123319
- if (ch === '\n') {
123320
- if (lastNewlinePos >= 0) {
123321
- const between = chars.slice(lastNewlinePos + 1, i).join('');
123322
- if (/^[ \t]*$/.test(between)) {
123323
- openStack.forEach(entry => { entry.hasBlankLine = true; });
123324
- }
123325
- }
123326
- lastNewlinePos = i;
123327
- }
123328
- }
123329
- // Skip already-escaped braces (odd run of preceding backslashes).
123330
- if (ch === '{' || ch === '}') {
123331
- let bs = 0;
123332
- for (let j = i - 1; j >= 0 && chars[j] === '\\'; j -= 1)
123333
- bs += 1;
123334
- if (bs % 2 === 1) {
123335
- // eslint-disable-next-line no-continue
123336
- continue;
123337
- }
123338
- }
123339
- if (ch === '{') {
123340
- // `=` (after whitespace) before `{` ⇒ JSX attribute expression. The
123341
- // mdxComponent tokenizer captures the whole component, so blank lines
123342
- // inside attribute values are harmless. Nested `{` inherits the flag.
123343
- let isAttrExpr = false;
123344
- for (let j = i - 1; j >= 0; j -= 1) {
123345
- const pc = chars[j];
123346
- if (pc === '=') {
123347
- isAttrExpr = true;
123348
- break;
123349
- }
123350
- if (pc !== ' ' && pc !== '\t')
123351
- break;
123352
- }
123353
- // Nested `{ ... }` inside an attribute value (e.g. `data={[{ ... }]}` or
123354
- // `data={{ a: { b: 1 } }}`) must inherit the same exemption; only the
123355
- // outer `{` is directly after `=`.
123356
- if (!isAttrExpr && openStack.length > 0 && openStack[openStack.length - 1].isAttrExpr) {
123357
- isAttrExpr = true;
123358
- }
123359
- openStack.push({ pos: i, hasBlankLine: false, isAttrExpr });
123360
- lastNewlinePos = -2;
123361
- }
123362
- else if (ch === '}') {
123363
- if (openStack.length > 0) {
123364
- const entry = openStack.pop();
123365
- // Pure `{/* ... */}` comments are handled downstream by the jsxComment
123366
- // tokenizer — escaping their braces would prevent it from running.
123367
- const isPureJsxComment = chars[entry.pos + 1] === '/' &&
123368
- chars[entry.pos + 2] === '*' &&
123369
- chars[i - 1] === '/' &&
123370
- chars[i - 2] === '*';
123371
- if (entry.hasBlankLine && !isPureJsxComment && !entry.isAttrExpr) {
123372
- toEscape.add(entry.pos);
123373
- toEscape.add(i);
123374
- }
123375
- }
123376
- else {
123377
- toEscape.add(i);
123378
- }
123301
+ const splitRawHtmlBlocks = (node) => {
123302
+ const segments = node.value.split(RAW_HTML_BLOCK_RE);
123303
+ if (segments.length === 1)
123304
+ return null; // no <HTMLBlock> present
123305
+ const parts = [];
123306
+ for (let i = 0; i < segments.length; i += 3) {
123307
+ const [text, attrs, body] = segments.slice(i, i + 3);
123308
+ if (text)
123309
+ parts.push({ type: 'html', value: text });
123310
+ if (body !== undefined) {
123311
+ // The opening tag's column equals the length of the line it starts on
123312
+ // (the text run since the previous newline preceding the match).
123313
+ const openingTagIndent = text.slice(text.lastIndexOf('\n') + 1).length;
123314
+ parts.push(htmlBlockFromRaw(attrs, body, node.position, openingTagIndent));
123379
123315
  }
123380
123316
  }
123381
- // Anything still open is unbalanced.
123382
- openStack.forEach(entry => toEscape.add(entry.pos));
123383
- // Reconstruct the content with the escaped braces.
123384
- const escapedContent = toEscape.size === 0 ? protectedContent : chars.map((ch, i) => (toEscape.has(i) ? `\\${ch}` : ch)).join('');
123385
- return restoreHTMLElements(escapedContent, htmlElements);
123386
- }
123317
+ return parts;
123318
+ };
123387
123319
  /**
123388
- * Preprocesses JSX-like markdown content before parsing.
123320
+ * Converts every `<HTMLBlock>` shape that survives parsing into the canonical
123321
+ * `html-block` MDAST node, reading the body from the tokenizer's template-literal
123322
+ * expression. Three shapes occur:
123389
123323
  *
123390
- * JSX attribute expressions (`href={baseUrl}`) are no longer rewritten here —
123391
- * they flow through the tokenizer as `mdxJsxAttributeValueExpression` nodes
123392
- * and are evaluated at the hast handler step.
123324
+ * 1. JSX element (`mdxJsxFlowElement`/`mdxJsxTextElement`) multiline/block
123325
+ * context and table cells (after their remarkMdx re-parse).
123326
+ * 2. Raw `html` blob (`splitRawHtmlBlocks`) single-line top-level, or nested
123327
+ * in raw HTML like an inline `<div>`.
123328
+ * 3. Inline-in-paragraph — split into `html` + expression + `html` siblings.
123393
123329
  *
123394
- * @param content
123395
- * @returns Preprocessed content ready for markdown parsing
123396
- */
123397
- function preprocessJSXExpressions(content) {
123398
- let processed = protectHTMLBlockContent(content);
123399
- const { protectedCode, protectedContent } = protectCodeBlocks(processed);
123400
- processed = escapeProblematicBraces(protectedContent);
123401
- processed = restoreCodeBlocks(processed, protectedCode);
123402
- return processed;
123403
- }
123404
-
123405
- ;// ./processor/transform/mdxish/mdxish-html-blocks.ts
123406
-
123407
-
123408
-
123409
-
123410
- /**
123411
- * Decodes HTMLBlock content that was protected during preprocessing.
123412
- * Content is wrapped in <!--RDMX_HTMLBLOCK:base64:RDMX_HTMLBLOCK-->
123413
- */
123414
- function decodeProtectedContent(content) {
123415
- // Escape special regex characters in the markers
123416
- const startEscaped = HTML_BLOCK_CONTENT_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
123417
- const endEscaped = HTML_BLOCK_CONTENT_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
123418
- const markerRegex = new RegExp(`${startEscaped}([A-Za-z0-9+/=]+)${endEscaped}`, 'g');
123419
- return content.replace(markerRegex, (_match, encoded) => {
123420
- try {
123421
- return base64Decode(encoded);
123422
- }
123423
- catch {
123424
- return encoded;
123425
- }
123426
- });
123427
- }
123428
- /**
123429
- * Collects text content from a node and its children recursively
123430
- */
123431
- function collectTextContent(node) {
123432
- const parts = [];
123433
- if (node.type === 'text' && node.value) {
123434
- parts.push(node.value);
123435
- }
123436
- else if (node.type === 'html' && node.value) {
123437
- parts.push(node.value);
123438
- }
123439
- else if (node.type === 'inlineCode' && node.value) {
123440
- parts.push(node.value);
123441
- }
123442
- else if (node.type === 'code' && node.value) {
123443
- // Reconstruct code fence syntax (markdown parser consumes opening ```)
123444
- const lang = node.lang || '';
123445
- const fence = `\`\`\`${lang ? `${lang}\n` : ''}`;
123446
- parts.push(fence);
123447
- parts.push(node.value);
123448
- // Add newline before closing fence if missing
123449
- const closingFence = node.value.endsWith('\n') ? '```' : '\n```';
123450
- parts.push(closingFence);
123451
- }
123452
- else if (node.children && Array.isArray(node.children)) {
123453
- node.children.forEach(child => {
123454
- if (typeof child === 'object' && child !== null) {
123455
- parts.push(collectTextContent(child));
123456
- }
123457
- });
123458
- }
123459
- return parts.join('');
123460
- }
123461
- /**
123462
- * Extracts boolean attribute from HTML tag. Handles JSX (safeMode={true}) and string (safeMode="true") syntax.
123463
- * Returns "true"/"false" string to survive rehypeRaw serialization.
123464
- */
123465
- function extractBooleanAttr(attrs, name) {
123466
- // Try JSX syntax: name={true|false}
123467
- const jsxMatch = attrs.match(new RegExp(`${name}=\\{(true|false)\\}`));
123468
- if (jsxMatch) {
123469
- return jsxMatch[1];
123470
- }
123471
- // Try string syntax: name="true"|true
123472
- const stringMatch = attrs.match(new RegExp(`${name}="?(true|false)"?`));
123473
- if (stringMatch) {
123474
- return stringMatch[1];
123475
- }
123476
- return undefined;
123477
- }
123478
- /**
123479
- * Extracts runScripts attribute from HTML tag. Returns boolean for "true"/"false", string for other values, or undefined if not found.
123480
- */
123481
- function extractRunScriptsAttr(attrs) {
123482
- const runScriptsMatch = attrs.match(/runScripts="?([^">\s]+)"?/);
123483
- if (!runScriptsMatch) {
123484
- return undefined;
123485
- }
123486
- const value = runScriptsMatch[1];
123487
- if (value === 'true') {
123488
- return true;
123489
- }
123490
- if (value === 'false') {
123491
- return false;
123492
- }
123493
- return value;
123494
- }
123495
- /**
123496
- * Creates an HTMLBlock node from HTML string and optional attributes
123497
- */
123498
- function createHTMLBlockNode(htmlString, position, runScripts, safeMode) {
123499
- return {
123500
- position,
123501
- children: [{ type: 'text', value: htmlString }],
123502
- type: NodeTypes.htmlBlock,
123503
- data: {
123504
- hName: 'html-block',
123505
- hProperties: {
123506
- html: htmlString,
123507
- ...(runScripts !== undefined && { runScripts }),
123508
- ...(safeMode !== undefined && { safeMode }),
123509
- },
123510
- },
123511
- };
123512
- }
123513
- /**
123514
- * Checks for opening tag only (for split detection)
123515
- */
123516
- function hasOpeningTagOnly(node) {
123517
- let hasOpening = false;
123518
- let hasClosed = false;
123519
- let attrs = '';
123520
- const check = (n) => {
123521
- if (n.type === 'html' && n.value) {
123522
- if (n.value === '<HTMLBlock>') {
123523
- hasOpening = true;
123524
- }
123525
- else {
123526
- const match = n.value.match(/^<HTMLBlock(\s[^>]*)?>$/);
123527
- if (match) {
123528
- hasOpening = true;
123529
- attrs = match[1] || '';
123530
- }
123531
- }
123532
- if (n.value === '</HTMLBlock>' || n.value.includes('</HTMLBlock>')) {
123533
- hasClosed = true;
123534
- }
123535
- }
123536
- if (n.children && Array.isArray(n.children)) {
123537
- n.children.forEach(child => {
123538
- check(child);
123539
- });
123540
- }
123541
- };
123542
- check(node);
123543
- // Return true only if opening without closing (split case)
123544
- return { attrs, found: hasOpening && !hasClosed };
123545
- }
123546
- /**
123547
- * Checks if a node contains an HTMLBlock closing tag
123548
- */
123549
- function hasClosingTag(node) {
123550
- if (node.type === 'html' && node.value) {
123551
- if (node.value === '</HTMLBlock>' || node.value.includes('</HTMLBlock>'))
123552
- return true;
123553
- }
123554
- if (node.children && Array.isArray(node.children)) {
123555
- return node.children.some(child => hasClosingTag(child));
123556
- }
123557
- return false;
123558
- }
123559
- /**
123560
- * Transforms HTMLBlock MDX JSX to html-block nodes. Handles <HTMLBlock>{`...`}</HTMLBlock> syntax.
123330
+ * Runs *after* `mdxishTables` so table cells are re-parsed first;
123331
+ * `mdxishTables` recognizes the still-JSX `<HTMLBlock>` element when deciding to
123332
+ * keep a table as a JSX `<Table>`. This replaces the old base64-comment marker
123333
+ * machinery — the #1455 tokenizer hands the body over already parsed.
123561
123334
  */
123562
123335
  const mdxishHtmlBlocks = () => tree => {
123563
- // Handle HTMLBlock split across root children (caused by newlines)
123564
- visit(tree, 'root', (root) => {
123565
- const children = root.children;
123566
- let i = 0;
123567
- while (i < children.length) {
123568
- const child = children[i];
123569
- const { attrs, found: hasOpening } = hasOpeningTagOnly(child);
123570
- if (hasOpening) {
123571
- // Find closing tag in subsequent siblings
123572
- let closingIdx = -1;
123573
- for (let j = i + 1; j < children.length; j += 1) {
123574
- if (hasClosingTag(children[j])) {
123575
- closingIdx = j;
123576
- break;
123577
- }
123578
- }
123579
- if (closingIdx !== -1) {
123580
- // Collect inner content between tags
123581
- const contentParts = [];
123582
- for (let j = i; j <= closingIdx; j += 1) {
123583
- const node = children[j];
123584
- contentParts.push(collectTextContent(node));
123585
- }
123586
- // Remove the opening/closing tags and template literal syntax from content
123587
- let content = contentParts.join('');
123588
- content = content.replace(/^<HTMLBlock[^>]*>\s*\{?\s*`?/, '').replace(/`?\s*\}?\s*<\/HTMLBlock>$/, '');
123589
- // Decode protected content that was base64 encoded during preprocessing
123590
- content = decodeProtectedContent(content);
123591
- const htmlString = formatHtmlForMdxish(content);
123592
- const runScripts = extractRunScriptsAttr(attrs);
123593
- const safeMode = extractBooleanAttr(attrs, 'safeMode');
123594
- // Replace range with single HTMLBlock node
123595
- const mdNode = createHTMLBlockNode(htmlString, children[i].position, runScripts, safeMode);
123596
- root.children.splice(i, closingIdx - i + 1, mdNode);
123597
- }
123598
- }
123599
- i += 1;
123600
- }
123336
+ // Shape 1: tokenized JSX element.
123337
+ visit(tree, node => node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement', (node, index, parent) => {
123338
+ const element = node;
123339
+ if (element.name !== 'HTMLBlock' || !parent || index === undefined)
123340
+ return;
123341
+ const exprChild = element.children.find(child => child.type === 'mdxFlowExpression' || child.type === 'mdxTextExpression');
123342
+ const openingTagIndent = (element.position?.start.column ?? 1) - 1;
123343
+ parent.children[index] = createHtmlBlockNode(formatHtmlForMdxish(extractTemplateLiteral(exprChild?.value), openingTagIndent), element.position, toRunScripts(jsxAttr(element, 'runScripts')), jsxAttr(element, 'safeMode'));
123601
123344
  });
123602
- // Handle HTMLBlock parsed as HTML elements (when template literal contains block-level HTML tags)
123345
+ // Shape 2: raw HTML blob.
123603
123346
  visit(tree, 'html', (node, index, parent) => {
123604
123347
  if (!parent || index === undefined)
123605
123348
  return;
123606
- const value = node.value;
123607
- if (!value)
123608
- return;
123609
- // Case 1: Full HTMLBlock in single node
123610
- const fullMatch = value.match(/^<HTMLBlock(\s[^>]*)?>([\s\S]*)<\/HTMLBlock>$/);
123611
- if (fullMatch) {
123612
- const attrs = fullMatch[1] || '';
123613
- let content = fullMatch[2] || '';
123614
- // Remove template literal syntax if present: {`...`}
123615
- content = content.replace(/^\s*\{\s*`/, '').replace(/`\s*\}\s*$/, '');
123616
- // Decode protected content that was base64 encoded during preprocessing
123617
- content = decodeProtectedContent(content);
123618
- const htmlString = formatHtmlForMdxish(content);
123619
- const runScripts = extractRunScriptsAttr(attrs);
123620
- const safeMode = extractBooleanAttr(attrs, 'safeMode');
123621
- parent.children[index] = createHTMLBlockNode(htmlString, node.position, runScripts, safeMode);
123622
- return;
123623
- }
123624
- // Case 2: Opening tag only (split by blank lines)
123625
- if (value === '<HTMLBlock>' || value.match(/^<HTMLBlock\s[^>]*>$/)) {
123626
- const siblings = parent.children;
123627
- let closingIdx = -1;
123628
- // Find closing tag in siblings
123629
- for (let i = index + 1; i < siblings.length; i += 1) {
123630
- const sibling = siblings[i];
123631
- if (sibling.type === 'html') {
123632
- const sibVal = sibling.value;
123633
- if (sibVal === '</HTMLBlock>' || sibVal?.includes('</HTMLBlock>')) {
123634
- closingIdx = i;
123635
- break;
123636
- }
123637
- }
123638
- }
123639
- if (closingIdx === -1)
123640
- return;
123641
- // Collect content between tags, skipping template literal delimiters
123642
- const contentParts = [];
123643
- for (let i = index + 1; i < closingIdx; i += 1) {
123644
- const sibling = siblings[i];
123645
- // Skip template literal delimiters
123646
- if (sibling.type === 'text') {
123647
- const textVal = sibling.value;
123648
- if (textVal === '{' || textVal === '}' || textVal === '{`' || textVal === '`}') {
123649
- // eslint-disable-next-line no-continue
123650
- continue;
123651
- }
123652
- }
123653
- contentParts.push(collectTextContent(sibling));
123654
- }
123655
- // Decode protected content that was base64 encoded during preprocessing
123656
- const decodedContent = decodeProtectedContent(contentParts.join(''));
123657
- const htmlString = formatHtmlForMdxish(decodedContent);
123658
- const runScripts = extractRunScriptsAttr(value);
123659
- const safeMode = extractBooleanAttr(value, 'safeMode');
123660
- // Replace opening tag with HTMLBlock node, remove consumed siblings
123661
- parent.children[index] = createHTMLBlockNode(htmlString, node.position, runScripts, safeMode);
123662
- parent.children.splice(index + 1, closingIdx - index);
123663
- }
123349
+ const replacement = splitRawHtmlBlocks(node);
123350
+ if (replacement)
123351
+ parent.children.splice(index, 1, ...replacement);
123664
123352
  });
123665
- // Handle HTMLBlock inside paragraphs (parsed as inline elements)
123666
- visit(tree, 'paragraph', (node, index, parent) => {
123667
- if (!parent || index === undefined)
123668
- return;
123669
- const children = node.children || [];
123670
- let htmlBlockStartIdx = -1;
123671
- let htmlBlockEndIdx = -1;
123672
- let templateLiteralStartIdx = -1;
123673
- let templateLiteralEndIdx = -1;
123353
+ // Shape 3: inline within a paragraph `<HTMLBlock>` open/close arrive as
123354
+ // separate `html` siblings with the template-literal expression between them.
123355
+ visit(tree, 'paragraph', (paragraph) => {
123356
+ // An html-block is block content, so it isn't a valid PhrasingContent child;
123357
+ // widen to RootContent (which HTMLBlock belongs to) for the in-place splice.
123358
+ const children = paragraph.children;
123674
123359
  for (let i = 0; i < children.length; i += 1) {
123675
- const child = children[i];
123676
- if (child.type === 'html' && typeof child.value === 'string') {
123677
- const value = child.value;
123678
- if (value === '<HTMLBlock>' || value.match(/^<HTMLBlock\s[^>]*>$/)) {
123679
- htmlBlockStartIdx = i;
123680
- }
123681
- else if (value === '</HTMLBlock>') {
123682
- htmlBlockEndIdx = i;
123683
- }
123684
- }
123685
- // Find opening brace after HTMLBlock start
123686
- if (htmlBlockStartIdx !== -1 && templateLiteralStartIdx === -1 && child.type === 'text') {
123687
- const value = child.value;
123688
- if (value === '{') {
123689
- templateLiteralStartIdx = i;
123360
+ const open = children[i];
123361
+ const openMatch = open.type === 'html' ? open.value.match(HTML_BLOCK_OPEN_RE) : null;
123362
+ if (!openMatch)
123363
+ continue; // eslint-disable-line no-continue
123364
+ const closeIdx = children.findIndex((child, j) => j > i && child.type === 'html' && child.value === '</HTMLBlock>');
123365
+ if (closeIdx === -1)
123366
+ continue; // eslint-disable-line no-continue
123367
+ const body = children
123368
+ .slice(i + 1, closeIdx)
123369
+ .map(child => {
123370
+ if (child.type === 'mdxTextExpression' || child.type === 'mdxFlowExpression') {
123371
+ return extractTemplateLiteral(child.value);
123690
123372
  }
123691
- }
123692
- // Find closing brace before HTMLBlock end
123693
- if (htmlBlockStartIdx !== -1 && htmlBlockEndIdx === -1 && child.type === 'text') {
123694
- const value = child.value;
123695
- if (value === '}') {
123696
- templateLiteralEndIdx = i;
123697
- }
123698
- }
123699
- }
123700
- if (htmlBlockStartIdx !== -1 &&
123701
- htmlBlockEndIdx !== -1 &&
123702
- templateLiteralStartIdx !== -1 &&
123703
- templateLiteralEndIdx !== -1 &&
123704
- templateLiteralStartIdx < templateLiteralEndIdx) {
123705
- const openingTag = children[htmlBlockStartIdx];
123706
- // Collect content between braces (handles code blocks)
123707
- const templateContent = [];
123708
- for (let i = templateLiteralStartIdx + 1; i < templateLiteralEndIdx; i += 1) {
123709
- const child = children[i];
123710
- templateContent.push(collectTextContent(child));
123711
- }
123712
- // Decode protected content that was base64 encoded during preprocessing
123713
- const decodedContent = decodeProtectedContent(templateContent.join(''));
123714
- const htmlString = formatHtmlForMdxish(decodedContent);
123715
- const runScripts = openingTag.value ? extractRunScriptsAttr(openingTag.value) : undefined;
123716
- const safeMode = openingTag.value ? extractBooleanAttr(openingTag.value, 'safeMode') : undefined;
123717
- const mdNode = createHTMLBlockNode(htmlString, node.position, runScripts, safeMode);
123718
- parent.children[index] = mdNode;
123719
- }
123720
- });
123721
- // Ensure html-block nodes have HTML in children as text node
123722
- visit(tree, 'html-block', (node) => {
123723
- const html = node.data?.hProperties?.html;
123724
- if (html &&
123725
- (!node.children ||
123726
- node.children.length === 0 ||
123727
- (node.children.length === 1 && node.children[0].type === 'text' && node.children[0].value !== html))) {
123728
- node.children = [
123729
- {
123730
- type: 'text',
123731
- value: html,
123732
- },
123733
- ];
123373
+ // Preserve raw text from any other phrasing sibling (e.g. stray
123374
+ // whitespace or content the tokenizer didn't claim) so it isn't
123375
+ // silently dropped from the html payload.
123376
+ return 'value' in child && typeof child.value === 'string' ? child.value : '';
123377
+ })
123378
+ .join('');
123379
+ const openingTagIndent = (open.position?.start.column ?? 1) - 1;
123380
+ children.splice(i, closeIdx - i + 1, htmlBlockFromRaw(openMatch[1], body, open.position, openingTagIndent));
123734
123381
  }
123735
123382
  });
123736
- return tree;
123737
123383
  };
123738
123384
  /* harmony default export */ const mdxish_html_blocks = (mdxishHtmlBlocks);
123739
123385
 
@@ -123965,7 +123611,7 @@ const transformAnchor = (jsx) => {
123965
123611
  */
123966
123612
  const transformImage = (jsx) => {
123967
123613
  const attrs = getAttrs(jsx);
123968
- const { align, alt = '', border, caption, className, framed, height, lazy, src = '', title = '', width } = attrs;
123614
+ const { align, alt = '', border, caption, className, framed, height, lazy, src = '', title = '', width, wrap, } = attrs;
123969
123615
  const validAlign = toImageAlign(align);
123970
123616
  const sizing = width !== undefined ? String(width) : undefined;
123971
123617
  const hProperties = {
@@ -123981,6 +123627,7 @@ const transformImage = (jsx) => {
123981
123627
  ...(lazy !== undefined && { lazy: toBool(lazy) }),
123982
123628
  ...(sizing && { sizing }),
123983
123629
  ...(sizing && { width: sizing }),
123630
+ ...(wrap !== undefined && { wrap: toBool(wrap) }),
123984
123631
  };
123985
123632
  return {
123986
123633
  type: NodeTypes.imageBlock,
@@ -123997,6 +123644,7 @@ const transformImage = (jsx) => {
123997
123644
  src,
123998
123645
  title,
123999
123646
  width: sizing,
123647
+ ...(wrap !== undefined && { wrap: toBool(wrap) }),
124000
123648
  data: {
124001
123649
  hName: 'img',
124002
123650
  hProperties,
@@ -124512,6 +124160,189 @@ const normalizeMdxJsxNodes = () => tree => {
124512
124160
  };
124513
124161
  /* harmony default export */ const normalize_mdx_jsx_nodes = (normalizeMdxJsxNodes);
124514
124162
 
124163
+ ;// ./lib/micromark/jsx-comment/pattern.ts
124164
+ /**
124165
+ * Matches a JSX comment: `{/*`, content, `*\/}` — no whitespace tolerated
124166
+ * between the braces and the comment markers.
124167
+ *
124168
+ * This grammar is mirrored by the flow tokenizer in ./syntax.ts. Any change
124169
+ * here needs a mirror change in the state machine; the parity test in
124170
+ * __tests__/lib/micromark/jsx-comment-pattern-parity.test.ts locks the two
124171
+ * together so they can't silently drift.
124172
+ */
124173
+ const JSX_COMMENT_REGEX = /\{\/\*[^*]*(?:\*(?!\/)[^*]*)*\*\/\}/g;
124174
+
124175
+ ;// ./processor/transform/mdxish/preprocess-jsx-expressions.ts
124176
+
124177
+
124178
+ /**
124179
+ * Removes JSX-style comments (e.g., { /* comment *\/ }) from content.
124180
+ *
124181
+ * @param content
124182
+ * @returns Content with JSX comments removed
124183
+ * @example
124184
+ * ```typescript
124185
+ * removeJSXComments('Text { /* comment *\/ } more text')
124186
+ * // Returns: 'Text more text'
124187
+ * ```
124188
+ */
124189
+ function removeJSXComments(content) {
124190
+ return content.replace(JSX_COMMENT_REGEX, '');
124191
+ }
124192
+ const HTML_ELEM_PLACEHOLDER_PREFIX = '___MDXISH_HTML_ELEM_';
124193
+ const HTML_ELEM_PLACEHOLDER = new RegExp(`${HTML_ELEM_PLACEHOLDER_PREFIX}(\\d+)___`, 'g');
124194
+ // Matches an HTML element that starts at a line boundary and ends at a line boundary.
124195
+ // Allows optional leading indentation and lazily matches until the same closing tag.
124196
+ const BLOCK_HTML_RE = /(?<=^|\n)[ \t]*<([a-z][a-zA-Z0-9]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>[ \t]*(?=\n|$)/g;
124197
+ /**
124198
+ * Hides line-anchored HTML elements from the brace-escaping pass so we don't leak `\{`
124199
+ * into rendered output (rehypeRaw renders the `\` literally, e.g. `<div>{foo</div>`).
124200
+ *
124201
+ * One carve-out: if an interior line at column 0 has bare text containing `{`, mdxish
124202
+ * parses that line as a paragraph and the mdxExpression step would throw without an
124203
+ * escape — so we leave that case to the brace balancer.
124204
+ */
124205
+ function protectHTMLElements(content) {
124206
+ const htmlElements = [];
124207
+ const protectedContent = content.replace(BLOCK_HTML_RE, match => {
124208
+ // Look at the lines between the open and close tags. If any of them starts
124209
+ // at column 0 with bare text (not whitespace, not another tag) and contains
124210
+ // `{`, mdxish will parse that line as a paragraph and the brace as an MDX
124211
+ // expression, which would throw an error. So we let the brace balancer escape it.
124212
+ // Otherwise, we need to extract the sequence to protect it from the brace escaping.
124213
+ const interior = match.split('\n').slice(1, -1);
124214
+ const hazard = interior.some(line => line.length > 0 && line[0] !== ' ' && line[0] !== '\t' && line[0] !== '<' && line.includes('{'));
124215
+ if (hazard)
124216
+ return match;
124217
+ htmlElements.push(match);
124218
+ return `${HTML_ELEM_PLACEHOLDER_PREFIX}${htmlElements.length - 1}___`;
124219
+ });
124220
+ return { htmlElements, protectedContent };
124221
+ }
124222
+ function restoreHTMLElements(content, htmlElements) {
124223
+ if (htmlElements.length === 0)
124224
+ return content;
124225
+ return content.replace(HTML_ELEM_PLACEHOLDER, (_m, idx) => htmlElements[parseInt(idx, 10)]);
124226
+ }
124227
+ /**
124228
+ * Escapes unbalanced and paragraph-spanning braces so MDX doesn't trip on them.
124229
+ */
124230
+ function escapeProblematicBraces(content) {
124231
+ const { htmlElements, protectedContent } = protectHTMLElements(content);
124232
+ let strDelim = null;
124233
+ let strEscaped = false;
124234
+ // Track position of last newline (outside strings) to detect blank lines
124235
+ // -2 means no recent newline
124236
+ let lastNewlinePos = -2;
124237
+ // Character state machine trackers
124238
+ const toEscape = new Set();
124239
+ // Convert to array of Unicode code points so that emojis and multi-byte characters are correctly tracked
124240
+ const chars = Array.from(protectedContent);
124241
+ const openStack = [];
124242
+ for (let i = 0; i < chars.length; i += 1) {
124243
+ const ch = chars[i];
124244
+ // Track string delimiters inside expressions to ignore braces within them
124245
+ if (openStack.length > 0) {
124246
+ if (strDelim) {
124247
+ if (strEscaped)
124248
+ strEscaped = false;
124249
+ else if (ch === '\\')
124250
+ strEscaped = true;
124251
+ else if (ch === strDelim)
124252
+ strDelim = null;
124253
+ // eslint-disable-next-line no-continue
124254
+ continue;
124255
+ }
124256
+ if (ch === '"' || ch === "'" || ch === '`') {
124257
+ strDelim = ch;
124258
+ // eslint-disable-next-line no-continue
124259
+ continue;
124260
+ }
124261
+ if (ch === '\n') {
124262
+ if (lastNewlinePos >= 0) {
124263
+ const between = chars.slice(lastNewlinePos + 1, i).join('');
124264
+ if (/^[ \t]*$/.test(between)) {
124265
+ openStack.forEach(entry => { entry.hasBlankLine = true; });
124266
+ }
124267
+ }
124268
+ lastNewlinePos = i;
124269
+ }
124270
+ }
124271
+ // Skip already-escaped braces (odd run of preceding backslashes).
124272
+ if (ch === '{' || ch === '}') {
124273
+ let bs = 0;
124274
+ for (let j = i - 1; j >= 0 && chars[j] === '\\'; j -= 1)
124275
+ bs += 1;
124276
+ if (bs % 2 === 1) {
124277
+ // eslint-disable-next-line no-continue
124278
+ continue;
124279
+ }
124280
+ }
124281
+ if (ch === '{') {
124282
+ // `=` (after whitespace) before `{` ⇒ JSX attribute expression. The
124283
+ // mdxComponent tokenizer captures the whole component, so blank lines
124284
+ // inside attribute values are harmless. Nested `{` inherits the flag.
124285
+ let isAttrExpr = false;
124286
+ for (let j = i - 1; j >= 0; j -= 1) {
124287
+ const pc = chars[j];
124288
+ if (pc === '=') {
124289
+ isAttrExpr = true;
124290
+ break;
124291
+ }
124292
+ if (pc !== ' ' && pc !== '\t')
124293
+ break;
124294
+ }
124295
+ // Nested `{ ... }` inside an attribute value (e.g. `data={[{ ... }]}` or
124296
+ // `data={{ a: { b: 1 } }}`) must inherit the same exemption; only the
124297
+ // outer `{` is directly after `=`.
124298
+ if (!isAttrExpr && openStack.length > 0 && openStack[openStack.length - 1].isAttrExpr) {
124299
+ isAttrExpr = true;
124300
+ }
124301
+ openStack.push({ pos: i, hasBlankLine: false, isAttrExpr });
124302
+ lastNewlinePos = -2;
124303
+ }
124304
+ else if (ch === '}') {
124305
+ if (openStack.length > 0) {
124306
+ const entry = openStack.pop();
124307
+ // Pure `{/* ... */}` comments are handled downstream by the jsxComment
124308
+ // tokenizer — escaping their braces would prevent it from running.
124309
+ const isPureJsxComment = chars[entry.pos + 1] === '/' &&
124310
+ chars[entry.pos + 2] === '*' &&
124311
+ chars[i - 1] === '/' &&
124312
+ chars[i - 2] === '*';
124313
+ if (entry.hasBlankLine && !isPureJsxComment && !entry.isAttrExpr) {
124314
+ toEscape.add(entry.pos);
124315
+ toEscape.add(i);
124316
+ }
124317
+ }
124318
+ else {
124319
+ toEscape.add(i);
124320
+ }
124321
+ }
124322
+ }
124323
+ // Anything still open is unbalanced.
124324
+ openStack.forEach(entry => toEscape.add(entry.pos));
124325
+ // Reconstruct the content with the escaped braces.
124326
+ const escapedContent = toEscape.size === 0 ? protectedContent : chars.map((ch, i) => (toEscape.has(i) ? `\\${ch}` : ch)).join('');
124327
+ return restoreHTMLElements(escapedContent, htmlElements);
124328
+ }
124329
+ /**
124330
+ * Preprocesses JSX-like markdown content before parsing.
124331
+ *
124332
+ * JSX attribute expressions (`href={baseUrl}`) are no longer rewritten here —
124333
+ * they flow through the tokenizer as `mdxJsxAttributeValueExpression` nodes
124334
+ * and are evaluated at the hast handler step.
124335
+ *
124336
+ * @param content
124337
+ * @returns Preprocessed content ready for markdown parsing
124338
+ */
124339
+ function preprocessJSXExpressions(content) {
124340
+ const { protectedCode, protectedContent } = protectCodeBlocks(content);
124341
+ let processed = escapeProblematicBraces(protectedContent);
124342
+ processed = restoreCodeBlocks(processed, protectedCode);
124343
+ return processed;
124344
+ }
124345
+
124515
124346
  ;// ./processor/transform/mdxish/restore-snake-case-component-name.ts
124516
124347
 
124517
124348
 
@@ -125628,7 +125459,7 @@ function mdxishAstProcessor(mdContent, opts = {}) {
125628
125459
  .use(inline_html, { safeMode })
125629
125460
  .use(restore_snake_case_component_name, { mapping: snakeCaseMapping })
125630
125461
  .use(mdxish_tables)
125631
- .use(mdxish_html_blocks)
125462
+ .use(mdxish_html_blocks) // Convert every <HTMLBlock> shape → html-block
125632
125463
  // The next few transformers must appear after mdxishMdxComponentBlocks
125633
125464
  // so nodes produced by the inline re-parse of component bodies
125634
125465
  // (e.g. code/image/embed inside <Tabs>) get visited too