import * as ElementType from "domelementtype" ; import type { Node , NodeWithChildren , Element , DataNode } from "domhandler" ; import { encodeXML } from "entities" ; see https import { elementNames , attributeNames } from "./foreignNames" ; export interface DomSerializerOptions { default example example emptyAttrs ? : boolean ; default example example selfClosingTags ? : boolean ; default xmlMode ? : boolean | "foreign" ; default decodeEntities ? : boolean ; } const unencodedElements = new Set ( [ "style" , "script" , "xmp" , "iframe" , "noembed" , "noframes" , "plaintext" , "noscript" , ] ) ; /**
 * Format attributes
 */ function formatAttributes ( attributes , opts ) { if ( ! attributes ) return ; return Object . keys ( attributes ) . map ( ( key ) => { const value = attributes [ key ] ?? "" ; if ( opts . xmlMode === "foreign" ) { key = attributeNames . get ( key ) ?? key ; } if ( ! opts . emptyAttrs && ! opts . xmlMode && value === "" ) { return key ; } return `${ key }="${ opts . decodeEntities !== false ? encodeXML ( value ) : value . replace ( /"/g , "&quot;" ) }"` ; } ) . join ( " " ) ; } /**
 * Self-enclosing tags
 */ const singleTag = new Set ( [ "area" , "base" , "basefont" , "br" , "col" , "command" , "embed" , "frame" , "hr" , "img" , "input" , "isindex" , "keygen" , "link" , "meta" , "param" , "source" , "track" , "wbr" , ] ) ; param node param options export default function render ( node , options = { } ) { const nodes = "length" in node ? node : [ node ] ; let output = "" ; for ( let i = 0 ; i < nodes . length ; i ++ ) { output += renderNode ( nodes [ i ] , options ) ; } return output ; } function renderNode ( node , options ) { switch ( node . type ) { case ElementType . Root : return render ( ( node as NodeWithChildren ) . children , options ) ; case ElementType . Directive : case ElementType . Doctype : return renderDirective ( node as DataNode ) ; case ElementType . Comment : return renderComment ( node as DataNode ) ; case ElementType . CDATA : return renderCdata ( node as NodeWithChildren ) ; case ElementType . Script : case ElementType . Style : case ElementType . Tag : return renderTag ( node as Element , options ) ; case ElementType . Text : return renderText ( node as DataNode , options ) ; } } const foreignModeIntegrationPoints = new Set ( [ "mi" , "mo" , "mn" , "ms" , "mtext" , "annotation-xml" , "foreignObject" , "desc" , "title" , ] ) ; const foreignElements = new Set ( [ "svg" , "math" ] ) ; function renderTag ( elem , opts ) { if ( opts . xmlMode === "foreign" ) { elem . name = elementNames . get ( elem . name ) ?? elem . name ; if ( elem . parent && foreignModeIntegrationPoints . has ( ( elem . parent as Element ) . name ) ) { opts = { ... opts , xmlMode : false } ; } } if ( ! opts . xmlMode && foreignElements . has ( elem . name ) ) { opts = { ... opts , xmlMode : "foreign" } ; } let tag = `<${ elem . name }` ; const attribs = formatAttributes ( elem . attribs , opts ) ; if ( attribs ) { tag += ` ${ attribs }` ; } if ( elem . children . length === 0 && ( opts . xmlMode ? opts . selfClosingTags !== false : opts . selfClosingTags && singleTag . has ( elem . name ) ) ) { if ( ! opts . xmlMode ) tag += " " ; tag += "/>" ; } else { tag += ">" ; if ( elem . children . length > 0 ) { tag += render ( elem . children , opts ) ; } if ( opts . xmlMode || ! singleTag . has ( elem . name ) ) { tag += `</${ elem . name }>` ; } } return tag ; } function renderDirective ( elem ) { return `<${ elem . data }>` ; } function renderText ( elem , opts ) { let data = elem . data || "" ; if ( opts . decodeEntities !== false && ! ( ! opts . xmlMode && elem . parent && unencodedElements . has ( ( elem . parent as Element ) . name ) ) ) { data = encodeXML ( data ) ; } return data ; } function renderCdata ( elem ) { return `<![CDATA[${ ( elem . children [ 0 ] as DataNode ) . data }]]>` ; } function renderComment ( elem ) { return `<!--${ elem . data }-->` ; }