@@ -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,15 +85,98 @@ 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
91+ . replace ( / & n b s p ( [ ^ ; ] ) / g, ' $1' )
92+ . replace ( / & a m p ; n b s p ; / g, ' ' ) ;
93+ preprocessedMarkdown = preprocessedMarkdown . replace ( / & n b s p (? ! [ ; ] ) / g, ' ' ) ;
94+
95+ // Process content in segments to ensure markdown parsing continues after HTML blocks
96+ const htmlBlockRegex =
97+ / ( < (?: d e t a i l s | d i v | s e c t i o n | a r t i c l e ) [ ^ > ] * > [ \s \S ] * ?< \/ (?: d e t a i l s | d i v | s e c t i o n | a r t i c l e ) > ) / g;
98+
99+ let parsedMarkdown = '' ;
100+
101+ // Check if there are any HTML blocks
102+ if ( htmlBlockRegex . test ( preprocessedMarkdown ) ) {
103+ // Reset regex for actual processing
104+ htmlBlockRegex . lastIndex = 0 ;
105+
106+ let lastIndex = 0 ;
107+ let match ;
108+
109+ while ( ( match = htmlBlockRegex . exec ( preprocessedMarkdown ) ) !== null ) {
110+ // Process markdown before the HTML block
111+ const markdownBefore = preprocessedMarkdown . slice ( lastIndex , match . index ) . trim ( ) ;
112+ if ( markdownBefore ) {
113+ const parsed = await marked . parse ( markdownBefore ) ;
114+ parsedMarkdown += parsed ;
115+ }
116+
117+ // Process the HTML block: parse markdown content inside while preserving HTML structure
118+ let htmlBlock = match [ 1 ] ;
119+
120+ // Find and process markdown content inside HTML tags
121+ const contentRegex = / > ( \s * [ \s \S ] * ?) \s * < / g;
122+ const contentMatches = [ ] ;
123+ let contentMatch ;
124+
125+ while ( ( contentMatch = contentRegex . exec ( htmlBlock ) ) !== null ) {
126+ const content = contentMatch [ 1 ] ;
127+ // Only process content that has markdown formatting but no extension syntax
128+ if (
129+ content . trim ( ) &&
130+ ! content . includes ( '{{' ) &&
131+ ( content . includes ( '**' ) || content . includes ( '- ' ) || content . includes ( '\n' ) )
132+ ) {
133+ // This looks like markdown content without extensions - parse it as block content
134+ const parsedContent = await marked . parse ( content . trim ( ) ) ;
135+ // Remove wrapping <p> tags if they exist since we're inside HTML already
136+ const cleanedContent = parsedContent . replace ( / ^ < p [ ^ > ] * > ( [ \s \S ] * ) < \/ p > [ \s ] * $ / g, '$1' ) ;
137+ contentMatches . push ( {
138+ original : contentMatch [ 0 ] ,
139+ replacement : `>${ cleanedContent } <` ,
140+ } ) ;
141+ }
142+ }
143+
144+ // Apply the content replacements
145+ contentMatches . forEach ( ( { original, replacement } ) => {
146+ htmlBlock = htmlBlock . replace ( original , replacement ) ;
147+ } ) ;
148+
149+ // Apply extensions (like admonitions) to the processed HTML block
150+ if ( extensions ) {
151+ extensions . forEach ( ( { regex, replace } ) => {
152+ if ( regex ) {
153+ htmlBlock = htmlBlock . replace ( regex , replace ) ;
154+ }
155+ } ) ;
156+ }
157+
158+ parsedMarkdown += htmlBlock ;
159+ lastIndex = htmlBlockRegex . lastIndex ;
160+ }
161+
162+ // Process any remaining markdown after the last HTML block
163+ const markdownAfter = preprocessedMarkdown . slice ( lastIndex ) . trim ( ) ;
164+ if ( markdownAfter ) {
165+ const parsed = await marked . parse ( markdownAfter ) ;
166+ parsedMarkdown += parsed ;
167+ }
168+ } else {
169+ // No HTML blocks found, process normally
170+ parsedMarkdown = await marked . parse ( preprocessedMarkdown ) ;
171+ }
89172 // Swap the temporary tokens back to code fences before we run the extensions
90173 let md = parsedMarkdown . replace ( / @ @ @ / g, '```' ) ;
91174
92175 if ( extensions ) {
93176 // Convert code spans back to md format before we run the custom extension regexes
94177 md = md . replace ( / < c o d e > ( .* ) < \/ c o d e > / g, '`$1`' ) ;
95178
96- extensions . forEach ( ( { regex, replace } ) => {
179+ extensions . forEach ( ( { regex, replace } , _index ) => {
97180 if ( regex ) {
98181 md = md . replace ( regex , replace ) ;
99182 }
@@ -102,6 +185,7 @@ export const markdownConvert = async (markdown: string, extensions?: ShowdownExt
102185 // Convert any remaining backticks back into code spans
103186 md = md . replace ( / ` ( .* ) ` / g, '<code>$1</code>' ) ;
104187 }
188+
105189 return DOMPurify . sanitize ( md ) ;
106190} ;
107191
@@ -210,7 +294,10 @@ const InlineMarkdownView: FC<InnerSyncMarkdownProps> = ({
210294 const id = useMemo ( ( ) => uniqueId ( 'markdown' ) , [ ] ) ;
211295 return (
212296 < div className = { css ( { 'is-empty' : isEmpty } as any , className ) } id = { id } >
213- < div dangerouslySetInnerHTML = { { __html : markup } } />
297+ < div
298+ style = { { marginBlockEnd : 'var(--pf-t-global--spacer--md)' } }
299+ dangerouslySetInnerHTML = { { __html : markup } }
300+ />
214301 { renderExtension && (
215302 < RenderExtension renderExtension = { renderExtension } selector = { `#${ id } ` } markup = { markup } />
216303 ) }
@@ -299,6 +386,7 @@ const IFrameMarkdownView: FC<InnerSyncMarkdownProps> = ({
299386 return (
300387 < >
301388 < iframe
389+ title = "Markdown content preview"
302390 sandbox = "allow-popups allow-popups-to-escape-sandbox allow-same-origin"
303391 srcDoc = { contents }
304392 style = { { border : '0px' , display : 'block' , width : '100%' , height : '0' } }
0 commit comments