import { REGEX } from '../../Regex' ; import { sanitizeTemplate } from '../../schema/templates/Sanitize' ; import { getTemplateType , Instruction , TEMPLATE_ATTRIBUTE_MAP , TEMPLATE_TYPES , TemplateChild , TemplateType } from '../../schema/templates/Templates' ; import { Block } from '../block/BlockList' ; import { Logger } from '../Logger' ; import Normalize from '../validation/Normalize' ; import { Renderer } from './Renderer' ; export class BlockRenderer { private renderer : Renderer ; constructor ( renderer ) { this . renderer = renderer ; } public toNode ( blocks , log ) { let extracted = this . extractTemplate ( blocks [ 0 ] . lines [ 0 ] , undefined ) ; const hasHeader = Boolean ( extracted ) ; let templateType = TEMPLATE_TYPES [ 0 ] ; if ( hasHeader && extracted && typeof ( extracted . title ) === 'string' ) { templateType = getTemplateType ( extracted . title ) || TEMPLATE_TYPES [ 0 ] ; } extracted = extracted || { title : '' , id : undefined , json : { } } ; const attribs = extracted . json ; attribs . id = attribs . id || extracted . id ; if ( extracted . title !== templateType ) { attribs . title = attribs . title || extracted . title ; if ( attribs . title . replace ( /:.*?:/g , '#' ) . length >= 25 ) { log . err ( 'card title too long' , '433' ) ; } } const attrName = TEMPLATE_ATTRIBUTE_MAP [ templateType ] ; if ( attrName !== null ) { attribs [ attrName ] = attribs [ attrName ] || [ ] ; } const body = [ ] ; let i = 0 ; while ( i < blocks . length ) { const block = blocks [ i ] ; if ( block . render ) { log . err ( templateType + ' cannot contain indented sections that are not choices/events' , '411' , blocks [ 0 ] . startLine ) ; } const lines = this . collate ( ( i === 0 && hasHeader ) ? block . lines . slice ( 1 ) : block . lines ) ; let child = null ; let instruction ; let lineIdx = 0 ; for ( const line of lines ) { lineIdx ++ ; if ( line . startsWith ( '* ' ) ) { const bullet = this . extractBulleted ( line , block . startLine + lineIdx , log ) ; child = ( bullet ) ? Object . assign ( { } , bullet , { outcome : [ ] } ) : null ; } else if ( line . startsWith ( '- ' ) && attrName !== null ) { const unCollated = ( i === 0 && hasHeader ) ? block . lines . slice ( 1 ) : block . lines ; let j = 0 ; while ( unCollated [ j ] === '' ) { j ++ ; } for ( ; j < unCollated . length ; j ++ ) { const l = unCollated [ j ] ; if ( l === '' ) { break ; } if ( ! l . startsWith ( '- ' ) ) { log . err ( 'need whitespace between list and next section' , '420' , block . startLine + j ) ; break ; } const bullet = this . extractBulleted ( l , blocks [ 0 ] . startLine + i , log ) ; if ( ! bullet ) { continue ; } let b = bullet ; if ( ! bullet . text ) { b = { json : bullet . json , text : '{{' + bullet . visible + '}}' , } ; } attribs [ attrName ] . push ( b ) ; } } else if ( line . startsWith ( '> ' ) ) { instruction = this . extractInstruction ( line ) ; body . push ( instruction ) ; } else { body . push ( line ) ; } } if ( child ) { let inner = blocks [ ++ i ] ; while ( i < blocks . length && inner . indent !== block . indent ) { if ( ! inner . render ) { log . internal ( 'found unexpected block with no render' , '501' , blocks [ 0 ] . startLine ) ; i ++ ; continue ; } child . outcome . push ( inner . render ) ; i ++ ; inner = blocks [ i ] ; } if ( child . text . trim ( ) === '' ) { log . err ( 'choice/event missing title' , '428' , blocks [ i - 1 ] . startLine - 2 ) ; } body . push ( child ) ; } else { i ++ ; } } const sanitized = sanitizeTemplate ( templateType , attribs , body , blocks [ 0 ] . startLine , ( ) => this . renderer . toTrigger ( { text : 'end' } , - 1 ) , log ) ; blocks [ 0 ] . render = this . renderer . toTemplate ( templateType , sanitized . attribs , sanitized . body , blocks [ 0 ] . startLine ) ; } public toQuest ( block , log ) { block . render = this . renderer . toQuest ( this . toMeta ( block , log ) , block . startLine ) ; } public toTrigger ( blocks , log ) { if ( blocks . length !== 1 ) { log . err ( 'trigger found with indented section - check your starting whitespace' , '415' ) ; } let extracted ; try { extracted = this . extractTrigger ( blocks [ 0 ] . lines [ 0 ] ) ; } catch ( e ) { log . err ( 'could not parse trigger' , '410' ) ; extracted = { title : 'end' , visible : undefined } ; } blocks [ 0 ] . render = this . renderer . toTrigger ( extracted , blocks [ 0 ] . startLine ) ; } public validate ( rendered , log ? ) { this . renderer . validate ( rendered , log ) ; } public toMeta ( block , log ? ) { if ( ! block ) { return { title : 'UNKNOWN' } ; } const attrs = { title : block . lines [ 0 ] . substr ( 1 ) . trim ( ) } ; for ( let i = 1 ; i < block . lines . length && block . lines [ i ] !== '' ; i ++ ) { const kv = block . lines [ i ] . split ( ':' ) ; if ( kv . length !== 2 ) { if ( log ) { log . err ( 'invalid quest attribute line "' + block . lines [ i ] + '"' , '420' , block . startLine + i ) ; } continue ; } const k = kv [ 0 ] . toLowerCase ( ) ; const v = kv [ 1 ] . trim ( ) ; attrs [ k ] = v ; if ( k !== 'title' ) { if ( log ) { log . err ( 'Quest attributes have migrated to the "Publish" button - simply delete this line.' , '429' , block . startLine + i ) ; } } } return Normalize . questAttrs ( attrs , log ) ; } public finalize ( zeroIndentBlockGroupRoots , log ) { const toRender = [ ] ; let quest = null ; if ( zeroIndentBlockGroupRoots && zeroIndentBlockGroupRoots . length > 0 ) { const questBlock = zeroIndentBlockGroupRoots [ 0 ] ; if ( questBlock . lines . length && questBlock . lines [ 0 ] . length && questBlock . lines [ 0 ] [ 0 ] === '#' ) { quest = questBlock . render ; } else { log . err ( 'root card must be a quest header' , '421' , 0 ) ; quest = this . renderer . toQuest ( { title : 'Error' } , - 1 ) ; } } else { log . err ( 'no quest blocks found' , '422' ) ; quest = this . renderer . toQuest ( { title : 'Error' } , - 1 ) ; } for ( let i = 1 ; i < zeroIndentBlockGroupRoots . length ; i ++ ) { const block = zeroIndentBlockGroupRoots [ i ] ; if ( ! block ) { log . internal ( 'empty block found in finalize step' , '503' ) ; continue ; } if ( ! block . render ) { log . internal ( 'Unrendered block found in finalize step' , '504' ) ; continue ; } toRender . push ( zeroIndentBlockGroupRoots [ i ] . render ) ; } if ( toRender . length === 0 ) { toRender . push ( this . renderer . toTemplate ( TEMPLATE_TYPES [ 0 ] , { } , [ ] , - 1 ) ) ; } return this . renderer . finalize ( quest , toRender ) ; } private extractTemplate ( line , log ? ) { try { const m = line . match ( /^_(.*)_[^{\(]*(\(#([a-zA-Z0-9]*?)\))?[^{\(]*(\{.*\})?/ ) ; if ( ! m || ! m [ 1 ] ) { throw new Error ( 'Missing title' ) ; } return { id : m [ 3 ] , json : ( m [ 4 ] ) ? JSON . parse ( m [ 4 ] ) : { } , title : m [ 1 ] , } ; } catch ( e ) { if ( log ) { log . err ( 'could not parse card header' , '413' ) ; } return null ; } } private extractBulleted ( line , idx , log ) { try { const m = line . match ( /^[\*-]\s*(\{\{(.*?)\}\})?\s*((?:[^{]|(?:{{[^}]*}})*)*)(\{.*\})?$/ ) ; if ( ! m ) { throw new Error ( 'Match failed' ) ; } return { json : ( m [ 4 ] ) ? JSON . parse ( m [ 4 ] ) : { } , text : ( m [ 3 ] ) ? m [ 3 ] . trim ( ) : '' , visible : m [ 2 ] || undefined , } ; } catch ( e ) { if ( log ) { log . err ( 'failed to parse bulleted line (check your JSON)' , '412' , idx ) ; } return null ; } } private extractInstruction ( line ) { const m = line . match ( REGEX . INSTRUCTION ) ; if ( ! m ) { return { text : '' , visible : 'false' , } ; } return { text : m [ 3 ] || '' , visible : m [ 2 ] , } ; } private extractTrigger ( line ) { const m = line . match ( REGEX . TRIGGER ) ; if ( ! m ) { return { text : '' , visible : 'false' , } ; } return { text : m [ 3 ] , visible : m [ 2 ] , } ; } private collate ( lines ) { const result = [ '' ] ; for ( let i = 0 ; i < lines . length ; i ++ ) { if ( lines [ i ] === '' ) { continue ; } if ( lines [ i - 1 ] === '' && result [ result . length - 1 ] !== '' ) { result . push ( '' ) ; } result [ result . length - 1 ] += ( result [ result . length - 1 ] !== '' ) ? ' ' + lines [ i ] : lines [ i ] ; } return result ; } }