import { ParsedMessagePart , ParsedMessagePartType } from './parsed-message-part' ; import { ParsedMessagePartText } from './parsed-message-part-text' ; import { ParsedMessagePartPlaceholder } from './parsed-message-part-placeholder' ; import { ParsedMessagePartStartTag } from './parsed-message-part-start-tag' ; import { ParsedMessagePartEndTag } from './parsed-message-part-end-tag' ; import { INormalizedMessage , ValidationErrors } from '../api/i-normalized-message' ; import { DOMUtilities } from './dom-utilities' ; import { IMessageParser } from './i-message-parser' ; import { format , isNullOrUndefined } from 'util' ; import { IICUMessage , IICUMessageTranslation } from '../api/i-icu-message' ; import { ParsedMessagePartICUMessage } from './parsed-message-part-icu-message' ; import { ParsedMessagePartICUMessageRef } from './parsed-message-part-icu-message-ref' ; import { ICUMessage } from './icu-message' ; import { ParsedMessagePartEmptyTag } from './parsed-message-part-empty-tag' ; /**
 * Created by martin on 05.05.2017.
 * A message text read from a translation file.
 * Can contain placeholders, tags, text.
 * This class is a representation independent of the concrete format.
 */ export class ParsedMessage implements INormalizedMessage { /**
     * Parser that created this message (determines the native format).
     */ private _parser : IMessageParser ; /**
     * The message where this one stems from as translation.
     * Optional, set only for messages created by calling translate.
     */ private sourceMessage : ParsedMessage ; /**
     * The parts of the message.
     */ private _parts : ParsedMessagePart [ ] ; /**
     * messages xml representation.
     */ private _xmlRepresentation : Element ; constructor ( parser , sourceMessage ) { this . _parser = parser ; this . sourceMessage = sourceMessage ; this . _parts = [ ] ; } return getParser ( ) { return this . _parser ; } param normalizedString throws translate ( normalizedString ) { if ( isNullOrUndefined ( this . getICUMessage ( ) ) ) { return this . _parser . parseNormalizedString ( < string > normalizedString , this ) ; } else { throw new Error ( format ( 'cannot translate ICU message with simple string, use translateICUMessage() instead ("%s", "%s")' , normalizedString , this . asNativeString ( ) ) ) ; } } param icuTranslation throws translateICUMessage ( icuTranslation ) { const icuMessage = this . getICUMessage ( ) ; if ( isNullOrUndefined ( icuMessage ) ) { throw new Error ( format ( 'this is not an ICU message, use translate() instead ("%s", "%s")' , icuTranslation , this . asNativeString ( ) ) ) ; } else { const translatedICUMessage = icuMessage . translate ( icuTranslation ) ; return this . _parser . parseICUMessage ( translatedICUMessage . asNativeString ( ) , this ) ; } } param nativeString translateNativeString ( nativeString ) { return this . _parser . createNormalizedMessageFromXMLString ( nativeString , this ) ; } param displayFormat public asDisplayString ( displayFormat ? ) { return this . _parts . map ( ( part ) => part . asDisplayString ( displayFormat ) ) . join ( '' ) ; } /**
     * Returns the message content as format dependent native string.
     * Includes all format specific markup like <ph id="INTERPOLATION" ../> ..
     */ asNativeString ( ) { if ( isNullOrUndefined ( this . getICUMessage ( ) ) ) { return DOMUtilities . getXMLContent ( this . _xmlRepresentation ) ; } else { return this . getICUMessage ( ) . asNativeString ( ) ; } } return public validate ( ) { let hasErrors = false ; const errors = { } ; let e ; e = this . checkPlaceholderAdded ( ) ; if ( ! isNullOrUndefined ( e ) ) { errors . placeholderAdded = e ; hasErrors = true ; } e = this . checkICUMessageRefRemoved ( ) ; if ( ! isNullOrUndefined ( e ) ) { errors . icuMessageRefRemoved = e ; hasErrors = true ; } e = this . checkICUMessageRefAdded ( ) ; if ( ! isNullOrUndefined ( e ) ) { errors . icuMessageRefAdded = e ; hasErrors = true ; } return hasErrors ? errors : null ; } return validateWarnings ( ) { let hasWarnings = false ; const warnings = { } ; let w ; w = this . checkPlaceholderRemoved ( ) ; if ( ! isNullOrUndefined ( w ) ) { warnings . placeholderRemoved = w ; hasWarnings = true ; } w = this . checkTagRemoved ( ) ; if ( ! isNullOrUndefined ( w ) ) { warnings . tagRemoved = w ; hasWarnings = true ; } w = this . checkTagAdded ( ) ; if ( ! isNullOrUndefined ( w ) ) { warnings . tagAdded = w ; hasWarnings = true ; } return hasWarnings ? warnings : null ; } return isICUMessage ( ) { return this . _parts . length === 1 && this . _parts [ 0 ] . type === ParsedMessagePartType . ICU_MESSAGE ; } return containsICUMessageRef ( ) { return this . _parts . findIndex ( part => part . type === ParsedMessagePartType . ICU_MESSAGE_REF ) >= 0 ; } return public getICUMessage ( ) { if ( this . _parts . length === 1 && this . _parts [ 0 ] . type === ParsedMessagePartType . ICU_MESSAGE ) { const icuPart = < ParsedMessagePartICUMessage > this . _parts [ 0 ] ; return icuPart . getICUMessage ( ) ; } else { return null ; } } return private checkPlaceholderAdded ( ) { let e = null ; const suspiciousIndexes = [ ] ; if ( this . sourceMessage ) { const sourcePlaceholders = this . sourceMessage . allPlaceholders ( ) ; const myPlaceholders = this . allPlaceholders ( ) ; myPlaceholders . forEach ( ( index ) => { if ( ! sourcePlaceholders . has ( index ) ) { suspiciousIndexes . push ( index ) ; } } ) ; } if ( suspiciousIndexes . length === 1 ) { e = 'added placeholder ' + suspiciousIndexes [ 0 ] + ', which is not in original message' ; } else if ( suspiciousIndexes . length > 1 ) { let allSuspiciousIndexes = '' ; let first = true ; suspiciousIndexes . forEach ( ( index ) => { if ( ! first ) { allSuspiciousIndexes = allSuspiciousIndexes + ', ' ; } allSuspiciousIndexes = allSuspiciousIndexes + index ; first = false ; } ) ; e = 'added placeholders ' + allSuspiciousIndexes + ', which are not in original message' ; } return e ; } return private checkPlaceholderRemoved ( ) { let w = null ; const suspiciousIndexes = [ ] ; if ( this . sourceMessage ) { const sourcePlaceholders = this . sourceMessage . allPlaceholders ( ) ; const myPlaceholders = this . allPlaceholders ( ) ; sourcePlaceholders . forEach ( ( index ) => { if ( ! myPlaceholders . has ( index ) ) { suspiciousIndexes . push ( index ) ; } } ) ; } if ( suspiciousIndexes . length === 1 ) { w = 'removed placeholder ' + suspiciousIndexes [ 0 ] + ' from original message' ; } else if ( suspiciousIndexes . length > 1 ) { let allSuspiciousIndexes = '' ; let first = true ; suspiciousIndexes . forEach ( ( index ) => { if ( ! first ) { allSuspiciousIndexes = allSuspiciousIndexes + ', ' ; } allSuspiciousIndexes = allSuspiciousIndexes + index ; first = false ; } ) ; w = 'removed placeholders ' + allSuspiciousIndexes + ' from original message' ; } return w ; } return private checkICUMessageRefAdded ( ) { let e = null ; const suspiciousIndexes = [ ] ; if ( this . sourceMessage ) { const sourceICURefs = this . sourceMessage . allICUMessageRefs ( ) ; const myICURefs = this . allICUMessageRefs ( ) ; myICURefs . forEach ( ( index ) => { if ( ! sourceICURefs . has ( index ) ) { suspiciousIndexes . push ( index ) ; } } ) ; } if ( suspiciousIndexes . length === 1 ) { e = 'added ICU message reference ' + suspiciousIndexes [ 0 ] + ', which is not in original message' ; } else if ( suspiciousIndexes . length > 1 ) { let allSuspiciousIndexes = '' ; let first = true ; suspiciousIndexes . forEach ( ( index ) => { if ( ! first ) { allSuspiciousIndexes = allSuspiciousIndexes + ', ' ; } allSuspiciousIndexes = allSuspiciousIndexes + index ; first = false ; } ) ; e = 'added ICU message references ' + allSuspiciousIndexes + ', which are not in original message' ; } return e ; } return private checkICUMessageRefRemoved ( ) { let e = null ; const suspiciousIndexes = [ ] ; if ( this . sourceMessage ) { const sourceICURefs = this . sourceMessage . allICUMessageRefs ( ) ; const myICURefs = this . allICUMessageRefs ( ) ; sourceICURefs . forEach ( ( index ) => { if ( ! myICURefs . has ( index ) ) { suspiciousIndexes . push ( index ) ; } } ) ; } if ( suspiciousIndexes . length === 1 ) { e = 'removed ICU message reference ' + suspiciousIndexes [ 0 ] + ' from original message' ; } else if ( suspiciousIndexes . length > 1 ) { let allSuspiciousIndexes = '' ; let first = true ; suspiciousIndexes . forEach ( ( index ) => { if ( ! first ) { allSuspiciousIndexes = allSuspiciousIndexes + ', ' ; } allSuspiciousIndexes = allSuspiciousIndexes + index ; first = false ; } ) ; e = 'removed ICU message references ' + allSuspiciousIndexes + ' from original message' ; } return e ; } /**
     * Get all indexes of placeholders used in the message.
     */ private allPlaceholders ( ) { const result = new Set < number > ( ) ; this . parts ( ) . forEach ( ( part ) => { if ( part . type === ParsedMessagePartType . PLACEHOLDER ) { const index = ( < ParsedMessagePartPlaceholder > part ) . index ( ) ; result . add ( index ) ; } } ) ; return result ; } param index return public getPlaceholderDisp ( index ) { let placeHolder = null ; this . parts ( ) . forEach ( ( part ) => { if ( part . type === ParsedMessagePartType . PLACEHOLDER ) { const phPart = < ParsedMessagePartPlaceholder > part ; if ( phPart . index ( ) === index ) { placeHolder = phPart ; } } } ) ; return placeHolder ? placeHolder . disp ( ) : null ; } /**
     * Get all indexes of ICU message refs used in the message.
     */ private allICUMessageRefs ( ) { const result = new Set < number > ( ) ; this . parts ( ) . forEach ( ( part ) => { if ( part . type === ParsedMessagePartType . ICU_MESSAGE_REF ) { const index = ( < ParsedMessagePartICUMessageRef > part ) . index ( ) ; result . add ( index ) ; } } ) ; return result ; } param index return public getICUMessageRefDisp ( index ) { let icuMessageRefPart = null ; this . parts ( ) . forEach ( ( part ) => { if ( part . type === ParsedMessagePartType . ICU_MESSAGE_REF ) { const refPart = < ParsedMessagePartICUMessageRef > part ; if ( refPart . index ( ) === index ) { icuMessageRefPart = refPart ; } } } ) ; return icuMessageRefPart ? icuMessageRefPart . disp ( ) : null ; } return private checkTagAdded ( ) { let e = null ; const suspiciousTags = [ ] ; if ( this . sourceMessage ) { const sourceTags = this . sourceMessage . allTags ( ) ; const myTags = this . allTags ( ) ; myTags . forEach ( ( tagName ) => { if ( ! sourceTags . has ( tagName ) ) { suspiciousTags . push ( tagName ) ; } } ) ; } if ( suspiciousTags . length === 1 ) { e = 'added tag <' + suspiciousTags [ 0 ] + '>, which is not in original message' ; } else if ( suspiciousTags . length > 1 ) { let allSuspiciousTags = '' ; let first = true ; suspiciousTags . forEach ( ( tag ) => { if ( ! first ) { allSuspiciousTags = allSuspiciousTags + ', ' ; } allSuspiciousTags = allSuspiciousTags + '<' + tag + '>' ; first = false ; } ) ; e = 'added tags ' + allSuspiciousTags + ', which are not in original message' ; } return e ; } return private checkTagRemoved ( ) { let w = null ; const suspiciousTags = [ ] ; if ( this . sourceMessage ) { const sourceTags = this . sourceMessage . allTags ( ) ; const myTags = this . allTags ( ) ; sourceTags . forEach ( ( tagName ) => { if ( ! myTags . has ( tagName ) ) { suspiciousTags . push ( tagName ) ; } } ) ; } if ( suspiciousTags . length === 1 ) { w = 'removed tag <' + suspiciousTags [ 0 ] + '> from original message' ; } else if ( suspiciousTags . length > 1 ) { let allSuspiciousTags = '' ; let first = true ; suspiciousTags . forEach ( ( tag ) => { if ( ! first ) { allSuspiciousTags = allSuspiciousTags + ', ' ; } allSuspiciousTags = allSuspiciousTags + '<' + tag + '>' ; first = false ; } ) ; w = 'removed tags ' + allSuspiciousTags + ' from original message' ; } return w ; } /**
     * Get all tag names used in the message.
     */ private allTags ( ) { const result = new Set < string > ( ) ; this . parts ( ) . forEach ( ( part ) => { if ( part . type === ParsedMessagePartType . START_TAG || part . type === ParsedMessagePartType . EMPTY_TAG ) { const tagName = ( < ParsedMessagePartStartTag > part ) . tagName ( ) ; result . add ( tagName ) ; } } ) ; return result ; } public parts ( ) { return this . _parts ; } setXmlRepresentation ( xmlRepresentation ) { this . _xmlRepresentation = xmlRepresentation ; } addText ( text ) { this . _parts . push ( new ParsedMessagePartText ( text ) ) ; } addPlaceholder ( index , disp ) { this . _parts . push ( new ParsedMessagePartPlaceholder ( index , disp ) ) ; } addStartTag ( tagname , idcounter ) { this . _parts . push ( new ParsedMessagePartStartTag ( tagname , idcounter ) ) ; } addEndTag ( tagname ) { const openTag = this . calculateOpenTagName ( ) ; if ( ! openTag || openTag !== tagname ) { throw new Error ( format ( 'unexpected close tag %s (currently open is %s, native xml is "%s")' , tagname , openTag , this . asNativeString ( ) ) ) ; } this . _parts . push ( new ParsedMessagePartEndTag ( tagname ) ) ; } addEmptyTag ( tagname , idcounter ) { this . _parts . push ( new ParsedMessagePartEmptyTag ( tagname , idcounter ) ) ; } addICUMessageRef ( index , disp ) { this . _parts . push ( new ParsedMessagePartICUMessageRef ( index , disp ) ) ; } addICUMessage ( text ) { this . _parts . push ( new ParsedMessagePartICUMessage ( text , this . _parser ) ) ; } /**
     * Determine, wether there is an open tag, that is not closed.
     * Returns the latest one or null, if there is no open tag.
     */ private calculateOpenTagName ( ) { const openTags = [ ] ; this . _parts . forEach ( ( part ) => { switch ( part . type ) { case ParsedMessagePartType . START_TAG : openTags . push ( ( < ParsedMessagePartStartTag > part ) . tagName ( ) ) ; break ; case ParsedMessagePartType . END_TAG : const tagName = ( < ParsedMessagePartEndTag > part ) . tagName ( ) ; if ( openTags . length === 0 || openTags [ openTags . length - 1 ] !== tagName ) { const openTag = ( openTags . length === 0 ) ? 'nothing' : openTags [ openTags . length - 1 ] ; throw new Error ( format ( 'unexpected close tag %s (currently open is %s, native xml is "%s")' , tagName , openTag , this . asNativeString ( ) ) ) ; } openTags . pop ( ) ; } } ) ; return openTags . length === 0 ? null : openTags [ openTags . length - 1 ] ; } }