@@ -565,37 +565,60 @@ class MappingInputProvider extends HTMLElement {
565565 * @param html Raw HTML provided by internal calls (never untrusted user input).
566566 * @returns A safe DocumentFragment ready to be inserted.
567567 */
568+ const fragment = document . createDocumentFragment ( ) ;
569+ if ( ! html . includes ( '<' ) ) {
570+ // fast path plain text
571+ fragment . appendChild ( document . createTextNode ( html ) ) ;
572+ return fragment ;
573+ }
568574 const template = document . createElement ( 'template' ) ;
569575 template . innerHTML = html ;
570- const allowedSpanClass = 'heroicons-outline--exclamation' ;
571- const walker = ( node : Node ) : Node | null => {
572- if ( node . nodeType === Node . ELEMENT_NODE ) {
573- const el = node as HTMLElement ;
574- if ( el . tagName === 'BR' ) return el ; // keep line breaks
575- else if ( el . tagName === 'EM' || el . tagName === 'STRONG' || el . tagName === 'B' || el . tagName === 'I' ) return el ; // keep emphasis
576- else if (
577- el . tagName === 'SPAN' &&
578- el . classList . length === 1 &&
579- el . classList . contains ( allowedSpanClass )
580- ) {
581- // strip any other attributes for safety
582- Array . from ( el . attributes ) . forEach ( ( attr ) => {
583- if ( attr . name !== 'class' ) el . removeAttribute ( attr . name ) ;
584- } ) ;
585- return el ;
576+ const allowedSimpleTags = new Set ( [ 'B' , 'I' ] ) ;
577+
578+ const sanitizeChildren = ( srcParent : Node , destParent : Node ) => {
579+ srcParent . childNodes . forEach ( ( child ) => walk ( child , destParent ) ) ;
580+ } ;
581+
582+ const walk = ( node : Node , parent : Node ) => {
583+ switch ( node . nodeType ) {
584+ case Node . TEXT_NODE : {
585+ if ( node . textContent ) parent . appendChild ( document . createTextNode ( node . textContent ) ) ;
586+ return ;
586587 }
587- // Replace disallowed element with a text node of its textContent
588- return document . createTextNode ( el . textContent ?? '' ) ;
588+ case Node . ELEMENT_NODE : {
589+ const el = node as HTMLElement ;
590+ const tag = el . tagName ;
591+ // <br> allowed (no attributes kept)
592+ if ( tag === 'BR' ) {
593+ parent . appendChild ( document . createElement ( 'br' ) ) ;
594+ return ;
595+ }
596+ // Emphasis tags: recreate element, drop all attributes, recurse into children
597+ if ( allowedSimpleTags . has ( tag ) ) {
598+ const neo = document . createElement ( tag . toLowerCase ( ) ) ;
599+ parent . appendChild ( neo ) ;
600+ sanitizeChildren ( el , neo ) ;
601+ return ;
602+ }
603+ // Span: recreate and transfer entire class list (strip other attributes)
604+ if ( tag === 'SPAN' ) {
605+ const span = document . createElement ( 'span' ) ;
606+ span . className = el . className ; // copy all classes
607+ parent . appendChild ( span ) ;
608+ sanitizeChildren ( el , span ) ;
609+ return ;
610+ }
611+ // Disallowed wrapper: do not keep the element, but recursively sanitize its children
612+ sanitizeChildren ( el , parent ) ;
613+ return ;
614+ }
615+ default : // ignore comments / others
616+ return ;
589617 }
590- if ( node . nodeType === Node . TEXT_NODE ) return node ;
591- return null ; // drop comments or others
592618 } ;
593- const outFrag = document . createDocumentFragment ( ) ;
594- Array . from ( template . content . childNodes ) . forEach ( ( child ) => {
595- const safe = walker ( child ) ;
596- if ( safe ) outFrag . appendChild ( safe ) ;
597- } ) ;
598- return outFrag ;
619+
620+ template . content . childNodes . forEach ( ( n ) => walk ( n , fragment ) ) ;
621+ return fragment ;
599622 }
600623
601624 /**
0 commit comments