import { Argument , MessageCode , MessageOpType , PluralChoice , PluralChoiceType , PluralNumberType , SelectChoice , } from './types' ; import { MessageMatcher , MessageState } from './matcher' ; const enum Chars { LEFT = '{' , RIGHT = '}' , MINUS = '-' , APOS = "'" , POUND = '#' } public export const parseMessagePattern = ( raw , matcher ) : MessageCode => new MessagePatternParser ( raw , matcher ) . parse ( ) ; /**
 * Hand-implemented parser for ICU message format. Designed to be compact and
 * fast vs. other implementations.
 * The parser produces an instruction tree which can be cached for repeated
 * use, and is intended to be evaluated by a separate engine.
 *
 * Note: The 'choice' formatter is not implemented since it is deprecated.
 *
 * See ICU docs for details on syntax:
 * https://unicode-org.github.io/icu-docs/apidoc/released/icu4j/com/ibm/icu/text/MessageFormat.html
 *
 * Rationale
 *
 * The decision to hand-implement the parser was for 2 reasons:
 * 1. Avoiding extra runtime dependencies (parser generators, e.g. pegjs, etc)
 * 2. Provide control over the memory allocation, garbage generation, and other
 *    aspects that impact parser performance.
 *
 * A major consideration is size of the resulting parser code. The OpenJS project
 * 'messageformat-parser' which is generated by Peg.js is 30kB minified. It also
 * requires other dependencies for the plural calculations, where this is already
 * supported in our library via @phensley/plurals
 *
 * See:
 *   https://unpkg.com/messageformat-parser/parser.js
 */ class MessagePatternParser { constructor ( private raw , private matcher ) { } parse ( ) { const t = this . raw ; return this . outer ( { t , s : 0 , e : t . length } ) ; } outer ( r , argsub ? ) { const str = this . raw ; const n = [ ] ; let buf = '' ; while ( r . s < r . e ) { const c = str [ r . s ] ; switch ( c ) { case Chars . LEFT : { if ( buf ) { n . push ( textarg ( buf , argsub ) ) ; buf = '' ; } const hidden = str [ r . s + 1 ] === Chars . MINUS ; const k = this . seek ( r . s , r . e ) ; if ( k === - 1 ) { n . push ( textarg ( str . substring ( r . s , r . e ) , argsub ) ) ; r . s = r . e ; } else if ( hidden ) { n . push ( text ( Chars . LEFT + str . substring ( r . s + 2 , k + 1 ) ) ) ; r . s = k ; } else { const child = this . inner ( { t : r . t , s : r . s + 1 , e : k } ) ; if ( ! child ) { if ( argsub !== undefined && r . s + 1 !== k ) { n . push ( textarg ( str . substring ( r . s + 1 , k ) , argsub ) ) ; } } else { n . push ( child ) ; } r . s = k ; } break ; } case Chars . APOS : { if ( c === str [ r . s + 1 ] ) { buf += c ; r . s ++ ; } else { r . s ++ ; let k = str . indexOf ( c , r . s ) ; if ( k === - 1 ) { k = r . e ; } buf += str . substring ( r . s , k ) ; r . s = k ; } break ; } default : buf += c ; break ; } r . s ++ ; } if ( buf ) { n . push ( textarg ( buf , argsub ) ) ; } return flatten ( n ) ; } inner ( r ) { const m = this . matcher ; m . spaces ( r ) ; const args = m . arguments ( r ) ; if ( ! args ) { return undefined ; } if ( ! m . spaces ( r ) || m . complete ( r ) ) { return [ MessageOpType . ARG , args [ 0 ] ] ; } const name = m . formatter ( r ) ; if ( ! name ) { return undefined ; } m . spaces ( r ) ; switch ( name ) { case 'plural' : case 'selectordinal' : const type = name === 'plural' ? PluralNumberType . CARDINAL : PluralNumberType . ORDINAL ; return this . plural ( args , type , m , r ) ; case 'select' : return this . select ( args , m , r ) ; default : return this . simple ( args , name , m , r ) ; } return NOOP ; } /**
     * Parse a nested tag sequence '{' ... '}'
     */ tag ( m , r , argsub ? ) { m . spaces ( r ) ; if ( m . char ( r ) !== Chars . LEFT ) { return undefined ; } const hidden = this . raw [ r . s + 1 ] === Chars . MINUS ; const k = this . seek ( r . s , r . e ) ; const node = hidden ? text ( Chars . LEFT + this . raw . substring ( r . s + 2 , k + 1 ) ) : this . outer ( { t : r . t , s : r . s + 1 , e : k } , argsub ) ; r . s = k + 1 ; return node ; } /**
     * Parse a plural instruction.
     */ plural ( args , type , m , r ) { const offset = m . pluralOffset ( r ) ; m . spaces ( r ) ; const choices = [ ] ; do { const choice = m . pluralChoice ( r ) ; if ( ! choice ) { break ; } const block = this . tag ( m , r , args [ 0 ] ) ; if ( ! block ) { return NOOP ; } let node ; if ( choice [ 0 ] === '=' ) { const num = choice . substring ( 1 ) ; node = [ PluralChoiceType . EXACT , num , block ] ; } else { node = [ PluralChoiceType . CATEGORY , choice , block ] ; } choices . push ( node ) ; m . spaces ( r ) ; } while ( ! m . complete ( r ) ) ; return choices . length ? [ MessageOpType . PLURAL , args , offset , type , choices ] : NOOP ; } /**
     * Parse a select instruction.
     */ select ( args , m , r ) { const choices = [ ] ; do { const ident = m . identifier ( r ) ; if ( ! ident ) { break ; } const block = this . tag ( m , r , args [ 0 ] ) ; if ( ! block ) { return NOOP ; } choices . push ( [ ident , block ] ) ; m . spaces ( r ) ; } while ( ! m . complete ( r ) ) ; return choices . length ? [ MessageOpType . SELECT , args , choices ] : NOOP ; } /**
     * Simple single-argument formatter with zero or more options.
     */ simple ( args , name , m , r ) { const options = m . options ( r ) ; return [ MessageOpType . SIMPLE , name , args , options ] ; } /**
     * Seek to the matching '}' character at the same nesting level,
     * skipping over any apostrophes. This adds some redundant scanning
     * of the string but simplifies some of the parsing logic in other
     * areas. It ensures we're always dealing with a well-formed tag
     * where all '{' have a corresponding '}'.
     */ seek ( i , j ) { const r = this . raw ; let d = 0 ; loop : while ( i < j ) { const c = r [ i ] ; switch ( c ) { case Chars . LEFT : d ++ ; break ; case Chars . RIGHT : d -- ; if ( ! d ) { break loop ; } break ; case Chars . APOS : if ( c === r [ i + 1 ] ) { i ++ ; } else { const k = r . indexOf ( c , i + 1 ) ; if ( k === - 1 ) { return - 1 ; } i = k ; } break ; } i ++ ; } return d ? - 1 : i ; } } const flatten = ( n ) : MessageCode => ! n . length ? NOOP : n . length === 1 ? n [ 0 ] : [ MessageOpType . BLOCK , n ] ; const text = ( s ) : MessageCode => [ MessageOpType . TEXT , s ] ; /**
 * Emit a text node, performing argument substitution for all occurrences of
 * the '#' character.
 */ const textarg = ( s , argsub ? ) : MessageCode => { let i = 0 ; let j = 0 ; if ( argsub === undefined ) { return text ( s ) ; } j = s . indexOf ( Chars . POUND ) ; if ( j === - 1 ) { return text ( s ) ; } const len = s . length ; const n = [ ] ; while ( j !== - 1 ) { if ( i < j ) { n . push ( text ( s . substring ( i , j ) ) ) ; } n . push ( [ MessageOpType . ARGSUB ] ) ; i = j + 1 ; j = s . indexOf ( Chars . POUND , i ) ; } if ( i < len ) { n . push ( text ( s . substring ( i ) ) ) ; } return flatten ( n ) ; } ; const NOOP = [ MessageOpType . NOOP ] ;