diff --git a/packages/module/package.json b/packages/module/package.json index e79fdf46..6683157f 100644 --- a/packages/module/package.json +++ b/packages/module/package.json @@ -66,7 +66,7 @@ "@rollup/plugin-commonjs": "^17.0.0", "@rollup/plugin-json": "^4.1.0", "@rollup/plugin-node-resolve": "^11.1.0", - "@testing-library/react": "^11.2.2", + "@testing-library/react": "^13.4.0", "@types/dompurify": "^3.0.5", "@types/enzyme": "^3.10.7", "@types/enzyme-adapter-react-16": "^1.0.6", diff --git a/packages/module/src/ConsoleInternal/components/markdown-view.tsx b/packages/module/src/ConsoleInternal/components/markdown-view.tsx index c3fa0eb1..e7291726 100644 --- a/packages/module/src/ConsoleInternal/components/markdown-view.tsx +++ b/packages/module/src/ConsoleInternal/components/markdown-view.tsx @@ -25,7 +25,7 @@ export const markdownConvert = async (markdown: string, extensions?: ShowdownExt return node; } - // add PF content classes + // add PF content classes to standard elements (details blocks get handled separately) if (node.nodeType === 1) { const contentElements = [ 'ul', @@ -85,7 +85,90 @@ export const markdownConvert = async (markdown: string, extensions?: ShowdownExt ); const markdownWithSubstitutedCodeFences = reverseString(reverseMarkdownWithSubstitutedCodeFences); - const parsedMarkdown = await marked.parse(markdownWithSubstitutedCodeFences); + // Fix malformed HTML entities early in the process + let preprocessedMarkdown = markdownWithSubstitutedCodeFences; + preprocessedMarkdown = preprocessedMarkdown + .replace(/ ([^;])/g, ' $1') + .replace(/&nbsp;/g, ' '); + preprocessedMarkdown = preprocessedMarkdown.replace(/ (?![;])/g, ' '); + + // Process content in segments to ensure markdown parsing continues after HTML blocks + const htmlBlockRegex = + /(<(?:details|div|section|article)[^>]*>[\s\S]*?<\/(?:details|div|section|article)>)/g; + + let parsedMarkdown = ''; + + // Check if there are any HTML blocks + if (htmlBlockRegex.test(preprocessedMarkdown)) { + // Reset regex for actual processing + htmlBlockRegex.lastIndex = 0; + + let lastIndex = 0; + let match; + + while ((match = htmlBlockRegex.exec(preprocessedMarkdown)) !== null) { + // Process markdown before the HTML block + const markdownBefore = preprocessedMarkdown.slice(lastIndex, match.index).trim(); + if (markdownBefore) { + const parsed = await marked.parse(markdownBefore); + parsedMarkdown += parsed; + } + + // Process the HTML block: parse markdown content inside while preserving HTML structure + let htmlBlock = match[1]; + + // Find and process markdown content inside HTML tags + const contentRegex = />(\s*[\s\S]*?)\s* tags if they exist since we're inside HTML already + const cleanedContent = parsedContent.replace(/^]*>([\s\S]*)<\/p>[\s]*$/g, '$1'); + contentMatches.push({ + original: contentMatch[0], + replacement: `>${cleanedContent}<`, + }); + } + } + + // Apply the content replacements + contentMatches.forEach(({ original, replacement }) => { + htmlBlock = htmlBlock.replace(original, replacement); + }); + + // Apply extensions (like admonitions) to the processed HTML block + if (extensions) { + extensions.forEach(({ regex, replace }) => { + if (regex) { + htmlBlock = htmlBlock.replace(regex, replace); + } + }); + } + + parsedMarkdown += htmlBlock; + lastIndex = htmlBlockRegex.lastIndex; + } + + // Process any remaining markdown after the last HTML block + const markdownAfter = preprocessedMarkdown.slice(lastIndex).trim(); + if (markdownAfter) { + const parsed = await marked.parse(markdownAfter); + parsedMarkdown += parsed; + } + } else { + // No HTML blocks found, process normally + parsedMarkdown = await marked.parse(preprocessedMarkdown); + } // Swap the temporary tokens back to code fences before we run the extensions let md = parsedMarkdown.replace(/@@@/g, '```'); @@ -93,7 +176,7 @@ export const markdownConvert = async (markdown: string, extensions?: ShowdownExt // Convert code spans back to md format before we run the custom extension regexes md = md.replace(/(.*)<\/code>/g, '`$1`'); - extensions.forEach(({ regex, replace }) => { + extensions.forEach(({ regex, replace }, _index) => { if (regex) { md = md.replace(regex, replace); } @@ -102,6 +185,7 @@ export const markdownConvert = async (markdown: string, extensions?: ShowdownExt // Convert any remaining backticks back into code spans md = md.replace(/`(.*)`/g, '$1'); } + return DOMPurify.sanitize(md); }; @@ -210,7 +294,10 @@ const InlineMarkdownView: FC = ({ const id = useMemo(() => uniqueId('markdown'), []); return (
-
+
{renderExtension && ( )} @@ -299,6 +386,7 @@ const IFrameMarkdownView: FC = ({ return ( <>