import * as ld from "lodash" ; import * as util from "util" ; import * as xmlbuilder from "xmlbuilder" ; import { stringifyJson5 } from "@adpt/utils" ; import indentString from "indent-string" ; import { InternalError } from "./error" ; import { AdaptElement , AdaptElementImpl , AdaptElementOrNull , AdaptMountedElement , AnyProps , childrenToArray , isComponentElement , isElement , isElementImpl , isMountedElement } from "./jsx" ; import { findMummyUrn } from "./reanimate" ; export interface SerializeOptions { reanimateable : boolean ; props : "all" | "none" | string [ ] ; } const defaultSerializeOptions = { reanimateable : false , props : "all" , } ; interface PreparedProps { [ key ] : string ; } interface AnyObj { [ key ] : any ; } const indentSpaces = 2 ; function serializeAny ( val , reanimateable ) { return stringifyJson5 ( val , { quote : `"` , replacer : serializeSpecials ( reanimateable ) , space : indentSpaces , useUndefined : true , } ) ; } function serializeSpecials ( reanimateable ) { return function ( this , key , value ) { return value ; } ; } function serializedShortPropIsString ( propVal ) { return ! ( /^\d/ . test ( propVal ) ) ; } function canBeShort ( propName , propVal ) { if ( propName === "xmlns" || propName . startsWith ( "xmlns:" ) ) return false ; if ( ld . isNumber ( propVal ) ) return true ; if ( ld . isString ( propVal ) ) { const json = JSON . stringify ( propVal ) ; return ( json . length < 10 ) && serializedShortPropIsString ( json . slice ( 1 , - 1 ) ) ; } return false ; } function serializeShortPropVal ( propVal ) { const long = serializeLongPropVal ( propVal , false , false ) ; if ( ld . isString ( long ) && ld . isString ( propVal ) ) { return long . slice ( 1 , - 1 ) ; } return long ; } function serializeLongPropVal ( propVal , pretty = true , reanimateable = true , indent = 0 ) { const json = stringifyJson5 ( propVal , { quote : `"` , replacer : serializeSpecials ( reanimateable ) , space : pretty ? indentSpaces : undefined , useUndefined : true , } ) ; if ( json == null ) return propVal . toString ( ) ; return indentString ( json , ( indent + 2 ) * indentSpaces ) . trimLeft ( ) ; } function collectProps ( elem , options , indent ) { const props = elem . props ; const shortProps = { } ; let longProps = null ; let propNames ; switch ( options . props ) { case "all" : propNames = Object . keys ( props ) . sort ( ) ; break ; case "none" : propNames = [ ] ; break ; default : if ( ! Array . isArray ( options . props ) ) { throw new InternalError ( `Invalid value '${ options . props }' for options.props` ) ; } propNames = options . props ; } for ( const propName of propNames ) { if ( propName === "children" || propName === "handle" ) continue ; const prop = props [ propName ] ; if ( canBeShort ( propName , prop ) ) { shortProps [ propName ] = serializeShortPropVal ( prop ) ; } else { if ( longProps == null ) { longProps = { } ; } longProps [ propName ] = serializeLongPropVal ( prop , true , options . reanimateable , indent ) ; } } return { shortProps , longProps } ; } function addPropsNode ( node , props ) { const propsNode = node . ele ( "__props__" , { } ) ; for ( const propName in props ) { if ( ! props . hasOwnProperty ( propName ) ) continue ; const prop = props [ propName ] ; propsNode . ele ( "prop" , { name : propName } , prop ) ; } } function serializeChildren ( context , node , children , options ) { context = { ... context , indent : context . indent + 1 , } ; for ( const child of children ) { switch ( true ) { case isElement ( child ) : serializeElement ( context , node , child , options ) ; break ; default : const serChild = serializeAny ( child , options . reanimateable ) ; if ( serChild == null ) { node . ele ( "typescript" , { } ) . cdata ( child . toString ( ) ) ; } else { node . ele ( "json" , { } , serChild ) ; } } } } function serializeChildrenFromElem ( context , node , elem , options ) { const children = childrenToArray ( elem . props . children ) ; serializeChildren ( context , node , children , options ) ; } function getUrn ( elem ) { if ( ! isComponentElement ( elem ) ) { throw new Error ( `Unable to create reanimateable representation of ` + `'${ elem . componentName }' because it doesn't extend ` + `Adapt.Component` ) ; } try { return findMummyUrn ( elem . componentType ) ; } catch { } try { new elem . componentType ( { } ) ; } catch { } return findMummyUrn ( elem . componentType ) ; } function serializeBuildData ( context , parent , elem , options ) { const bdNode = parent . ele ( "buildData" , { } ) ; const succ = elem . buildData . successor ; const origChildren = elem . buildData . origChildren ; if ( succ !== undefined ) { const isNull = succ === null ; const succNode = bdNode . ele ( "successor" , { isNull } ) ; if ( succ !== null ) serializeElement ( context , succNode , succ , options ) ; } if ( origChildren !== undefined ) { const origChildrenNode = bdNode . ele ( "origChildren" , { } ) ; serializeChildren ( context , origChildrenNode , origChildren , options ) ; } } function addLifecycleNode ( context , parent , elem , options ) { if ( ! isElementImpl ( elem ) ) throw new Error ( `Element not an ElementImpl: ${ util . inspect ( elem ) }` ) ; const lcNode = parent . ele ( "__lifecycle__" , { } ) ; lcNode . ele ( "field" , { name : "stateNamespace" } , JSON . stringify ( elem . stateNamespace ) ) ; lcNode . ele ( "field" , { name : "keyPath" } , JSON . stringify ( elem . keyPath ) ) ; lcNode . ele ( "field" , { name : "path" } , JSON . stringify ( elem . path ) ) ; if ( "Enable this when we figure out how to serialize and reanimate SFCs" . length === 0 ) { serializeBuildData ( context , lcNode , elem , options ) ; } } function serializeElement ( context , parent , elem , options ) { if ( context . serializedElements . has ( elem ) && isMountedElement ( elem ) ) { parent . ele ( "__elementRef__" , { ref : elem . id } ) ; return ; } const { shortProps , longProps } = collectProps ( elem , options , context . indent ) ; let node ; if ( options . reanimateable ) { const urn = getUrn ( elem ) ; node = parent . ele ( elem . componentName , { ... shortProps , xmlns : urn } ) ; } else { node = parent . ele ( elem . componentName , shortProps ) ; } if ( longProps != null ) { addPropsNode ( node , longProps ) ; } serializeChildrenFromElem ( context , node , elem , options ) ; if ( isMountedElement ( elem ) && options . reanimateable ) { context . work . push ( ( ) => addLifecycleNode ( context , node , elem , options ) ) ; } } interface SerializationContext { indent : number ; serializedElements : Set < AdaptElement > ; work : ( ( ) => void ) [ ] ; } export function serializeDom ( root , options = { } ) { const opts = { ... defaultSerializeOptions , ... options } ; if ( opts . reanimateable && opts . props !== "all" ) { throw new Error ( `Invalid options for serializeDom: props must be "all" when reanimateable is true` ) ; } const context = { indent : 1 , serializedElements : new Set < AdaptElement > ( ) , work : [ ] } ; const doc = xmlbuilder . create ( "Adapt" , { headless : true } ) ; if ( root != null ) serializeElement ( context , doc , root , opts ) ; while ( context . work . length > 0 ) { const toDo = context . work . shift ( ) ; if ( toDo ) toDo ( ) ; } return doc . end ( { pretty : true } ) + "\n" ; }