diff --git a/__tests__/lib/stripComments.test.ts b/__tests__/lib/stripComments.test.ts
index 6794ab628..4c6864dfb 100644
--- a/__tests__/lib/stripComments.test.ts
+++ b/__tests__/lib/stripComments.test.ts
@@ -328,6 +328,29 @@ end"`);
`);
});
+ it.each([
+ ['mdx', { mdx: true }],
+ ['mdxish', { mdxish: true }],
+ ])('preserves comments inside MDX HTMLBlocks (%s)', async (_name, opts) => {
+ const input = `{\`
+
+Hello world
+\`}`;
+ const output = await stripComments(input, opts);
+ expect(output).toContain('');
+ });
+
+ it('preserves HTMLBlock template literal expressions in mdxish mode', async () => {
+ const input = `{\`
+
+ Hello, World!
+
+\`}`;
+
+ const output = await stripComments(input, { mdxish: true });
+ expect(output).toBe(input);
+ });
+
it('preserves jsx tables in mdxish mode', async () => {
const input = `
diff --git a/lib/stripComments.ts b/lib/stripComments.ts
index 5bd204141..e5839a0f8 100644
--- a/lib/stripComments.ts
+++ b/lib/stripComments.ts
@@ -12,7 +12,8 @@ import { stripCommentsTransformer } from '../processor/transform/stripComments';
import { jsxTableFromMarkdown } from './mdast-util/jsx-table';
import { jsxTable } from './micromark/jsx-table';
-import { extractMagicBlocks, restoreMagicBlocks } from './utils/extractMagicBlocks';
+import { protectHTMLBlockContent, restoreHTMLBlockContent } from './utils/extractors/html-blocks';
+import { extractMagicBlocks, restoreMagicBlocks } from './utils/extractors/magic-blocks';
interface Opts {
mdx?: boolean;
@@ -23,7 +24,16 @@ interface Opts {
* Removes Markdown and MDX comments.
*/
async function stripComments(doc: string, { mdx, mdxish }: Opts = {}): Promise {
- const { replaced, blocks } = extractMagicBlocks(doc);
+ // Preprocessing step: Don't touch magic blocks and HTML block content
+ let preprocessedDoc = doc;
+ const { replaced, blocks } = extractMagicBlocks(preprocessedDoc);
+ preprocessedDoc = replaced;
+
+ // We only need to protect HTML block content if we're in MDXish mode
+ // MDX has correctly handled them
+ if (mdxish) {
+ preprocessedDoc = protectHTMLBlockContent(preprocessedDoc);
+ }
const processor = unified();
@@ -75,10 +85,15 @@ async function stripComments(doc: string, { mdx, mdxish }: Opts = {}): Promise{` ... `}`
+ */
+export const HTMLBLOCK_TEMPLATE_LITERAL_REGEX = /(]*>)\{\s*`((?:[^`\\]|\\.)*)`\s*\}(<\/HTMLBlock>)/g;
+
+/**
+ * Base64 encodes HTMLBlock template literal content to prevent markdown parser from consuming `}';
+ * protectHTMLBlockContent(input)
+ * // Returns: 'RDMX-!#@-HTMLBLOCK-START%:PHNjcmlwdD5hbGVydCgieHNzIik8L3NjcmlwdD4=:%RDMX-!#@-HTMLBLOCK-END'
+ * ```
+ */
+export function protectHTMLBlockContent(content: string) {
+ return content.replace(
+ HTMLBLOCK_TEMPLATE_LITERAL_REGEX,
+ (_match, openTag: string, templateContent: string, closeTag: string) => {
+ const encoded = base64Encode(templateContent);
+ return `${openTag}${HTML_BLOCK_CONTENT_START}${encoded}${HTML_BLOCK_CONTENT_END}${closeTag}`;
+ },
+ );
+}
+
+/**
+ * Restores HTMLBlock content that was protected by `protectHTMLBlockContent`.
+ * When `withBackticks` is true, re-wraps the decoded body as `{`...`}` (used by `stripComments` to preserve
+ * original markup). When false or omitted, inserts decoded HTML only (typical for transformers).
+ *
+ * @param content
+ * @param withBackticks
+ * @returns Content with protected markers replaced by decoded HTML, optionally wrapped in template literal syntax
+ * @example
+ * ```typescript
+ * const input =
+ * 'RMDX-!#@-HTMLBLOCK-START%:PHNjcmlwdD5hbGVydCgieHNzIik8L3NjcmlwdD4=:RMDX-!#@-HTMLBLOCK-END';
+ * restoreHTMLBlockContent(input, true);
+ * // Returns: '{``}'
+ * restoreHTMLBlockContent(input);
+ * // Returns: ''
+ * ```
+ */
+export function restoreHTMLBlockContent(content: string, withBackticks?: boolean) {
+ const markerRegex = new RegExp(`${HTML_BLOCK_CONTENT_START}([A-Za-z0-9+/=]+)${HTML_BLOCK_CONTENT_END}`, 'g');
+ return content.replace(markerRegex, (_match, encoded: string) => {
+ try {
+ const decoded = base64Decode(encoded);
+ return withBackticks ? `{\`${decoded}\`}` : decoded;
+ } catch {
+ return encoded;
+ }
+ });
+}
diff --git a/lib/utils/extractMagicBlocks.ts b/lib/utils/extractors/magic-blocks.ts
similarity index 100%
rename from lib/utils/extractMagicBlocks.ts
rename to lib/utils/extractors/magic-blocks.ts
diff --git a/processor/transform/mdxish/mdxish-html-blocks.ts b/processor/transform/mdxish/mdxish-html-blocks.ts
index 26398b11f..b14f4af53 100644
--- a/processor/transform/mdxish/mdxish-html-blocks.ts
+++ b/processor/transform/mdxish/mdxish-html-blocks.ts
@@ -5,28 +5,9 @@ import type { Transform } from 'mdast-util-from-markdown';
import { visit } from 'unist-util-visit';
import { NodeTypes } from '../../../enums';
+import { restoreHTMLBlockContent } from '../../../lib/utils/extractors/html-blocks';
import { formatHtmlForMdxish } from '../../utils';
-import { base64Decode, HTML_BLOCK_CONTENT_END, HTML_BLOCK_CONTENT_START } from './preprocess-jsx-expressions';
-
-/**
- * Decodes HTMLBlock content that was protected during preprocessing.
- * Content is wrapped in
- */
-function decodeProtectedContent(content: string): string {
- // Escape special regex characters in the markers
- const startEscaped = HTML_BLOCK_CONTENT_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
- const endEscaped = HTML_BLOCK_CONTENT_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
- const markerRegex = new RegExp(`${startEscaped}([A-Za-z0-9+/=]+)${endEscaped}`, 'g');
- return content.replace(markerRegex, (_match, encoded: string) => {
- try {
- return base64Decode(encoded);
- } catch {
- return encoded;
- }
- });
-}
-
/**
* Collects text content from a node and its children recursively
*/
@@ -205,7 +186,7 @@ const mdxishHtmlBlocks = (): Transform => tree => {
let content = contentParts.join('');
content = content.replace(/^]*>\s*\{?\s*`?/, '').replace(/`?\s*\}?\s*<\/HTMLBlock>$/, '');
// Decode protected content that was base64 encoded during preprocessing
- content = decodeProtectedContent(content);
+ content = restoreHTMLBlockContent(content);
const htmlString = formatHtmlForMdxish(content);
const runScripts = extractRunScriptsAttr(attrs);
@@ -241,7 +222,7 @@ const mdxishHtmlBlocks = (): Transform => tree => {
// Remove template literal syntax if present: {`...`}
content = content.replace(/^\s*\{\s*`/, '').replace(/`\s*\}\s*$/, '');
// Decode protected content that was base64 encoded during preprocessing
- content = decodeProtectedContent(content);
+ content = restoreHTMLBlockContent(content);
const htmlString = formatHtmlForMdxish(content);
const runScripts = extractRunScriptsAttr(attrs);
@@ -286,7 +267,7 @@ const mdxishHtmlBlocks = (): Transform => tree => {
}
// Decode protected content that was base64 encoded during preprocessing
- const decodedContent = decodeProtectedContent(contentParts.join(''));
+ const decodedContent = restoreHTMLBlockContent(contentParts.join(''));
const htmlString = formatHtmlForMdxish(decodedContent);
const runScripts = extractRunScriptsAttr(value);
const safeMode = extractBooleanAttr(value, 'safeMode');
@@ -356,7 +337,7 @@ const mdxishHtmlBlocks = (): Transform => tree => {
}
// Decode protected content that was base64 encoded during preprocessing
- const decodedContent = decodeProtectedContent(templateContent.join(''));
+ const decodedContent = restoreHTMLBlockContent(templateContent.join(''));
const htmlString = formatHtmlForMdxish(decodedContent);
const runScripts = openingTag.value ? extractRunScriptsAttr(openingTag.value) : undefined;
diff --git a/processor/transform/mdxish/preprocess-jsx-expressions.ts b/processor/transform/mdxish/preprocess-jsx-expressions.ts
index a274f70c0..0fca5ae42 100644
--- a/processor/transform/mdxish/preprocess-jsx-expressions.ts
+++ b/processor/transform/mdxish/preprocess-jsx-expressions.ts
@@ -1,4 +1,5 @@
import { JSX_COMMENT_REGEX } from '../../../lib/micromark/jsx-comment/pattern';
+import { protectHTMLBlockContent } from '../../../lib/utils/extractors/html-blocks';
import {
type ProtectedCode,
protectCodeBlocks,
@@ -6,22 +7,6 @@ import {
restoreInlineCode,
} from '../../../lib/utils/mdxish/protect-code-blocks';
-// Base64 encode (Node.js + browser compatible)
-function base64Encode(str: string): string {
- if (typeof Buffer !== 'undefined') {
- return Buffer.from(str, 'utf-8').toString('base64');
- }
- return btoa(unescape(encodeURIComponent(str)));
-}
-
-// Base64 decode (Node.js + browser compatible)
-export function base64Decode(str: string): string {
- if (typeof Buffer !== 'undefined') {
- return Buffer.from(str, 'base64').toString('utf-8');
- }
- return decodeURIComponent(escape(atob(str)));
-}
-
function escapeHtmlAttribute(value: string): string {
return value
.replace(/&/g, '&')
@@ -35,10 +20,6 @@ function escapeHtmlAttribute(value: string): string {
// Using a prefix that won't conflict with regular string values
export const JSON_VALUE_MARKER = '__MDXISH_JSON__';
-// Markers for protected HTMLBlock content (HTML comments avoid markdown parsing issues)
-export const HTML_BLOCK_CONTENT_START = '';
-
/**
* Pre-processes JSX-like expressions before markdown parsing.
* Converts href={'value'} to href="value", evaluates {expressions}, etc.
@@ -66,28 +47,6 @@ export function evaluateExpression(expression: string, context: JSXContext) {
return func(...contextValues);
}
-/**
- * Base64 encodes HTMLBlock template literal content to prevent markdown parser from consuming `}';
- * protectHTMLBlockContent(input)
- * // Returns: ''
- * ```
- */
-function protectHTMLBlockContent(content: string): string {
- return content.replace(
- /(]*>)\{\s*`((?:[^`\\]|\\.)*)`\s*\}(<\/HTMLBlock>)/g,
- (_match, openTag: string, templateContent: string, closeTag: string) => {
- const encoded = base64Encode(templateContent);
- return `${openTag}${HTML_BLOCK_CONTENT_START}${encoded}${HTML_BLOCK_CONTENT_END}${closeTag}`;
- },
- );
-}
-
/**
* Removes JSX-style comments (e.g., { /* comment *\/ }) from content.
*