'use strict' ; import { Document } from "./types" ; import * as Syntax from './syntax' ; const emptyArray = Object . freeze ( [ ] ) ; const emptyObject = Object . freeze ( Object . create ( null ) ) ; const namedEntities = Object . freeze ( { '&amp;' : '&' , '&apos;' : "'" , '&gt;' : '>' , '&lt;' : '<' , '&quot;' : '"' } ) ; const NODE_TYPE_CDATA = 'cdata' ; const NODE_TYPE_COMMENT = 'comment' ; const NODE_TYPE_DOCUMENT = 'document' ; const NODE_TYPE_ELEMENT = 'element' ; const NODE_TYPE_TEXT = 'text' ; export class ParseXml { parse ( xml , options = emptyObject ) { if ( xml [ 0 ] === '\uFEFF' ) { xml = xml . slice ( 1 ) ; } xml = xml . replace ( /\r\n?/g , '\n' ) ; let doc = { type : NODE_TYPE_DOCUMENT , children : [ ] , parent : null } ; doc . toJSON = this . nodeToJson . bind ( doc ) ; let state = { length : xml . length , options , parent : doc , pos : 0 , prevPos : 0 , slice : null , xml } ; this . consumeProlog ( state ) ; if ( ! this . consumeElement ( state ) ) { this . error ( state , 'Root element is missing or invalid' ) ; } while ( this . consumeMisc ( state ) ) { } if ( ! this . isEof ( state ) ) { this . error ( state , `Extra content at the end of the document` ) ; } return doc ; } addNode ( state , node ) { node . parent = state . parent ; node . toJSON = this . nodeToJson . bind ( node ) ; state . parent . children . push ( node ) ; } addText ( state , text ) { let { children } = state . parent ; let prevNode = children [ children . length - 1 ] ; if ( prevNode !== void 0 && prevNode . type === NODE_TYPE_TEXT ) { prevNode . text += text ; } else { this . addNode ( state , { type : NODE_TYPE_TEXT , text } ) ; } } consumeCDSect ( state ) { let [ match , text ] = scan ( state , Syntax . Anchored . CDSect ) ; if ( match === void 0 ) { return false ; } if ( state . options . preserveCdata ) { this . addNode ( state , { type : NODE_TYPE_CDATA , text } ) ; } else { this . addText ( state , text ) ; } return true ; } consumeCharData ( state ) { let [ text ] = scan ( state , Syntax . Anchored . CharData ) ; if ( text === void 0 ) { return false ; } let cdataCloseIndex = text . indexOf ( ']]>' ) ; if ( cdataCloseIndex !== - 1 ) { state . pos = state . prevPos + cdataCloseIndex ; this . error ( state , 'Element content may not contain the CDATA section close delimiter `]]>`' ) ; } if ( ! Syntax . CharOnly . test ( text ) ) { state . pos = state . prevPos + text . search ( new RegExp ( `(?!${ Syntax . Char . source })` ) ) ; this . error ( state , 'Element content contains an invalid character' ) ; } this . addText ( state , text ) ; return true ; } consumeComment ( state ) { let [ , content ] = scan ( state , Syntax . Anchored . Comment ) ; if ( content === void 0 ) { return false ; } if ( state . options . preserveComments ) { this . addNode ( state , { type : NODE_TYPE_COMMENT , content : content . trim ( ) } ) ; } return true ; } consumeDoctypeDecl ( state ) { return scan ( state , Syntax . Anchored . doctypedecl ) . length > 0 ; } consumeElement ( state ) { let [ tag , name , attrs ] = scan ( state , Syntax . Anchored . EmptyElemTag ) ; let isEmpty = tag !== void 0 ; if ( ! isEmpty ) { [ tag , name , attrs ] = scan ( state , Syntax . Anchored . STag ) ; if ( tag === void 0 ) { return false ; } } let { parent } = state ; let parsedAttrs = this . parseAttrs ( state , attrs ) ; let node = { type : NODE_TYPE_ELEMENT , name , attributes : parsedAttrs , children : [ ] } ; let xmlSpace = parsedAttrs [ 'xml:space' ] ; if ( xmlSpace === 'preserve' || ( xmlSpace !== 'default' && parent . preserveWhitespace ) ) { node . preserveWhitespace = true ; } if ( ! isEmpty ) { state . parent = node ; this . consumeCharData ( state ) ; while ( this . consumeElement ( state ) || this . consumeReference ( state ) || this . consumeCDSect ( state ) || this . consumePI ( state ) || this . consumeComment ( state ) ) { this . consumeCharData ( state ) ; } let [ , endName ] = scan ( state , Syntax . Anchored . ETag ) ; if ( endName !== name ) { state . pos = state . prevPos ; this . error ( state , `Missing end tag for element ${ name }` ) ; } state . parent = parent ; } this . addNode ( state , node ) ; return true ; } consumeMisc ( state ) { return this . consumeComment ( state ) || this . consumePI ( state ) || this . consumeWhitespace ( state ) ; } consumePI ( state ) { let [ match , target ] = scan ( state , Syntax . Anchored . PI ) ; if ( match === void 0 ) { return false ; } if ( target . toLowerCase ( ) === 'xml' ) { state . pos = state . prevPos ; this . error ( state , 'XML declaration is only allowed at the start of the document' ) ; } return true ; } consumeProlog ( state ) { let { pos } = state ; scan ( state , Syntax . Anchored . XMLDecl ) ; while ( this . consumeMisc ( state ) ) { } if ( this . consumeDoctypeDecl ( state ) ) { while ( this . consumeMisc ( state ) ) { } } return state . pos > pos ; } consumeReference ( state ) { let [ ref ] = scan ( state , Syntax . Anchored . Reference ) ; if ( ref === void 0 ) { return false ; } this . addText ( state , this . replaceReference ( ref , state ) ) ; return true ; } consumeWhitespace ( state ) { return scan ( state , Syntax . Anchored . S ) . length > 0 ; } error ( state , message ) { let { pos , xml } = state ; let column = 1 ; let excerpt = '' ; let line = 1 ; for ( let i = 0 ; i < pos ; ++ i ) { let char = xml [ i ] ; if ( char === '\n' ) { column = 1 ; excerpt = '' ; line += 1 ; } else { column += 1 ; excerpt += char ; } } let eol = xml . indexOf ( '\n' , pos ) ; excerpt += eol === - 1 ? xml . slice ( pos ) : xml . slice ( pos , eol ) ; let excerptStart = 0 ; if ( excerpt . length > 50 ) { if ( column < 40 ) { excerpt = excerpt . slice ( 0 , 50 ) ; } else { excerptStart = column - 20 ; excerpt = excerpt . slice ( excerptStart , column + 30 ) ; } } let err = new Error ( `${ message } (line ${ line }, column ${ column })\n` + `  ${ excerpt }\n` + ' ' . repeat ( column - excerptStart + 1 ) + '^\n' ) ; err . column = column ; err . excerpt = excerpt ; err . line = line ; err . pos = pos ; throw err ; } isEof ( state ) { return state . pos >= state . length - 1 ; } nodeToJson ( ) { let json = Object . assign ( Object . create ( null ) , this ) ; delete json . parent ; return json ; } normalizeAttrValue ( state , value ) { return value . replace ( Syntax . Global . Reference , ( ref ) => this . replaceReference ( ref , state ) ) . replace ( Syntax . Global . S , ' ' ) . trim ( ) ; } parseAttrs ( state , attrs ) { let parsedAttrs = Object . create ( null ) ; if ( ! attrs ) { return parsedAttrs ; } let attrPairs = attrs . match ( Syntax . Global . Attribute ) . sort ( ) ; for ( let i = 0 , len = attrPairs . length ; i < len ; ++ i ) { let attrPair = attrPairs [ i ] ; let name = attrPair . trim ( ) ; let value = '' ; let eqMatch = attrPair . match ( Syntax . Eq ) ; if ( eqMatch && eqMatch . index > - 1 ) { name = attrPair . slice ( 0 , eqMatch . index ) ; value = attrPair . slice ( eqMatch . index + eqMatch [ 0 ] . length ) ; } if ( name in parsedAttrs ) { state . pos = state . prevPos ; this . error ( state , `Attribute \`${ name }\` redefined` ) ; } value = this . normalizeAttrValue ( state , value . slice ( 1 , - 1 ) ) ; if ( name === 'xml:space' ) { if ( value !== 'default' && value !== 'preserve' ) { state . pos = state . prevPos ; this . error ( state , `Value of the \`xml:space\` attribute must be "default" or "preserve"` ) ; } } parsedAttrs [ name ] = value ; } return parsedAttrs ; } replaceReference ( ref , state ) { if ( ref [ 1 ] === '#' ) { let codePoint ; if ( ref [ 2 ] === 'x' ) { codePoint = parseInt ( ref . slice ( 3 , - 1 ) , 16 ) ; } else { codePoint = parseInt ( ref . slice ( 2 , - 1 ) , 10 ) ; } if ( isNaN ( codePoint ) ) { state . pos = state . prevPos ; this . error ( state , `Invalid character entity \`${ ref }\`` ) ; } let char = String . fromCodePoint ( codePoint ) ; if ( ! Syntax . Char . test ( char ) ) { state . pos = state . prevPos ; this . error ( state , `Invalid character entity \`${ ref }\`` ) ; } return char ; } let value = namedEntities [ ref ] ; if ( value !== void 0 ) { return value ; } if ( state . options . resolveUndefinedEntity ) { let resolvedValue = state . options . resolveUndefinedEntity ( ref ) ; if ( resolvedValue !== null && resolvedValue !== void 0 ) { return resolvedValue ; } } if ( state . options . ignoreUndefinedEntities ) { return ref ; } state . pos = state . prevPos ; this . error ( state , `Named entity isn't defined: \`${ ref }\`` ) ; } } export function scan ( state , regex ) { let { pos , slice , xml } = state ; if ( ! slice ) { if ( pos > 0 ) { slice = xml . slice ( pos ) ; state . slice = slice ; } else { slice = xml ; } } let matches = slice . match ( regex ) ; if ( ! matches ) { return emptyArray ; } state . prevPos = state . pos ; state . pos += matches [ 0 ] . length ; state . slice = null ; return matches ; }