@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.js CHANGED
@@ -12209,10 +12209,12 @@ const LightboxPortal = ({ children }) => {
12209
12209
  return (0,external_amd_react_dom_commonjs2_react_dom_commonjs_react_dom_root_ReactDOM_umd_react_dom_.createPortal)(children, document.body);
12210
12210
  };
12211
12211
  const Image = (Props) => {
12212
- const { align = '', alt = '', border: borderProp = false, caption, className = '', framed: framedProp = false, height = 'auto', src, title = '', width = 'auto', lazy = true, children, } = Props;
12212
+ const { align = '', alt = '', border: borderProp = false, caption, className = '', framed: framedProp = false, height = 'auto', src, title = '', width = 'auto', lazy = true, children, wrap: wrapProp, } = Props;
12213
12213
  // Normalize border/framed: MDXish passes {false} as the string "false", not a boolean
12214
12214
  const border = borderProp === true || borderProp === 'true';
12215
12215
  const framed = framedProp === true || framedProp === 'true';
12216
+ // Default (undefined) keeps legacy behavior: left/right images float and wrap text.
12217
+ const noWrap = (align === 'left' || align === 'right') && (wrapProp === false || wrapProp === 'false');
12216
12218
  const [lightbox, setLightbox] = external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_.useState(false);
12217
12219
  if (className === 'emoji') {
12218
12220
  return external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_.createElement("img", { alt: alt, height: height, loading: lazy ? 'lazy' : 'eager', src: src, title: title, width: width });
@@ -12242,7 +12244,8 @@ const Image = (Props) => {
12242
12244
  setLightbox(!lightbox);
12243
12245
  };
12244
12246
  // Framed images center the <img> itself; outer wrapper handles left/right alignment via text-align.
12245
- const imgElement = (external_amd_react_commonjs_react_commonjs2_react_root_React_umd_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 }));
12247
+ const imgClass = `img ${caption || children || framed ? 'img-align-center' : align ? `img-align-${align}` : ''} ${border ? 'border' : ''}${noWrap ? ' img-no-wrap' : ''}`;
12248
+ const imgElement = (external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_.createElement("img", { alt: alt, className: imgClass, height: height, loading: lazy ? 'lazy' : 'eager', src: src, title: title, width: width }));
12246
12249
  const closedLightbox = (ariaLabel, content) => (external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_.createElement("span", { "aria-label": ariaLabel, className: "img lightbox closed", onClick: toggle, onKeyDown: handleKeyDown, role: 'button', tabIndex: 0 },
12247
12250
  external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_.createElement("span", { className: "lightbox-inner" }, content)));
12248
12251
  const lightboxOverlay = lightbox ? (external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_.createElement(LightboxPortal, null,
@@ -12254,10 +12257,10 @@ const Image = (Props) => {
12254
12257
  external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_.createElement("button", { "aria-label": "Minimize image", className: "lightbox-close", onClick: toggle, type: "button" },
12255
12258
  external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_.createElement("i", { "aria-hidden": "true", className: "fa-solid fa-xmark" }))))) : null;
12256
12259
  if (framed) {
12257
- const frameClass = `img-frame img-frame-${align || 'center'}`;
12258
- // Left/right frames shrink to fit, so percentage widths can't resolve
12259
- // against the parent, hoist onto the wrapper. Center frames are full-width.
12260
- const isClamped = align === 'left' || align === 'right';
12260
+ const frameClass = `img-frame img-frame-${align || 'center'}${noWrap ? ' img-no-wrap' : ''}`;
12261
+ // Only left/right wrapping frames shrink to fit; for those, hoist percentage
12262
+ // widths onto the wrapper since they can't resolve against a shrink-to-fit parent.
12263
+ const isClamped = (align === 'left' || align === 'right') && !noWrap;
12261
12264
  const frameStyle = isClamped && typeof width === 'string' && width.endsWith('%') ? { width } : undefined;
12262
12265
  if (children || caption) {
12263
12266
  return (external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_.createElement("figure", { className: frameClass, style: frameStyle },
@@ -12270,11 +12273,20 @@ const Image = (Props) => {
12270
12273
  lightboxOverlay));
12271
12274
  }
12272
12275
  if (children || caption) {
12273
- return (external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_.createElement("figure", null,
12274
- closedLightbox(alt || 'Expand image', external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_.createElement(external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_.Fragment, null,
12275
- imgElement,
12276
- external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_.createElement("figcaption", null, children || caption))),
12277
- lightboxOverlay));
12276
+ // Mirrors the framed pattern: left/right captioned figures float and shrink
12277
+ // to fit so a long caption doesn't widen the float past the image.
12278
+ const isFloating = (align === 'left' || align === 'right') && !noWrap;
12279
+ const figureClass = [
12280
+ (align === 'left' || align === 'right') && `img-figure-${align}`,
12281
+ noWrap && 'img-no-wrap',
12282
+ ]
12283
+ .filter(Boolean)
12284
+ .join(' ');
12285
+ const figureStyle = isFloating && typeof width === 'string' && width.endsWith('%') ? { width } : undefined;
12286
+ return (external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_.createElement("figure", { className: figureClass || undefined, style: figureStyle },
12287
+ closedLightbox(alt || 'Expand image', imgElement),
12288
+ lightboxOverlay,
12289
+ external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_.createElement("figcaption", null, children || caption)));
12278
12290
  }
12279
12291
  return (external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_.createElement(external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_.Fragment, null,
12280
12292
  closedLightbox('Expand image', imgElement),
@@ -13106,6 +13118,7 @@ const INLINE_COMPONENT_TAGS = new Set(['Anchor', 'Glossary']);
13106
13118
  /**
13107
13119
  * PascalCase tags that have their own dedicated tokenizer / transformer
13108
13120
  * and must not be claimed by the generic `mdxComponent` construct.
13121
+ * Subject to change as we add more dedicated tokenizers.
13109
13122
  */
13110
13123
  const DEDICATED_COMPONENT_TAGS = ['HTMLBlock', 'Table'];
13111
13124
  /**
@@ -13116,6 +13129,14 @@ const GENERIC_MDX_COMPONENT_EXCLUDED_TAGS = new Set([
13116
13129
  ...DEDICATED_COMPONENT_TAGS,
13117
13130
  ...INLINE_COMPONENT_TAGS,
13118
13131
  ]);
13132
+ /**
13133
+ * Tags the micromark `mdxComponent` tokenizer must not claim, which
13134
+ * are inline components and those that have their own dedicated tokenizer
13135
+ */
13136
+ const TOKENIZER_MDX_COMPONENT_EXCLUDED_TAGS = new Set([
13137
+ 'Table',
13138
+ ...INLINE_COMPONENT_TAGS,
13139
+ ]);
13119
13140
  /**
13120
13141
  * Lowercased variant of {@link INLINE_COMPONENT_TAGS} for consumers that
13121
13142
  * run after rehype (where hast `tagName` is normalized to lowercase).
@@ -54510,18 +54531,28 @@ const isMDXEsm = (node) => {
54510
54531
  * Takes an HTML string and formats it for display in the editor. Removes leading/trailing newlines
54511
54532
  * and unindents the HTML.
54512
54533
  *
54513
- * @param {string} html - HTML content from template literal
54534
+ * @param {string} html - cooked HTML payload (callers strip any template-literal backticks first)
54535
+ * @param {number} [openingTagIndent=0] - column the `<HTMLBlock>` opening tag sits at, used to
54536
+ * dedent each content line so its indentation reads relative to the tag, not the line start
54514
54537
  * @returns {string} processed HTML
54515
54538
  */
54516
- function formatHtmlForMdxish(html) {
54517
- // Remove leading/trailing backticks if present, since they're used to keep the HTML
54518
- // from being parsed prematurely
54519
- let processed = html;
54520
- if (processed.startsWith('`') && processed.endsWith('`')) {
54521
- processed = processed.slice(1, -1);
54522
- }
54539
+ function formatHtmlForMdxish(html, openingTagIndent = 0) {
54523
54540
  // Removes the leading/trailing newlines
54524
- let cleaned = processed.replace(/^\s*\n|\n\s*$/g, '');
54541
+ let cleaned = html.replace(/^\s*\n|\n\s*$/g, '');
54542
+ // Strip / deindent the lines in the HTML string so that the indents are relative
54543
+ // to the opening HTMLBlock tag, not the literal line start
54544
+ // Keep any deeper indent
54545
+ if (openingTagIndent > 0) {
54546
+ cleaned = cleaned
54547
+ .split('\n')
54548
+ .map(line => {
54549
+ let i = 0;
54550
+ while (i < openingTagIndent && (line[i] === ' ' || line[i] === '\t'))
54551
+ i += 1;
54552
+ return line.slice(i);
54553
+ })
54554
+ .join('\n');
54555
+ }
54525
54556
  // Convert literal \n sequences to actual newlines only inside <pre> and <code>.
54526
54557
  // Because <pre> needs to respect the newline visual and
54527
54558
  // escape characters should be processed in the <code> tag.
@@ -75678,11 +75709,15 @@ const symmetrizePair = (html, { openStart, openEnd, closeStart, closeEnd }) => {
75678
75709
  const postCloser = html.slice(closeEnd, closeLine.end);
75679
75710
  const openerHasExtras = preOpener.trim().length > 0 || postOpener.trim().length > 0;
75680
75711
  const closerHasExtras = preCloser.trim().length > 0 || postCloser.trim().length > 0;
75681
- // Both match (both bare or both attached) — mdxjs parses this fine.
75682
- if (openerHasExtras === closerHasExtras)
75712
+ // A blank line splits opener/closer into separate paragraphs.
75713
+ // If either tag is attached to surrounding text, mdxjs can fail to match.
75714
+ const spansBlankLine = /\n[^\S\n]*\n/.test(html.slice(openEnd, closeStart));
75715
+ // If both sides are already symmetric, keep as-is.
75716
+ // Exception: attached + blank-line-split pairs still need normalization.
75717
+ if (openerHasExtras === closerHasExtras && !(openerHasExtras && spansBlankLine))
75683
75718
  return [];
75684
- // Asymmetric. Push non-tag content on the offending side to its own line,
75685
- // visually aligning the inserted line with the existing whitespace prefix.
75719
+ // For asymmetric (or attached-but-split) pairs, move side content to its own
75720
+ // line so opener/closer become line-symmetric.
75686
75721
  const indentFor = (linePrefix) => linePrefix.match(/^\s*/)?.[0] ?? '';
75687
75722
  const inserts = [];
75688
75723
  if (openerHasExtras) {
@@ -76128,6 +76163,7 @@ const repairUnclosedTags = (html) => {
76128
76163
 
76129
76164
 
76130
76165
 
76166
+
76131
76167
 
76132
76168
 
76133
76169
  const isTableCell = (node) => isMDXElement(node) && ['th', 'td'].includes(node.name);
@@ -76228,6 +76264,15 @@ const processTableNode = (node, index, parent, documentPosition) => {
76228
76264
  const { align: alignAttr } = getAttrs(node);
76229
76265
  const align = Array.isArray(alignAttr) ? alignAttr : null;
76230
76266
  let tableHasFlowContent = false;
76267
+ // An `<HTMLBlock>` (still a JSX element here; converted to `html-block` by
76268
+ // `mdxishHtmlBlocks` after this transformer) is block-level content that a
76269
+ // markdown table cell can't represent, so keep the table as a JSX `<Table>`.
76270
+ visit(node, candidate => candidate.type === NodeTypes.htmlBlock ||
76271
+ ((candidate.type === 'mdxJsxFlowElement' || candidate.type === 'mdxJsxTextElement') &&
76272
+ candidate.name === 'HTMLBlock'), () => {
76273
+ tableHasFlowContent = true;
76274
+ return EXIT;
76275
+ });
76231
76276
  // Re-parse text-only cells through markdown and detect flow content
76232
76277
  visit(node, isTableCell, (cell) => {
76233
76278
  if (!isTextOnly(cell.children))
@@ -100868,7 +100913,7 @@ function createTokenize(mode) {
100868
100913
  return tagNameRest;
100869
100914
  }
100870
100915
  // Tag name complete — check exclusions
100871
- if (GENERIC_MDX_COMPONENT_EXCLUDED_TAGS.has(tagName)) {
100916
+ if (TOKENIZER_MDX_COMPONENT_EXCLUDED_TAGS.has(tagName)) {
100872
100917
  return nok(code);
100873
100918
  }
100874
100919
  depth = 1;
@@ -102959,557 +103004,158 @@ const magicBlockTransformer = (options = {}) => tree => {
102959
103004
  };
102960
103005
  /* harmony default export */ const magic_block_transformer = (magicBlockTransformer);
102961
103006
 
102962
- ;// ./lib/micromark/jsx-comment/pattern.ts
102963
- /**
102964
- * Matches a JSX comment: `{/*`, content, `*\/}` — no whitespace tolerated
102965
- * between the braces and the comment markers.
102966
- *
102967
- * This grammar is mirrored by the flow tokenizer in ./syntax.ts. Any change
102968
- * here needs a mirror change in the state machine; the parity test in
102969
- * __tests__/lib/micromark/jsx-comment-pattern-parity.test.ts locks the two
102970
- * together so they can't silently drift.
102971
- */
102972
- const JSX_COMMENT_REGEX = /\{\/\*[^*]*(?:\*(?!\/)[^*]*)*\*\/\}/g;
103007
+ ;// ./processor/transform/mdxish/mdxish-html-blocks.ts
102973
103008
 
102974
- ;// ./processor/transform/mdxish/preprocess-jsx-expressions.ts
102975
103009
 
102976
103010
 
102977
- // Base64 encode (Node.js + browser compatible)
102978
- function base64Encode(str) {
102979
- if (typeof Buffer !== 'undefined') {
102980
- return Buffer.from(str, 'utf-8').toString('base64');
102981
- }
102982
- return btoa(unescape(encodeURIComponent(str)));
102983
- }
102984
- // Base64 decode (Node.js + browser compatible)
102985
- function base64Decode(str) {
102986
- if (typeof Buffer !== 'undefined') {
102987
- return Buffer.from(str, 'base64').toString('utf-8');
102988
- }
102989
- return decodeURIComponent(escape(atob(str)));
102990
- }
102991
- // Markers for protected HTMLBlock content (HTML comments avoid markdown parsing issues)
102992
- const HTML_BLOCK_CONTENT_START = '<!--RDMX_HTMLBLOCK:';
102993
- const HTML_BLOCK_CONTENT_END = ':RDMX_HTMLBLOCK-->';
103011
+ // `<HTMLBlock …>{`…`}</HTMLBlock>` embedded inside a raw HTML block (e.g. a
103012
+ // single-line `<div>…</div>`). CommonMark slurps the whole div as one `html`
103013
+ // node, so the tokenizer never sees the HTMLBlock — we recover it here.
103014
+ const RAW_HTML_BLOCK_RE = /<HTMLBlock\b([^>]*)>\s*\{\s*`((?:[^`\\]|\\.)*)`\s*\}\s*<\/HTMLBlock>/g;
103015
+ // Opening `<HTMLBlock …>` as its own `html` node — produced inside a paragraph
103016
+ // when an HTMLBlock appears inline alongside text.
103017
+ const HTML_BLOCK_OPEN_RE = /^<HTMLBlock\b([^>]*)>$/;
102994
103018
  /**
102995
- * Base64 encodes HTMLBlock template literal content to prevent markdown parser from consuming <script>/<style> tags.
102996
- *
102997
- * @param content
102998
- * @returns Content with HTMLBlock template literals base64 encoded in HTML comments
102999
- * @example
103000
- * ```typescript
103001
- * const input = '<HTMLBlock>{`<script>alert("xss")</script>`}</HTMLBlock>';
103002
- * protectHTMLBlockContent(input)
103003
- * // Returns: '<HTMLBlock><!--RDMX_HTMLBLOCK:PHNjcmlwdD5hbGVydCgieHNzIik8L3NjcmlwdD4=:RDMX_HTMLBLOCK--></HTMLBlock>'
103004
- * ```
103019
+ * Builds the canonical `html-block` MDAST node the renderer expects.
103005
103020
  */
103006
- function protectHTMLBlockContent(content) {
103007
- return content.replace(/(<HTMLBlock[^>]*>)\{\s*`((?:[^`\\]|\\.)*)`\s*\}(<\/HTMLBlock>)/g, (_match, openTag, templateContent, closeTag) => {
103008
- const encoded = base64Encode(templateContent);
103009
- return `${openTag}${HTML_BLOCK_CONTENT_START}${encoded}${HTML_BLOCK_CONTENT_END}${closeTag}`;
103010
- });
103011
- }
103021
+ const createHtmlBlockNode = (html, position, runScripts, safeMode) => ({
103022
+ position,
103023
+ children: [{ type: 'text', value: html }],
103024
+ type: NodeTypes.htmlBlock,
103025
+ data: {
103026
+ hName: 'html-block',
103027
+ hProperties: {
103028
+ html,
103029
+ ...(runScripts !== undefined && { runScripts }),
103030
+ ...(safeMode !== undefined && { safeMode }),
103031
+ },
103032
+ },
103033
+ });
103012
103034
  /**
103013
- * Removes JSX-style comments (e.g., { /* comment *\/ }) from content.
103014
- *
103015
- * @param content
103016
- * @returns Content with JSX comments removed
103017
- * @example
103018
- * ```typescript
103019
- * removeJSXComments('Text { /* comment *\/ } more text')
103020
- * // Returns: 'Text more text'
103021
- * ```
103035
+ * Reads the cooked string out of a brace expression wrapping a single template
103036
+ * literal (`` `<p>n</p>` `` → `<p>n</p>`).
103022
103037
  */
103023
- function removeJSXComments(content) {
103024
- return content.replace(JSX_COMMENT_REGEX, '');
103025
- }
103026
- const HTML_ELEM_PLACEHOLDER_PREFIX = '___MDXISH_HTML_ELEM_';
103027
- const HTML_ELEM_PLACEHOLDER = new RegExp(`${HTML_ELEM_PLACEHOLDER_PREFIX}(\\d+)___`, 'g');
103028
- // Matches an HTML element that starts at a line boundary and ends at a line boundary.
103029
- // Allows optional leading indentation and lazily matches until the same closing tag.
103030
- const BLOCK_HTML_RE = /(?<=^|\n)[ \t]*<([a-z][a-zA-Z0-9]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>[ \t]*(?=\n|$)/g;
103038
+ const extractTemplateLiteral = (value) => {
103039
+ if (!value)
103040
+ return '';
103041
+ const match = value.trim().match(/^`([\s\S]*)`$/);
103042
+ // Non-template-literal bodies (e.g. `{someVar}`) are malformed mdxish input;
103043
+ // returning '' beats shipping JS identifier source as an HTML payload.
103044
+ return match ? match[1] : '';
103045
+ };
103046
+ const toRunScripts = (raw) => raw === 'true' ? true : raw === 'false' ? false : raw;
103047
+ /** Reads an attribute from a raw `<HTMLBlock …>` attribute string. */
103048
+ const rawAttr = (attrs, name) => {
103049
+ const quoted = attrs.match(new RegExp(`\\b${name}\\s*=\\s*"([^"]*)"`));
103050
+ if (quoted)
103051
+ return quoted[1];
103052
+ const expr = attrs.match(new RegExp(`\\b${name}\\s*=\\s*\\{(true|false)\\}`));
103053
+ if (expr)
103054
+ return expr[1];
103055
+ return new RegExp(`\\b${name}\\b`).test(attrs) ? 'true' : undefined;
103056
+ };
103057
+ /** Reads an attribute from a parsed `<HTMLBlock>` JSX element. */
103058
+ const jsxAttr = (element, name) => {
103059
+ const attr = element.attributes.find(a => a.type === 'mdxJsxAttribute' && a.name === name);
103060
+ if (!attr || attr.type !== 'mdxJsxAttribute')
103061
+ return undefined;
103062
+ if (typeof attr.value === 'string')
103063
+ return attr.value;
103064
+ if (attr.value && typeof attr.value === 'object' && 'value' in attr.value)
103065
+ return attr.value.value;
103066
+ return 'true'; // bare boolean attribute, e.g. <HTMLBlock runScripts />
103067
+ };
103068
+ /** Builds an `html-block` from a raw attribute string and (unparsed) body. */
103069
+ const htmlBlockFromRaw = (attrs, html, position, openingTagIndent = 0) => createHtmlBlockNode(formatHtmlForMdxish(html, openingTagIndent), position, toRunScripts(rawAttr(attrs, 'runScripts')), rawAttr(attrs, 'safeMode'));
103031
103070
  /**
103032
- * Hides line-anchored HTML elements from the brace-escaping pass so we don't leak `\{`
103033
- * into rendered output (rehypeRaw renders the `\` literally, e.g. `<div>{foo</div>`).
103071
+ * Splits a raw `html` node that embeds one or more `<HTMLBlock>`s into
103072
+ * `[html before, html-block, html after, …]`. Returns null when there is none.
103034
103073
  *
103035
- * One carve-out: if an interior line at column 0 has bare text containing `{`, mdxish
103036
- * parses that line as a paragraph and the mdxExpression step would throw without an
103037
- * escape — so we leave that case to the brace balancer.
103074
+ * `String.split` on a regex with capture groups interleaves the captures into
103075
+ * the result, so segments arrive as `[text, attrs, body, text, attrs, body, …]`.
103038
103076
  */
103039
- function protectHTMLElements(content) {
103040
- const htmlElements = [];
103041
- const protectedContent = content.replace(BLOCK_HTML_RE, match => {
103042
- // Look at the lines between the open and close tags. If any of them starts
103043
- // at column 0 with bare text (not whitespace, not another tag) and contains
103044
- // `{`, mdxish will parse that line as a paragraph and the brace as an MDX
103045
- // expression, which would throw an error. So we let the brace balancer escape it.
103046
- // Otherwise, we need to extract the sequence to protect it from the brace escaping.
103047
- const interior = match.split('\n').slice(1, -1);
103048
- const hazard = interior.some(line => line.length > 0 && line[0] !== ' ' && line[0] !== '\t' && line[0] !== '<' && line.includes('{'));
103049
- if (hazard)
103050
- return match;
103051
- htmlElements.push(match);
103052
- return `${HTML_ELEM_PLACEHOLDER_PREFIX}${htmlElements.length - 1}___`;
103053
- });
103054
- return { htmlElements, protectedContent };
103055
- }
103056
- function restoreHTMLElements(content, htmlElements) {
103057
- if (htmlElements.length === 0)
103058
- return content;
103059
- return content.replace(HTML_ELEM_PLACEHOLDER, (_m, idx) => htmlElements[parseInt(idx, 10)]);
103060
- }
103061
- /**
103062
- * Escapes unbalanced and paragraph-spanning braces so MDX doesn't trip on them.
103063
- */
103064
- function escapeProblematicBraces(content) {
103065
- const { htmlElements, protectedContent } = protectHTMLElements(content);
103066
- let strDelim = null;
103067
- let strEscaped = false;
103068
- // Track position of last newline (outside strings) to detect blank lines
103069
- // -2 means no recent newline
103070
- let lastNewlinePos = -2;
103071
- // Character state machine trackers
103072
- const toEscape = new Set();
103073
- // Convert to array of Unicode code points so that emojis and multi-byte characters are correctly tracked
103074
- const chars = Array.from(protectedContent);
103075
- const openStack = [];
103076
- for (let i = 0; i < chars.length; i += 1) {
103077
- const ch = chars[i];
103078
- // Track string delimiters inside expressions to ignore braces within them
103079
- if (openStack.length > 0) {
103080
- if (strDelim) {
103081
- if (strEscaped)
103082
- strEscaped = false;
103083
- else if (ch === '\\')
103084
- strEscaped = true;
103085
- else if (ch === strDelim)
103086
- strDelim = null;
103087
- // eslint-disable-next-line no-continue
103088
- continue;
103089
- }
103090
- if (ch === '"' || ch === "'" || ch === '`') {
103091
- strDelim = ch;
103092
- // eslint-disable-next-line no-continue
103093
- continue;
103094
- }
103095
- if (ch === '\n') {
103096
- if (lastNewlinePos >= 0) {
103097
- const between = chars.slice(lastNewlinePos + 1, i).join('');
103098
- if (/^[ \t]*$/.test(between)) {
103099
- openStack.forEach(entry => { entry.hasBlankLine = true; });
103100
- }
103101
- }
103102
- lastNewlinePos = i;
103103
- }
103104
- }
103105
- // Skip already-escaped braces (odd run of preceding backslashes).
103106
- if (ch === '{' || ch === '}') {
103107
- let bs = 0;
103108
- for (let j = i - 1; j >= 0 && chars[j] === '\\'; j -= 1)
103109
- bs += 1;
103110
- if (bs % 2 === 1) {
103111
- // eslint-disable-next-line no-continue
103112
- continue;
103113
- }
103114
- }
103115
- if (ch === '{') {
103116
- // `=` (after whitespace) before `{` ⇒ JSX attribute expression. The
103117
- // mdxComponent tokenizer captures the whole component, so blank lines
103118
- // inside attribute values are harmless. Nested `{` inherits the flag.
103119
- let isAttrExpr = false;
103120
- for (let j = i - 1; j >= 0; j -= 1) {
103121
- const pc = chars[j];
103122
- if (pc === '=') {
103123
- isAttrExpr = true;
103124
- break;
103125
- }
103126
- if (pc !== ' ' && pc !== '\t')
103127
- break;
103128
- }
103129
- // Nested `{ ... }` inside an attribute value (e.g. `data={[{ ... }]}` or
103130
- // `data={{ a: { b: 1 } }}`) must inherit the same exemption; only the
103131
- // outer `{` is directly after `=`.
103132
- if (!isAttrExpr && openStack.length > 0 && openStack[openStack.length - 1].isAttrExpr) {
103133
- isAttrExpr = true;
103134
- }
103135
- openStack.push({ pos: i, hasBlankLine: false, isAttrExpr });
103136
- lastNewlinePos = -2;
103137
- }
103138
- else if (ch === '}') {
103139
- if (openStack.length > 0) {
103140
- const entry = openStack.pop();
103141
- // Pure `{/* ... */}` comments are handled downstream by the jsxComment
103142
- // tokenizer — escaping their braces would prevent it from running.
103143
- const isPureJsxComment = chars[entry.pos + 1] === '/' &&
103144
- chars[entry.pos + 2] === '*' &&
103145
- chars[i - 1] === '/' &&
103146
- chars[i - 2] === '*';
103147
- if (entry.hasBlankLine && !isPureJsxComment && !entry.isAttrExpr) {
103148
- toEscape.add(entry.pos);
103149
- toEscape.add(i);
103150
- }
103151
- }
103152
- else {
103153
- toEscape.add(i);
103154
- }
103155
- }
103156
- }
103157
- // Anything still open is unbalanced.
103158
- openStack.forEach(entry => toEscape.add(entry.pos));
103159
- // Reconstruct the content with the escaped braces.
103160
- const escapedContent = toEscape.size === 0 ? protectedContent : chars.map((ch, i) => (toEscape.has(i) ? `\\${ch}` : ch)).join('');
103161
- return restoreHTMLElements(escapedContent, htmlElements);
103162
- }
103077
+ const splitRawHtmlBlocks = (node) => {
103078
+ const segments = node.value.split(RAW_HTML_BLOCK_RE);
103079
+ if (segments.length === 1)
103080
+ return null; // no <HTMLBlock> present
103081
+ const parts = [];
103082
+ for (let i = 0; i < segments.length; i += 3) {
103083
+ const [text, attrs, body] = segments.slice(i, i + 3);
103084
+ if (text)
103085
+ parts.push({ type: 'html', value: text });
103086
+ if (body !== undefined) {
103087
+ // The opening tag's column equals the length of the line it starts on
103088
+ // (the text run since the previous newline preceding the match).
103089
+ const openingTagIndent = text.slice(text.lastIndexOf('\n') + 1).length;
103090
+ parts.push(htmlBlockFromRaw(attrs, body, node.position, openingTagIndent));
103091
+ }
103092
+ }
103093
+ return parts;
103094
+ };
103163
103095
  /**
103164
- * Preprocesses JSX-like markdown content before parsing.
103096
+ * Converts every `<HTMLBlock>` shape that survives parsing into the canonical
103097
+ * `html-block` MDAST node, reading the body from the tokenizer's template-literal
103098
+ * expression. Three shapes occur:
103165
103099
  *
103166
- * JSX attribute expressions (`href={baseUrl}`) are no longer rewritten here —
103167
- * they flow through the tokenizer as `mdxJsxAttributeValueExpression` nodes
103168
- * and are evaluated at the hast handler step.
103100
+ * 1. JSX element (`mdxJsxFlowElement`/`mdxJsxTextElement`) multiline/block
103101
+ * context and table cells (after their remarkMdx re-parse).
103102
+ * 2. Raw `html` blob (`splitRawHtmlBlocks`) single-line top-level, or nested
103103
+ * in raw HTML like an inline `<div>`.
103104
+ * 3. Inline-in-paragraph — split into `html` + expression + `html` siblings.
103169
103105
  *
103170
- * @param content
103171
- * @returns Preprocessed content ready for markdown parsing
103172
- */
103173
- function preprocessJSXExpressions(content) {
103174
- let processed = protectHTMLBlockContent(content);
103175
- const { protectedCode, protectedContent } = protectCodeBlocks(processed);
103176
- processed = escapeProblematicBraces(protectedContent);
103177
- processed = restoreCodeBlocks(processed, protectedCode);
103178
- return processed;
103179
- }
103180
-
103181
- ;// ./processor/transform/mdxish/mdxish-html-blocks.ts
103182
-
103183
-
103184
-
103185
-
103186
- /**
103187
- * Decodes HTMLBlock content that was protected during preprocessing.
103188
- * Content is wrapped in <!--RDMX_HTMLBLOCK:base64:RDMX_HTMLBLOCK-->
103189
- */
103190
- function decodeProtectedContent(content) {
103191
- // Escape special regex characters in the markers
103192
- const startEscaped = HTML_BLOCK_CONTENT_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
103193
- const endEscaped = HTML_BLOCK_CONTENT_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
103194
- const markerRegex = new RegExp(`${startEscaped}([A-Za-z0-9+/=]+)${endEscaped}`, 'g');
103195
- return content.replace(markerRegex, (_match, encoded) => {
103196
- try {
103197
- return base64Decode(encoded);
103198
- }
103199
- catch {
103200
- return encoded;
103201
- }
103202
- });
103203
- }
103204
- /**
103205
- * Collects text content from a node and its children recursively
103206
- */
103207
- function collectTextContent(node) {
103208
- const parts = [];
103209
- if (node.type === 'text' && node.value) {
103210
- parts.push(node.value);
103211
- }
103212
- else if (node.type === 'html' && node.value) {
103213
- parts.push(node.value);
103214
- }
103215
- else if (node.type === 'inlineCode' && node.value) {
103216
- parts.push(node.value);
103217
- }
103218
- else if (node.type === 'code' && node.value) {
103219
- // Reconstruct code fence syntax (markdown parser consumes opening ```)
103220
- const lang = node.lang || '';
103221
- const fence = `\`\`\`${lang ? `${lang}\n` : ''}`;
103222
- parts.push(fence);
103223
- parts.push(node.value);
103224
- // Add newline before closing fence if missing
103225
- const closingFence = node.value.endsWith('\n') ? '```' : '\n```';
103226
- parts.push(closingFence);
103227
- }
103228
- else if (node.children && Array.isArray(node.children)) {
103229
- node.children.forEach(child => {
103230
- if (typeof child === 'object' && child !== null) {
103231
- parts.push(collectTextContent(child));
103232
- }
103233
- });
103234
- }
103235
- return parts.join('');
103236
- }
103237
- /**
103238
- * Extracts boolean attribute from HTML tag. Handles JSX (safeMode={true}) and string (safeMode="true") syntax.
103239
- * Returns "true"/"false" string to survive rehypeRaw serialization.
103240
- */
103241
- function extractBooleanAttr(attrs, name) {
103242
- // Try JSX syntax: name={true|false}
103243
- const jsxMatch = attrs.match(new RegExp(`${name}=\\{(true|false)\\}`));
103244
- if (jsxMatch) {
103245
- return jsxMatch[1];
103246
- }
103247
- // Try string syntax: name="true"|true
103248
- const stringMatch = attrs.match(new RegExp(`${name}="?(true|false)"?`));
103249
- if (stringMatch) {
103250
- return stringMatch[1];
103251
- }
103252
- return undefined;
103253
- }
103254
- /**
103255
- * Extracts runScripts attribute from HTML tag. Returns boolean for "true"/"false", string for other values, or undefined if not found.
103256
- */
103257
- function extractRunScriptsAttr(attrs) {
103258
- const runScriptsMatch = attrs.match(/runScripts="?([^">\s]+)"?/);
103259
- if (!runScriptsMatch) {
103260
- return undefined;
103261
- }
103262
- const value = runScriptsMatch[1];
103263
- if (value === 'true') {
103264
- return true;
103265
- }
103266
- if (value === 'false') {
103267
- return false;
103268
- }
103269
- return value;
103270
- }
103271
- /**
103272
- * Creates an HTMLBlock node from HTML string and optional attributes
103273
- */
103274
- function createHTMLBlockNode(htmlString, position, runScripts, safeMode) {
103275
- return {
103276
- position,
103277
- children: [{ type: 'text', value: htmlString }],
103278
- type: NodeTypes.htmlBlock,
103279
- data: {
103280
- hName: 'html-block',
103281
- hProperties: {
103282
- html: htmlString,
103283
- ...(runScripts !== undefined && { runScripts }),
103284
- ...(safeMode !== undefined && { safeMode }),
103285
- },
103286
- },
103287
- };
103288
- }
103289
- /**
103290
- * Checks for opening tag only (for split detection)
103291
- */
103292
- function hasOpeningTagOnly(node) {
103293
- let hasOpening = false;
103294
- let hasClosed = false;
103295
- let attrs = '';
103296
- const check = (n) => {
103297
- if (n.type === 'html' && n.value) {
103298
- if (n.value === '<HTMLBlock>') {
103299
- hasOpening = true;
103300
- }
103301
- else {
103302
- const match = n.value.match(/^<HTMLBlock(\s[^>]*)?>$/);
103303
- if (match) {
103304
- hasOpening = true;
103305
- attrs = match[1] || '';
103306
- }
103307
- }
103308
- if (n.value === '</HTMLBlock>' || n.value.includes('</HTMLBlock>')) {
103309
- hasClosed = true;
103310
- }
103311
- }
103312
- if (n.children && Array.isArray(n.children)) {
103313
- n.children.forEach(child => {
103314
- check(child);
103315
- });
103316
- }
103317
- };
103318
- check(node);
103319
- // Return true only if opening without closing (split case)
103320
- return { attrs, found: hasOpening && !hasClosed };
103321
- }
103322
- /**
103323
- * Checks if a node contains an HTMLBlock closing tag
103324
- */
103325
- function hasClosingTag(node) {
103326
- if (node.type === 'html' && node.value) {
103327
- if (node.value === '</HTMLBlock>' || node.value.includes('</HTMLBlock>'))
103328
- return true;
103329
- }
103330
- if (node.children && Array.isArray(node.children)) {
103331
- return node.children.some(child => hasClosingTag(child));
103332
- }
103333
- return false;
103334
- }
103335
- /**
103336
- * Transforms HTMLBlock MDX JSX to html-block nodes. Handles <HTMLBlock>{`...`}</HTMLBlock> syntax.
103106
+ * Runs *after* `mdxishTables` so table cells are re-parsed first;
103107
+ * `mdxishTables` recognizes the still-JSX `<HTMLBlock>` element when deciding to
103108
+ * keep a table as a JSX `<Table>`. This replaces the old base64-comment marker
103109
+ * machinery — the #1455 tokenizer hands the body over already parsed.
103337
103110
  */
103338
103111
  const mdxishHtmlBlocks = () => tree => {
103339
- // Handle HTMLBlock split across root children (caused by newlines)
103340
- visit(tree, 'root', (root) => {
103341
- const children = root.children;
103342
- let i = 0;
103343
- while (i < children.length) {
103344
- const child = children[i];
103345
- const { attrs, found: hasOpening } = hasOpeningTagOnly(child);
103346
- if (hasOpening) {
103347
- // Find closing tag in subsequent siblings
103348
- let closingIdx = -1;
103349
- for (let j = i + 1; j < children.length; j += 1) {
103350
- if (hasClosingTag(children[j])) {
103351
- closingIdx = j;
103352
- break;
103353
- }
103354
- }
103355
- if (closingIdx !== -1) {
103356
- // Collect inner content between tags
103357
- const contentParts = [];
103358
- for (let j = i; j <= closingIdx; j += 1) {
103359
- const node = children[j];
103360
- contentParts.push(collectTextContent(node));
103361
- }
103362
- // Remove the opening/closing tags and template literal syntax from content
103363
- let content = contentParts.join('');
103364
- content = content.replace(/^<HTMLBlock[^>]*>\s*\{?\s*`?/, '').replace(/`?\s*\}?\s*<\/HTMLBlock>$/, '');
103365
- // Decode protected content that was base64 encoded during preprocessing
103366
- content = decodeProtectedContent(content);
103367
- const htmlString = formatHtmlForMdxish(content);
103368
- const runScripts = extractRunScriptsAttr(attrs);
103369
- const safeMode = extractBooleanAttr(attrs, 'safeMode');
103370
- // Replace range with single HTMLBlock node
103371
- const mdNode = createHTMLBlockNode(htmlString, children[i].position, runScripts, safeMode);
103372
- root.children.splice(i, closingIdx - i + 1, mdNode);
103373
- }
103374
- }
103375
- i += 1;
103376
- }
103112
+ // Shape 1: tokenized JSX element.
103113
+ visit(tree, node => node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement', (node, index, parent) => {
103114
+ const element = node;
103115
+ if (element.name !== 'HTMLBlock' || !parent || index === undefined)
103116
+ return;
103117
+ const exprChild = element.children.find(child => child.type === 'mdxFlowExpression' || child.type === 'mdxTextExpression');
103118
+ const openingTagIndent = (element.position?.start.column ?? 1) - 1;
103119
+ parent.children[index] = createHtmlBlockNode(formatHtmlForMdxish(extractTemplateLiteral(exprChild?.value), openingTagIndent), element.position, toRunScripts(jsxAttr(element, 'runScripts')), jsxAttr(element, 'safeMode'));
103377
103120
  });
103378
- // Handle HTMLBlock parsed as HTML elements (when template literal contains block-level HTML tags)
103121
+ // Shape 2: raw HTML blob.
103379
103122
  visit(tree, 'html', (node, index, parent) => {
103380
103123
  if (!parent || index === undefined)
103381
103124
  return;
103382
- const value = node.value;
103383
- if (!value)
103384
- return;
103385
- // Case 1: Full HTMLBlock in single node
103386
- const fullMatch = value.match(/^<HTMLBlock(\s[^>]*)?>([\s\S]*)<\/HTMLBlock>$/);
103387
- if (fullMatch) {
103388
- const attrs = fullMatch[1] || '';
103389
- let content = fullMatch[2] || '';
103390
- // Remove template literal syntax if present: {`...`}
103391
- content = content.replace(/^\s*\{\s*`/, '').replace(/`\s*\}\s*$/, '');
103392
- // Decode protected content that was base64 encoded during preprocessing
103393
- content = decodeProtectedContent(content);
103394
- const htmlString = formatHtmlForMdxish(content);
103395
- const runScripts = extractRunScriptsAttr(attrs);
103396
- const safeMode = extractBooleanAttr(attrs, 'safeMode');
103397
- parent.children[index] = createHTMLBlockNode(htmlString, node.position, runScripts, safeMode);
103398
- return;
103399
- }
103400
- // Case 2: Opening tag only (split by blank lines)
103401
- if (value === '<HTMLBlock>' || value.match(/^<HTMLBlock\s[^>]*>$/)) {
103402
- const siblings = parent.children;
103403
- let closingIdx = -1;
103404
- // Find closing tag in siblings
103405
- for (let i = index + 1; i < siblings.length; i += 1) {
103406
- const sibling = siblings[i];
103407
- if (sibling.type === 'html') {
103408
- const sibVal = sibling.value;
103409
- if (sibVal === '</HTMLBlock>' || sibVal?.includes('</HTMLBlock>')) {
103410
- closingIdx = i;
103411
- break;
103412
- }
103413
- }
103414
- }
103415
- if (closingIdx === -1)
103416
- return;
103417
- // Collect content between tags, skipping template literal delimiters
103418
- const contentParts = [];
103419
- for (let i = index + 1; i < closingIdx; i += 1) {
103420
- const sibling = siblings[i];
103421
- // Skip template literal delimiters
103422
- if (sibling.type === 'text') {
103423
- const textVal = sibling.value;
103424
- if (textVal === '{' || textVal === '}' || textVal === '{`' || textVal === '`}') {
103425
- // eslint-disable-next-line no-continue
103426
- continue;
103427
- }
103428
- }
103429
- contentParts.push(collectTextContent(sibling));
103430
- }
103431
- // Decode protected content that was base64 encoded during preprocessing
103432
- const decodedContent = decodeProtectedContent(contentParts.join(''));
103433
- const htmlString = formatHtmlForMdxish(decodedContent);
103434
- const runScripts = extractRunScriptsAttr(value);
103435
- const safeMode = extractBooleanAttr(value, 'safeMode');
103436
- // Replace opening tag with HTMLBlock node, remove consumed siblings
103437
- parent.children[index] = createHTMLBlockNode(htmlString, node.position, runScripts, safeMode);
103438
- parent.children.splice(index + 1, closingIdx - index);
103439
- }
103125
+ const replacement = splitRawHtmlBlocks(node);
103126
+ if (replacement)
103127
+ parent.children.splice(index, 1, ...replacement);
103440
103128
  });
103441
- // Handle HTMLBlock inside paragraphs (parsed as inline elements)
103442
- visit(tree, 'paragraph', (node, index, parent) => {
103443
- if (!parent || index === undefined)
103444
- return;
103445
- const children = node.children || [];
103446
- let htmlBlockStartIdx = -1;
103447
- let htmlBlockEndIdx = -1;
103448
- let templateLiteralStartIdx = -1;
103449
- let templateLiteralEndIdx = -1;
103129
+ // Shape 3: inline within a paragraph `<HTMLBlock>` open/close arrive as
103130
+ // separate `html` siblings with the template-literal expression between them.
103131
+ visit(tree, 'paragraph', (paragraph) => {
103132
+ // An html-block is block content, so it isn't a valid PhrasingContent child;
103133
+ // widen to RootContent (which HTMLBlock belongs to) for the in-place splice.
103134
+ const children = paragraph.children;
103450
103135
  for (let i = 0; i < children.length; i += 1) {
103451
- const child = children[i];
103452
- if (child.type === 'html' && typeof child.value === 'string') {
103453
- const value = child.value;
103454
- if (value === '<HTMLBlock>' || value.match(/^<HTMLBlock\s[^>]*>$/)) {
103455
- htmlBlockStartIdx = i;
103456
- }
103457
- else if (value === '</HTMLBlock>') {
103458
- htmlBlockEndIdx = i;
103459
- }
103460
- }
103461
- // Find opening brace after HTMLBlock start
103462
- if (htmlBlockStartIdx !== -1 && templateLiteralStartIdx === -1 && child.type === 'text') {
103463
- const value = child.value;
103464
- if (value === '{') {
103465
- templateLiteralStartIdx = i;
103466
- }
103467
- }
103468
- // Find closing brace before HTMLBlock end
103469
- if (htmlBlockStartIdx !== -1 && htmlBlockEndIdx === -1 && child.type === 'text') {
103470
- const value = child.value;
103471
- if (value === '}') {
103472
- templateLiteralEndIdx = i;
103136
+ const open = children[i];
103137
+ const openMatch = open.type === 'html' ? open.value.match(HTML_BLOCK_OPEN_RE) : null;
103138
+ if (!openMatch)
103139
+ continue; // eslint-disable-line no-continue
103140
+ const closeIdx = children.findIndex((child, j) => j > i && child.type === 'html' && child.value === '</HTMLBlock>');
103141
+ if (closeIdx === -1)
103142
+ continue; // eslint-disable-line no-continue
103143
+ const body = children
103144
+ .slice(i + 1, closeIdx)
103145
+ .map(child => {
103146
+ if (child.type === 'mdxTextExpression' || child.type === 'mdxFlowExpression') {
103147
+ return extractTemplateLiteral(child.value);
103473
103148
  }
103474
- }
103475
- }
103476
- if (htmlBlockStartIdx !== -1 &&
103477
- htmlBlockEndIdx !== -1 &&
103478
- templateLiteralStartIdx !== -1 &&
103479
- templateLiteralEndIdx !== -1 &&
103480
- templateLiteralStartIdx < templateLiteralEndIdx) {
103481
- const openingTag = children[htmlBlockStartIdx];
103482
- // Collect content between braces (handles code blocks)
103483
- const templateContent = [];
103484
- for (let i = templateLiteralStartIdx + 1; i < templateLiteralEndIdx; i += 1) {
103485
- const child = children[i];
103486
- templateContent.push(collectTextContent(child));
103487
- }
103488
- // Decode protected content that was base64 encoded during preprocessing
103489
- const decodedContent = decodeProtectedContent(templateContent.join(''));
103490
- const htmlString = formatHtmlForMdxish(decodedContent);
103491
- const runScripts = openingTag.value ? extractRunScriptsAttr(openingTag.value) : undefined;
103492
- const safeMode = openingTag.value ? extractBooleanAttr(openingTag.value, 'safeMode') : undefined;
103493
- const mdNode = createHTMLBlockNode(htmlString, node.position, runScripts, safeMode);
103494
- parent.children[index] = mdNode;
103495
- }
103496
- });
103497
- // Ensure html-block nodes have HTML in children as text node
103498
- visit(tree, 'html-block', (node) => {
103499
- const html = node.data?.hProperties?.html;
103500
- if (html &&
103501
- (!node.children ||
103502
- node.children.length === 0 ||
103503
- (node.children.length === 1 && node.children[0].type === 'text' && node.children[0].value !== html))) {
103504
- node.children = [
103505
- {
103506
- type: 'text',
103507
- value: html,
103508
- },
103509
- ];
103149
+ // Preserve raw text from any other phrasing sibling (e.g. stray
103150
+ // whitespace or content the tokenizer didn't claim) so it isn't
103151
+ // silently dropped from the html payload.
103152
+ return 'value' in child && typeof child.value === 'string' ? child.value : '';
103153
+ })
103154
+ .join('');
103155
+ const openingTagIndent = (open.position?.start.column ?? 1) - 1;
103156
+ children.splice(i, closeIdx - i + 1, htmlBlockFromRaw(openMatch[1], body, open.position, openingTagIndent));
103510
103157
  }
103511
103158
  });
103512
- return tree;
103513
103159
  };
103514
103160
  /* harmony default export */ const mdxish_html_blocks = (mdxishHtmlBlocks);
103515
103161
 
@@ -103741,7 +103387,7 @@ const transformAnchor = (jsx) => {
103741
103387
  */
103742
103388
  const transformImage = (jsx) => {
103743
103389
  const attrs = getAttrs(jsx);
103744
- const { align, alt = '', border, caption, className, framed, height, lazy, src = '', title = '', width } = attrs;
103390
+ const { align, alt = '', border, caption, className, framed, height, lazy, src = '', title = '', width, wrap, } = attrs;
103745
103391
  const validAlign = toImageAlign(align);
103746
103392
  const sizing = width !== undefined ? String(width) : undefined;
103747
103393
  const hProperties = {
@@ -103757,6 +103403,7 @@ const transformImage = (jsx) => {
103757
103403
  ...(lazy !== undefined && { lazy: toBool(lazy) }),
103758
103404
  ...(sizing && { sizing }),
103759
103405
  ...(sizing && { width: sizing }),
103406
+ ...(wrap !== undefined && { wrap: toBool(wrap) }),
103760
103407
  };
103761
103408
  return {
103762
103409
  type: NodeTypes.imageBlock,
@@ -103773,6 +103420,7 @@ const transformImage = (jsx) => {
103773
103420
  src,
103774
103421
  title,
103775
103422
  width: sizing,
103423
+ ...(wrap !== undefined && { wrap: toBool(wrap) }),
103776
103424
  data: {
103777
103425
  hName: 'img',
103778
103426
  hProperties,
@@ -104288,6 +103936,189 @@ const normalizeMdxJsxNodes = () => tree => {
104288
103936
  };
104289
103937
  /* harmony default export */ const normalize_mdx_jsx_nodes = (normalizeMdxJsxNodes);
104290
103938
 
103939
+ ;// ./lib/micromark/jsx-comment/pattern.ts
103940
+ /**
103941
+ * Matches a JSX comment: `{/*`, content, `*\/}` — no whitespace tolerated
103942
+ * between the braces and the comment markers.
103943
+ *
103944
+ * This grammar is mirrored by the flow tokenizer in ./syntax.ts. Any change
103945
+ * here needs a mirror change in the state machine; the parity test in
103946
+ * __tests__/lib/micromark/jsx-comment-pattern-parity.test.ts locks the two
103947
+ * together so they can't silently drift.
103948
+ */
103949
+ const JSX_COMMENT_REGEX = /\{\/\*[^*]*(?:\*(?!\/)[^*]*)*\*\/\}/g;
103950
+
103951
+ ;// ./processor/transform/mdxish/preprocess-jsx-expressions.ts
103952
+
103953
+
103954
+ /**
103955
+ * Removes JSX-style comments (e.g., { /* comment *\/ }) from content.
103956
+ *
103957
+ * @param content
103958
+ * @returns Content with JSX comments removed
103959
+ * @example
103960
+ * ```typescript
103961
+ * removeJSXComments('Text { /* comment *\/ } more text')
103962
+ * // Returns: 'Text more text'
103963
+ * ```
103964
+ */
103965
+ function removeJSXComments(content) {
103966
+ return content.replace(JSX_COMMENT_REGEX, '');
103967
+ }
103968
+ const HTML_ELEM_PLACEHOLDER_PREFIX = '___MDXISH_HTML_ELEM_';
103969
+ const HTML_ELEM_PLACEHOLDER = new RegExp(`${HTML_ELEM_PLACEHOLDER_PREFIX}(\\d+)___`, 'g');
103970
+ // Matches an HTML element that starts at a line boundary and ends at a line boundary.
103971
+ // Allows optional leading indentation and lazily matches until the same closing tag.
103972
+ const BLOCK_HTML_RE = /(?<=^|\n)[ \t]*<([a-z][a-zA-Z0-9]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>[ \t]*(?=\n|$)/g;
103973
+ /**
103974
+ * Hides line-anchored HTML elements from the brace-escaping pass so we don't leak `\{`
103975
+ * into rendered output (rehypeRaw renders the `\` literally, e.g. `<div>{foo</div>`).
103976
+ *
103977
+ * One carve-out: if an interior line at column 0 has bare text containing `{`, mdxish
103978
+ * parses that line as a paragraph and the mdxExpression step would throw without an
103979
+ * escape — so we leave that case to the brace balancer.
103980
+ */
103981
+ function protectHTMLElements(content) {
103982
+ const htmlElements = [];
103983
+ const protectedContent = content.replace(BLOCK_HTML_RE, match => {
103984
+ // Look at the lines between the open and close tags. If any of them starts
103985
+ // at column 0 with bare text (not whitespace, not another tag) and contains
103986
+ // `{`, mdxish will parse that line as a paragraph and the brace as an MDX
103987
+ // expression, which would throw an error. So we let the brace balancer escape it.
103988
+ // Otherwise, we need to extract the sequence to protect it from the brace escaping.
103989
+ const interior = match.split('\n').slice(1, -1);
103990
+ const hazard = interior.some(line => line.length > 0 && line[0] !== ' ' && line[0] !== '\t' && line[0] !== '<' && line.includes('{'));
103991
+ if (hazard)
103992
+ return match;
103993
+ htmlElements.push(match);
103994
+ return `${HTML_ELEM_PLACEHOLDER_PREFIX}${htmlElements.length - 1}___`;
103995
+ });
103996
+ return { htmlElements, protectedContent };
103997
+ }
103998
+ function restoreHTMLElements(content, htmlElements) {
103999
+ if (htmlElements.length === 0)
104000
+ return content;
104001
+ return content.replace(HTML_ELEM_PLACEHOLDER, (_m, idx) => htmlElements[parseInt(idx, 10)]);
104002
+ }
104003
+ /**
104004
+ * Escapes unbalanced and paragraph-spanning braces so MDX doesn't trip on them.
104005
+ */
104006
+ function escapeProblematicBraces(content) {
104007
+ const { htmlElements, protectedContent } = protectHTMLElements(content);
104008
+ let strDelim = null;
104009
+ let strEscaped = false;
104010
+ // Track position of last newline (outside strings) to detect blank lines
104011
+ // -2 means no recent newline
104012
+ let lastNewlinePos = -2;
104013
+ // Character state machine trackers
104014
+ const toEscape = new Set();
104015
+ // Convert to array of Unicode code points so that emojis and multi-byte characters are correctly tracked
104016
+ const chars = Array.from(protectedContent);
104017
+ const openStack = [];
104018
+ for (let i = 0; i < chars.length; i += 1) {
104019
+ const ch = chars[i];
104020
+ // Track string delimiters inside expressions to ignore braces within them
104021
+ if (openStack.length > 0) {
104022
+ if (strDelim) {
104023
+ if (strEscaped)
104024
+ strEscaped = false;
104025
+ else if (ch === '\\')
104026
+ strEscaped = true;
104027
+ else if (ch === strDelim)
104028
+ strDelim = null;
104029
+ // eslint-disable-next-line no-continue
104030
+ continue;
104031
+ }
104032
+ if (ch === '"' || ch === "'" || ch === '`') {
104033
+ strDelim = ch;
104034
+ // eslint-disable-next-line no-continue
104035
+ continue;
104036
+ }
104037
+ if (ch === '\n') {
104038
+ if (lastNewlinePos >= 0) {
104039
+ const between = chars.slice(lastNewlinePos + 1, i).join('');
104040
+ if (/^[ \t]*$/.test(between)) {
104041
+ openStack.forEach(entry => { entry.hasBlankLine = true; });
104042
+ }
104043
+ }
104044
+ lastNewlinePos = i;
104045
+ }
104046
+ }
104047
+ // Skip already-escaped braces (odd run of preceding backslashes).
104048
+ if (ch === '{' || ch === '}') {
104049
+ let bs = 0;
104050
+ for (let j = i - 1; j >= 0 && chars[j] === '\\'; j -= 1)
104051
+ bs += 1;
104052
+ if (bs % 2 === 1) {
104053
+ // eslint-disable-next-line no-continue
104054
+ continue;
104055
+ }
104056
+ }
104057
+ if (ch === '{') {
104058
+ // `=` (after whitespace) before `{` ⇒ JSX attribute expression. The
104059
+ // mdxComponent tokenizer captures the whole component, so blank lines
104060
+ // inside attribute values are harmless. Nested `{` inherits the flag.
104061
+ let isAttrExpr = false;
104062
+ for (let j = i - 1; j >= 0; j -= 1) {
104063
+ const pc = chars[j];
104064
+ if (pc === '=') {
104065
+ isAttrExpr = true;
104066
+ break;
104067
+ }
104068
+ if (pc !== ' ' && pc !== '\t')
104069
+ break;
104070
+ }
104071
+ // Nested `{ ... }` inside an attribute value (e.g. `data={[{ ... }]}` or
104072
+ // `data={{ a: { b: 1 } }}`) must inherit the same exemption; only the
104073
+ // outer `{` is directly after `=`.
104074
+ if (!isAttrExpr && openStack.length > 0 && openStack[openStack.length - 1].isAttrExpr) {
104075
+ isAttrExpr = true;
104076
+ }
104077
+ openStack.push({ pos: i, hasBlankLine: false, isAttrExpr });
104078
+ lastNewlinePos = -2;
104079
+ }
104080
+ else if (ch === '}') {
104081
+ if (openStack.length > 0) {
104082
+ const entry = openStack.pop();
104083
+ // Pure `{/* ... */}` comments are handled downstream by the jsxComment
104084
+ // tokenizer — escaping their braces would prevent it from running.
104085
+ const isPureJsxComment = chars[entry.pos + 1] === '/' &&
104086
+ chars[entry.pos + 2] === '*' &&
104087
+ chars[i - 1] === '/' &&
104088
+ chars[i - 2] === '*';
104089
+ if (entry.hasBlankLine && !isPureJsxComment && !entry.isAttrExpr) {
104090
+ toEscape.add(entry.pos);
104091
+ toEscape.add(i);
104092
+ }
104093
+ }
104094
+ else {
104095
+ toEscape.add(i);
104096
+ }
104097
+ }
104098
+ }
104099
+ // Anything still open is unbalanced.
104100
+ openStack.forEach(entry => toEscape.add(entry.pos));
104101
+ // Reconstruct the content with the escaped braces.
104102
+ const escapedContent = toEscape.size === 0 ? protectedContent : chars.map((ch, i) => (toEscape.has(i) ? `\\${ch}` : ch)).join('');
104103
+ return restoreHTMLElements(escapedContent, htmlElements);
104104
+ }
104105
+ /**
104106
+ * Preprocesses JSX-like markdown content before parsing.
104107
+ *
104108
+ * JSX attribute expressions (`href={baseUrl}`) are no longer rewritten here —
104109
+ * they flow through the tokenizer as `mdxJsxAttributeValueExpression` nodes
104110
+ * and are evaluated at the hast handler step.
104111
+ *
104112
+ * @param content
104113
+ * @returns Preprocessed content ready for markdown parsing
104114
+ */
104115
+ function preprocessJSXExpressions(content) {
104116
+ const { protectedCode, protectedContent } = protectCodeBlocks(content);
104117
+ let processed = escapeProblematicBraces(protectedContent);
104118
+ processed = restoreCodeBlocks(processed, protectedCode);
104119
+ return processed;
104120
+ }
104121
+
104291
104122
  ;// ./processor/transform/mdxish/restore-snake-case-component-name.ts
104292
104123
 
104293
104124
 
@@ -105404,7 +105235,7 @@ function mdxishAstProcessor(mdContent, opts = {}) {
105404
105235
  .use(inline_html, { safeMode })
105405
105236
  .use(restore_snake_case_component_name, { mapping: snakeCaseMapping })
105406
105237
  .use(mdxish_tables)
105407
- .use(mdxish_html_blocks)
105238
+ .use(mdxish_html_blocks) // Convert every <HTMLBlock> shape → html-block
105408
105239
  // The next few transformers must appear after mdxishMdxComponentBlocks
105409
105240
  // so nodes produced by the inline re-parse of component bodies
105410
105241
  // (e.g. code/image/embed inside <Tabs>) get visited too