Skip to content

Commit 65e9816

Browse files
committed
fix: correctly render admonitions and accordions
Assisted-by: Cursor
1 parent 262bcb8 commit 65e9816

File tree

8 files changed

+882
-79
lines changed

8 files changed

+882
-79
lines changed

packages/module/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
"@rollup/plugin-commonjs": "^17.0.0",
6767
"@rollup/plugin-json": "^4.1.0",
6868
"@rollup/plugin-node-resolve": "^11.1.0",
69-
"@testing-library/react": "^11.2.2",
69+
"@testing-library/react": "^13.4.0",
7070
"@types/dompurify": "^3.0.5",
7171
"@types/enzyme": "^3.10.7",
7272
"@types/enzyme-adapter-react-16": "^1.0.6",

packages/module/src/ConsoleInternal/components/markdown-view.tsx

Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export const markdownConvert = async (markdown: string, extensions?: ShowdownExt
2525
return node;
2626
}
2727

28-
// add PF content classes
28+
// add PF content classes to standard elements (details blocks get handled separately)
2929
if (node.nodeType === 1) {
3030
const contentElements = [
3131
'ul',
@@ -85,23 +85,100 @@ export const markdownConvert = async (markdown: string, extensions?: ShowdownExt
8585
);
8686
const markdownWithSubstitutedCodeFences = reverseString(reverseMarkdownWithSubstitutedCodeFences);
8787

88-
const parsedMarkdown = await marked.parse(markdownWithSubstitutedCodeFences);
88+
// Fix malformed HTML entities early in the process
89+
let preprocessedMarkdown = markdownWithSubstitutedCodeFences;
90+
preprocessedMarkdown = preprocessedMarkdown.replace(/&nbsp([^;])/g, ' $1').replace(/ /g, ' ');
91+
preprocessedMarkdown = preprocessedMarkdown.replace(/&nbsp(?![;])/g, ' ');
92+
93+
// Process content in segments to ensure markdown parsing continues after HTML blocks
94+
const htmlBlockRegex = /(<(?:details|div|section|article)[^>]*>[\s\S]*?<\/(?:details|div|section|article)>)/g;
95+
96+
let parsedMarkdown = '';
97+
98+
// Check if there are any HTML blocks
99+
if (htmlBlockRegex.test(preprocessedMarkdown)) {
100+
// Reset regex for actual processing
101+
htmlBlockRegex.lastIndex = 0;
102+
103+
let lastIndex = 0;
104+
let match;
105+
106+
while ((match = htmlBlockRegex.exec(preprocessedMarkdown)) !== null) {
107+
// Process markdown before the HTML block
108+
const markdownBefore = preprocessedMarkdown.slice(lastIndex, match.index).trim();
109+
if (markdownBefore) {
110+
const parsed = await marked.parse(markdownBefore);
111+
parsedMarkdown += parsed;
112+
}
113+
114+
// Process the HTML block: parse markdown content inside while preserving HTML structure
115+
let htmlBlock = match[1];
116+
117+
// Find and process markdown content inside HTML tags
118+
const contentRegex = />(\s*[\s\S]*?)\s*</g;
119+
const contentMatches = [];
120+
let contentMatch;
121+
122+
while ((contentMatch = contentRegex.exec(htmlBlock)) !== null) {
123+
const content = contentMatch[1];
124+
// Only process content that has markdown formatting but no extension syntax
125+
if (content.trim() && !content.includes('{{') && (content.includes('**') || content.includes('- ') || content.includes('\n'))) {
126+
// This looks like markdown content without extensions - parse it as block content
127+
const parsedContent = await marked.parse(content.trim());
128+
// Remove wrapping <p> tags if they exist since we're inside HTML already
129+
const cleanedContent = parsedContent.replace(/^<p[^>]*>([\s\S]*)<\/p>[\s]*$/g, '$1');
130+
contentMatches.push({
131+
original: contentMatch[0],
132+
replacement: `>${cleanedContent}<`
133+
});
134+
}
135+
}
136+
137+
// Apply the content replacements
138+
contentMatches.forEach(({ original, replacement }) => {
139+
htmlBlock = htmlBlock.replace(original, replacement);
140+
});
141+
142+
// Apply extensions (like admonitions) to the processed HTML block
143+
if (extensions) {
144+
extensions.forEach(({ regex, replace }) => {
145+
if (regex) {
146+
htmlBlock = htmlBlock.replace(regex, replace);
147+
}
148+
});
149+
}
150+
151+
parsedMarkdown += htmlBlock;
152+
lastIndex = htmlBlockRegex.lastIndex;
153+
}
154+
155+
// Process any remaining markdown after the last HTML block
156+
const markdownAfter = preprocessedMarkdown.slice(lastIndex).trim();
157+
if (markdownAfter) {
158+
const parsed = await marked.parse(markdownAfter);
159+
parsedMarkdown += parsed;
160+
}
161+
} else {
162+
// No HTML blocks found, process normally
163+
parsedMarkdown = await marked.parse(preprocessedMarkdown);
164+
}
89165
// Swap the temporary tokens back to code fences before we run the extensions
90166
let md = parsedMarkdown.replace(/@@@/g, '```');
91167

92168
if (extensions) {
93169
// Convert code spans back to md format before we run the custom extension regexes
94170
md = md.replace(/<code>(.*)<\/code>/g, '`$1`');
95171

96-
extensions.forEach(({ regex, replace }) => {
172+
extensions.forEach(({ regex, replace }, index) => {
97173
if (regex) {
98174
md = md.replace(regex, replace);
99175
}
100176
});
101177

102178
// Convert any remaining backticks back into code spans
103179
md = md.replace(/`(.*)`/g, '<code>$1</code>');
104-
}
180+
}
181+
105182
return DOMPurify.sanitize(md);
106183
};
107184

@@ -210,7 +287,7 @@ const InlineMarkdownView: FC<InnerSyncMarkdownProps> = ({
210287
const id = useMemo(() => uniqueId('markdown'), []);
211288
return (
212289
<div className={css({ 'is-empty': isEmpty } as any, className)} id={id}>
213-
<div dangerouslySetInnerHTML={{ __html: markup }} />
290+
<div style={{ marginBlockEnd: 'var(--pf-t-global--spacer--md)' }} dangerouslySetInnerHTML={{ __html: markup }} />
214291
{renderExtension && (
215292
<RenderExtension renderExtension={renderExtension} selector={`#${id}`} markup={markup} />
216293
)}

0 commit comments

Comments
 (0)