import * as stream from "stream" ; import * as util from "util" ; import { parseJson5 } from "@adpt/utils" ; import * as ld from "lodash" ; import * as sb from "stream-buffers" ; import * as xml2js from "xml2js" ; import { AnyProps , DOMNode , DOMObject , } from "./dom" ; const rootName = "Adapt" ; async function parseXML ( xmlStr ) { return new Promise < any > ( ( res , rej ) => { xml2js . parseString ( xmlStr , { explicitRoot : false , explicitChildren : true , preserveChildrenOrder : true , charsAsChildren : true , xmlns : true , } , ( err , result ) => { if ( err != null ) rej ( err ) ; else res ( result ) ; } ) ; } ) ; } export type XMLNode = GenericXMLNode | XMLPropsNode | XMLPropNode | XMLTextNode ; export interface GenericXMLNode { "#name" : string ; "$" ? : Attrs ; "$$" ? : XMLNode [ ] ; "$ns" ? : XMLNS ; } export interface XMLPropsNode extends GenericXMLNode { "#name" : "__props__" ; "$$" ? : XMLPropNode [ ] ; } export interface XMLPropNode extends GenericXMLNode { "#name" : "prop" ; "$$" : XMLTextNode [ ] ; } export interface XMLLifecycleNode extends GenericXMLNode { "#name" : "__lifecycle__" ; "$$" ? : XMLLifecycleFieldNode [ ] ; } export interface XMLLifecycleFieldNode extends GenericXMLNode { "#name" : "field" ; "$$" : XMLTextNode [ ] ; } export interface XMLTextNode extends GenericXMLNode { "#name" : "__text__" ; "_" : string ; } export interface XMLNS { local : string ; uri : string ; } export interface Attr { local : string ; name : string ; prefix : string ; uri : string ; value : any ; } export interface Attrs { [ key ] : Attr ; } function nameOf ( xmlNode ) { if ( xmlNode . $ns ) return xmlNode . $ns . local ; return xmlNode [ "#name" ] ; } function uriOf ( xmlNode ) { if ( xmlNode . $ns ) return xmlNode . $ns . uri ; return "" ; } export function handleShortProp ( val ) { if ( /^\d/ . test ( val ) ) { const ret = Number ( val ) ; if ( isNaN ( ret ) ) { throw new Error ( "invalid short prop value \"" + val + "\"" ) ; } return ret ; } else { return val ; } } function isTextNode ( xmlNode ) { return xmlNode [ "#name" ] === "__text__" ; } function extractSoleText ( xmlNode ) { if ( xmlNode . $$ == null ) { throw new Error ( "body missing" ) ; } if ( ! ld . isArray ( xmlNode . $$ ) ) { throw new Error ( "Internal Error" ) ; } if ( xmlNode . $$ . length > 1 ) { throw new Error ( "too many children" ) ; } if ( xmlNode . $$ . length === 0 ) { throw new Error ( "node has no body" ) ; } const child = xmlNode . $$ [ 0 ] ; if ( isTextNode ( child ) ) { return child . _ ; } else { throw new Error ( "no text node found" ) ; } } function extractDOMObject ( xmlNode ) { if ( xmlNode . $$ == null ) new Error ( "body missing" ) ; if ( ! ld . isArray ( xmlNode . $$ ) ) throw new Error ( "Internal Error" ) ; if ( xmlNode . $$ . length > 1 ) throw new Error ( "too many children" ) ; if ( xmlNode . $$ . length === 0 ) throw new Error ( "node has no body" ) ; const child = xmlNode . $$ [ 0 ] ; const uri = uriOf ( child ) ; if ( ! uri . startsWith ( "urn:" ) ) throw new Error ( "child has bad xmlns" ) ; const json = extractSoleText ( child ) ; return new DOMObject ( uri , parseJson5 ( json ) ) ; } function handleJSON ( xmlNode ) { if ( nameOf ( xmlNode ) !== "json" ) { throw new Error ( "Internal Error: Request to handle json node for non-json: " + util . inspect ( xmlNode ) ) ; } let txt = "" ; try { txt = extractSoleText ( xmlNode ) ; return parseJson5 ( txt ) ; } catch ( e ) { throw new Error ( "malformed json node body: " + e . message ) ; } } function getPropsNode ( xmlNode ) { const anode = xmlNode as any ; if ( anode . __props__ != null ) { return anode . __props__ ; } return [ ] ; } function extractProp ( prop ) { try { return parseJson5 ( extractSoleText ( prop ) ) ; } catch ( err ) { } return extractDOMObject ( prop ) ; } function computeProps ( xmlNodeIn ) { const ret = { } ; const xmlNode = xmlNodeIn as GenericXMLNode ; if ( xmlNode . $ != null ) { for ( const prop in xmlNode . $ ) { if ( prop === "xmlns" || prop . startsWith ( "xmlns:" ) ) continue ; if ( ! Object . prototype . hasOwnProperty . apply ( xmlNode . $ , [ prop ] ) ) continue ; ret [ prop ] = handleShortProp ( xmlNode . $ [ prop ] . value ) ; } } const propsNodes = getPropsNode ( xmlNode ) ; if ( propsNodes . length > 1 ) { throw new Error ( "malformed node, multiple __props__ children" ) ; } if ( propsNodes . length < 1 ) { return ret ; } const propsNode = propsNodes [ 0 ] ; if ( propsNode . $$ != null ) { for ( const prop of propsNode . $$ ) { if ( prop . $ != null ) { const name = prop . $ . name . value ; if ( ret [ name ] != null ) throw new Error ( "duplicate prop: " + name ) ; try { ret [ name ] = extractProp ( prop ) ; } catch ( e ) { throw new Error ( "malformed prop node for " + name + ": " + e . message ) ; } } } } return ret ; } export interface LifecycleInfo { stateNamespace : string [ ] ; keyPath : string [ ] ; path : string ; } function getLifecycleNode ( xmlNode ) { const anode = xmlNode as any ; if ( anode . __lifecycle__ != null ) { return anode . __lifecycle__ ; } return [ ] ; } function extractStringArray ( xmlNode ) { const dataJson = extractSoleText ( xmlNode ) ; if ( dataJson == null ) throw new Error ( `No text data in node: ${ util . inspect ( xmlNode ) }` ) ; const data = parseJson5 ( dataJson ) ; if ( ! util . isArray ( data ) ) throw new Error ( `text data is not an array, expecting string[]: ${ util . inspect ( xmlNode ) }` ) ; const notString = data . find ( ( v ) => ! util . isString ( v ) ) ; if ( notString !== undefined ) { throw new Error ( `${ util . inspect ( notString ) } is not string, expecting string[]:` + util . inspect ( xmlNode ) ) ; } return data as string [ ] ; } function extractString ( xmlNode ) { const dataJson = extractSoleText ( xmlNode ) ; if ( dataJson == null ) throw new Error ( `No text data in node: ${ util . inspect ( xmlNode ) }` ) ; const data = parseJson5 ( dataJson ) ; if ( ! util . isString ( data ) ) throw new Error ( `text data is not a string: ${ util . inspect ( xmlNode ) }` ) ; return data ; } function augmentError < T > ( msg , f ) { try { return f ( ) ; } catch ( e ) { throw new Error ( msg + ":" + e . message ) ; } } function computeLifecycleInfo ( xmlNode ) { const lifecycleNodes = getLifecycleNode ( xmlNode ) ; if ( lifecycleNodes . length > 1 ) { throw new Error ( `malformed ndoe, multiple __lifecycle__ children: ${ util . inspect ( xmlNode ) }` ) ; } if ( lifecycleNodes . length < 1 ) return ; const lifecycleNode = lifecycleNodes [ 0 ] ; if ( lifecycleNode . $$ == null ) return ; const ret = { } ; for ( const field of lifecycleNode . $$ ) { if ( field . $ == null ) { throw new Error ( `malformed node, lifecycle field with no name ${ util . inspect ( lifecycleNode ) }` ) ; } const fieldName = field . $ . name . value ; switch ( fieldName ) { case "stateNamespace" : ret . stateNamespace = augmentError ( "extracting stateNamespace" , ( ) => extractStringArray ( field ) ) ; break ; case "keyPath" : ret . keyPath = augmentError ( "extracting keyPath" , ( ) => extractStringArray ( field ) ) ; break ; case "path" : ret . path = augmentError ( "extracting path" , ( ) => extractString ( field ) ) ; break ; default : throw new Error ( `malformed node, ` + `uknown lifecycle field name "${ fieldName }": ${ util . inspect ( lifecycleNode ) }` ) ; } } const nodeInfo = util . inspect ( xmlNode ) ; if ( ret . stateNamespace === undefined ) throw new Error ( `no stateNamespace in lifecycle data: ${ nodeInfo }` ) ; if ( ret . keyPath === undefined ) throw new Error ( `no keyPath in lifecycle data: ${ nodeInfo }` ) ; if ( ret . path === undefined ) throw new Error ( `no path in lifecycle data: ${ nodeInfo }` ) ; return ret as LifecycleInfo ; } function buildFromXMLNode ( xmlNode ) { const name = nameOf ( xmlNode ) ; const uri = uriOf ( xmlNode ) ; const props = computeProps ( xmlNode ) ; const lifecycle = computeLifecycleInfo ( xmlNode ) ; const children = [ ] ; if ( xmlNode . $$ != null ) { if ( ld . isArray ( xmlNode . $$ ) ) { for ( const child of xmlNode . $$ ) { if ( ld . isObject ( child ) ) { if ( nameOf ( child ) === "__props__" ) { continue ; } else if ( nameOf ( child ) === "__lifecycle__" ) { continue ; } else if ( nameOf ( child ) === "json" ) { children . push ( handleJSON ( child ) ) ; } else { children . push ( buildFromXMLNode ( child ) ) ; } } else { children . push ( child ) ; } } } else { throw new Error ( "Internal parse error" ) ; } } return new DOMNode ( name , props , lifecycle , uri , children ) ; } export async function domFromXMLObj ( xmlObj ) { if ( nameOf ( xmlObj ) !== rootName ) { throw new Error ( "Unknown root node: " + nameOf ( xmlObj ) ) ; } const topLevel = xmlObj . $$ ; if ( topLevel == null ) { return null ; } if ( ! ld . isArray ( topLevel ) ) { throw new Error ( "Internal error parsing DOM" ) ; } if ( topLevel . length === 0 ) { return null ; } if ( topLevel . length > 1 ) { throw new Error ( "Too many elements at root, must have single DOM Node" ) ; } return buildFromXMLNode ( topLevel [ 0 ] ) ; } export async function domFromString ( xmlStr ) { const xmlObj = await parseXML ( xmlStr ) ; if ( xmlObj == null ) throw new Error ( `Invalid empty XML` ) ; return domFromXMLObj ( xmlObj ) ; } async function stringFromStream ( ins ) { return new Promise < string > ( ( res , rej ) => { const buf = new sb . WritableStreamBuffer ( ) ; ins . pipe ( buf ) ; buf . on ( "close" , ( ) => res ( buf . getContentsAsString ( ) || undefined ) ) ; buf . on ( "error" , ( e ) => rej ( e ) ) ; } ) ; } export async function domFromStream ( ins ) { const xmlStr = await stringFromStream ( ins ) ; return domFromString ( xmlStr ) ; }