import { REGEX } from '../Regex' ; import { Block , BlockList } from './block/BlockList' ; import { Logger , LogMessage , LogMessageMap } from './Logger' ; import { BlockRenderer } from './render/BlockRenderer' ; import { Renderer } from './render/Renderer' ; import { XMLRenderer } from './render/XMLRenderer' ; export class QDLParser { private renderer : BlockRenderer ; private result : any ; private log : Logger | null ; private blockList : BlockList ; private reverseLookup : { [ n ] : number ; } ; constructor ( renderer ) { this . renderer = new BlockRenderer ( renderer ) ; this . result = null ; } public render ( blockList ) { const startTime = Date . now ( ) ; this . log = blockList . logger || new Logger ( ) ; if ( ! blockList || blockList . length === 0 ) { this . result = this . renderer . finalize ( [ ] , this . log ) ; return ; } this . blockList = blockList ; const groups = this . getBlockGroups ( ) ; this . log . dbg ( 'Block groups:' ) ; this . log . dbg ( JSON . stringify ( groups ) ) ; const indents = Object . keys ( groups ) . sort ( ( a , b ) => ( parseInt ( a , 10 ) - parseInt ( b , 10 ) ) ) ; for ( let i = indents . length - 1 ; i >= 0 ; i -- ) { const indentGroups = groups [ indents [ i ] ] ; for ( const group of indentGroups ) { if ( group . length === 0 ) { continue ; } this . log . extend ( this . renderSegment ( indents [ i + 1 ] , group [ 0 ] , group [ group . length - 1 ] ) ) ; } } const zeroIndentBlockRoots = [ ] ; const minIndent = '0' ; for ( const indent of groups [ minIndent ] ) { zeroIndentBlockRoots . push ( this . blockList . at ( indent [ 0 ] ) ) ; } this . result = this . renderer . finalize ( zeroIndentBlockRoots , this . log ) ; this . renderer . validate ( this . blockList . at ( 0 ) . render , this . log ) ; this . reverseLookup = { } ; for ( const indent of indents ) { const indentGroups = groups [ indent ] ; for ( const group of indentGroups ) { for ( const key of group ) { this . reverseLookup [ key ] = group [ 0 ] ; } } } this . log . dbg ( 'XML rendered in ' + ( Date . now ( ) - startTime ) + 'ms' ) ; } public getResult ( ) { return this . result ; } public getResultAt ( line ) { if ( ! this . blockList ) { return null ; } for ( let i = 0 ; i < this . blockList . length ; i ++ ) { const block = this . blockList . at ( i ) ; if ( block . startLine <= line && block . startLine + block . lines . length > line ) { return this . blockList . at ( this . reverseLookup [ i ] ) . render ; } } return null ; } public getMeta ( ) { if ( ! this . blockList ) { return { } ; } return this . renderer . toMeta ( this . blockList . at ( 0 ) , undefined ) ; } private hasHeader ( block ) { return ( Boolean ( block ) && Boolean ( block . lines . length ) && Boolean ( block . lines [ 0 ] . length ) && ( block . lines [ 0 ] [ 0 ] === '_' || block . lines [ 0 ] [ 0 ] === '#' || REGEX . TRIGGER . test ( block . lines [ 0 ] ) ) ) ; } private getBlockGroups ( ) { const groups = { } ; for ( let i = 0 ; i < this . blockList . length ; i ++ ) { const curr = this . blockList . at ( i ) ; if ( ! groups [ curr . indent ] ) { groups [ curr . indent ] = [ [ ] ] ; } if ( this . hasHeader ( curr ) && groups [ curr . indent ] [ groups [ curr . indent ] . length - 1 ] . length > 0 ) { groups [ curr . indent ] . push ( [ ] ) ; } groups [ curr . indent ] [ groups [ curr . indent ] . length - 1 ] . push ( i ) ; if ( curr && curr . lines . length && curr . lines [ 0 ] . length && REGEX . TRIGGER . test ( curr . lines [ 0 ] ) ) { if ( i === this . blockList . length - 1 ) { } else { groups [ curr . indent ] . push ( [ ] ) ; } } const indents = Object . keys ( groups ) . sort ( ( a , b ) => a - b ) ; for ( let j = indents . length - 1 ; indents [ j ] > curr . indent ; j -- ) { const jlen = groups [ indents [ j ] ] . length ; if ( groups [ indents [ j ] ] [ jlen - 1 ] . length > 0 ) { groups [ indents [ j ] ] . push ( [ ] ) ; } } } return groups ; } public getFinalizedLogs ( ) { if ( ! this . log ) { throw new Error ( 'No log assigned to QDLParser' ) ; } const finalized = this . log . finalize ( ) ; this . log = null ; const logMap = { info : [ ] , warning : [ ] , error : [ ] , internal : [ ] } ; for ( const m of finalized ) { switch ( m . type ) { case 'info' : logMap . info . push ( m ) ; break ; case 'warning' : logMap . warning . push ( m ) ; break ; case 'error' : logMap . error . push ( m ) ; break ; case 'internal' : logMap . internal . push ( m ) ; break ; default : const log = new Logger ( ) ; log . internal ( 'Unknown message type' , '506' ) ; Array . prototype . push . apply ( logMap . internal , log . finalize ( ) ) ; break ; } } return logMap ; } private renderSegment ( nextIndent , startBlockIdx , endBlockIdx ) { const baseIndent = this . blockList . at ( startBlockIdx ) . indent ; let afterBlock = this . blockList . at ( endBlockIdx + 1 ) ; if ( afterBlock && '' + afterBlock . indent === nextIndent ) { do { endBlockIdx ++ ; afterBlock = this . blockList . at ( endBlockIdx + 1 ) ; } while ( afterBlock && afterBlock . indent > baseIndent ) ; } const toRender = [ ] ; const log = new Logger ( ) ; for ( let i = startBlockIdx ; i <= endBlockIdx ; i ++ ) { const block = this . blockList . at ( i ) ; if ( block . render === undefined ) { if ( block . indent !== baseIndent ) { const l2 = new Logger ( [ block ] ) ; l2 . internal ( 'found unrendered non-baseIndent block' , '507' ) ; log . extend ( l2 . finalize ( ) ) ; } toRender . push ( block ) ; } else if ( nextIndent !== undefined ) { if ( '' + block . indent !== nextIndent || block . render === null ) { continue ; } toRender . push ( block ) ; } } log . extend ( this . renderBlockList ( toRender ) ) ; return log . finalize ( ) ; } private renderBlockList ( blocks ) { const log = new Logger ( blocks ) ; if ( ! blocks . length ) { log . internal ( 'empty or null block set given' , '508' ) ; return log . finalize ( ) ; } if ( blocks [ 0 ] . render ) { log . internal ( 'found rendered zeroth block' , '509' ) ; return log . finalize ( ) ; } const headerLine = blocks [ 0 ] . lines [ 0 ] ; let lines = '' ; for ( const b of blocks ) { lines += ' ' + b . startLine ; if ( b . render === undefined ) { b . render = null ; } } log . dbg ( 'Rendering block group:' + lines ) ; if ( headerLine [ 0 ] === '#' ) { if ( blocks . length !== 1 ) { log . err ( 'quest card group cannot contain multiple cards' , '423' ) ; } this . renderer . toQuest ( blocks [ 0 ] , log ) ; } else if ( REGEX . TRIGGER . test ( headerLine ) ) { this . renderer . toTrigger ( blocks , log ) ; } else { this . renderer . toNode ( blocks , log ) ; } return log . finalize ( ) ; } } export function renderXML ( md ) { const qdl = new QDLParser ( XMLRenderer ) ; qdl . render ( new BlockList ( md ) ) ; return qdl ; }