import { BlockPathError , ErrorLocation , Position , errorHasRange } from "../errors" ; import { ATTR_PRESENT , CLASS_NAME_IDENT , DEFAULT_EXPORT , ROOT_CLASS , } from "./BlockSyntax" ; interface BlockToken { type : "block" ; name : string ; } interface ClassToken { type : "class" ; name : string ; } interface IAttrToken { namespace ? : string ; value ? : string ; name : string ; quoted ? : boolean ; } interface AttrToken extends IAttrToken { type : "attribute" ; } type Token = BlockToken | ClassToken | AttrToken ; export { IAttrToken as AttrToken } ; const isBlock = ( token ? ) : token is BlockToken => ! ! token && token . type === "block" ; const isClass = ( token ? ) : token is ClassToken => ! ! token && token . type === "class" ; const isAttribute = ( token ? ) : token is AttrToken => ! ! token && token . type === "attribute" ; const isQuoted = ( token ? ) : boolean => ! ! token && ! ! token . quoted ; const isIdent = ( ident ? ) : boolean => ! ident || CLASS_NAME_IDENT . test ( ident ) ; const hasName = ( token ? ) : boolean => ! ! token && ! ! token . name ; const isValidNamespace = ( token ? ) : boolean => ! ! token && ( token . namespace === undefined ) ; const ATTR_BEGIN = "[" ; const ATTR_END = "]" ; const CLASS_BEGIN = "." ; const NAMESPACE_END = "|" ; const VALUE_START = "=" ; const SINGLE_QUOTE = `'` ; const DOUBLE_QUOTE = `"` ; const PSEUDO_BEGIN = ":" ; const WHITESPACE_REGEXP = /\s/g ; const SEPARATORS = new Set ( [ CLASS_BEGIN , ATTR_BEGIN , PSEUDO_BEGIN ] ) ; export const ERRORS = { whitespace : "Whitespace is only allowed in quoted attribute values" , namespace : "Namespaces aren't used for states in a block path." , noname : "Block path segments must include a valid name" , unclosedAttribute : "Unclosed attribute selector" , mismatchedQuote : "No closing quote found in Block path" , invalidIdent : ( i ) => `Invalid identifier "${ i }" found in Block path.` , expectsSepInsteadRec : ( c ) => `Expected separator tokens "[" or ".", instead found \`${ c }\`` , illegalCharNotInAttribute : ( c ) => `Only attribute selectors may contain the \`${ c }\` character.` , illegalCharInAttribute : ( c ) => `Attribute selectors may not contain the \`${ c }\` character.` , multipleOfType : ( t ) => `Cannot have more than one ${ t } selector in the same Block path` , } ; function stringify ( tokens ) { let out = "" ; for ( let token of tokens ) { if ( isBlock ( token ) ) { out += token . name === DEFAULT_EXPORT ? "" : token . name ; } else if ( isClass ( token ) ) { out += token . name === ROOT_CLASS ? token . name : `.${ token . name }` ; } else if ( isAttribute ( token ) ) { let namespace = token . namespace ? `${ token . namespace }|` : "" ; let value = token . value && token . value !== ATTR_PRESENT ? `="${ token . value }"` : "" ; out += `[${ namespace }${ token . name }${ value }]` ; } } return out ; } /**
 * Simple utility to easily walk over string data one character at a time.
 */ class Walker { private data = "" ; private length = 0 ; private idx = 0 ; init ( data ) { this . data = data ; this . length = data . length ; this . idx = 0 ; } next ( ) { return this . data [ this . idx ++ ] ; } peek ( ) { return this . data [ this . idx ] ; } index ( ) { return this . idx ; } param stop consume ( stop , ... args ) { let out = "" ; stop = typeof stop === "string" ? new Set ( [ stop , ... args ] ) : stop ; while ( ! stop . has ( this . data [ this . idx ] ) && this . idx <= this . length - 1 ) { out += this . data [ this . idx ++ ] ; } return out ; } } /**
 * Parser and container object for Block Path data.
 */ export class BlockPath { private _location ? : ErrorLocation ; private _block ? : BlockToken ; private _class ? : ClassToken ; private _attribute ? : AttrToken ; private walker : Walker = new Walker ( ) ; private _tokens : Token [ ] = [ ] ; private parts : Token [ ] = [ ] ; public readonly original : string ; param msg private throw ( msg , len = 0 ) { let filename ; let start ; let end ; if ( this . _location ) { filename = this . _location . filename ; if ( errorHasRange ( this . _location ) ) { start = { ... this . _location . start } ; end = { ... this . _location . end } ; } else { start = { column : 0 , line : 1 } ; end = { column : 0 , line : 1 } ; } end . column = start . column + this . walker . index ( ) ; start . column = start . column + this . walker . index ( ) - len ; } throw new BlockPathError ( msg , { filename , start , end } ) ; } param token private addToken ( token , isUserProvided ) { if ( ! token . type ) { return ; } if ( ! isBlock ( token ) && ! hasName ( token ) ) { this . throw ( ERRORS . noname ) ; } if ( isAttribute ( token ) && ! isValidNamespace ( token ) ) { this . throw ( ERRORS . namespace ) ; } if ( isBlock ( token ) ) { token . name = token . name || DEFAULT_EXPORT ; this . _block = this . _block ? this . throw ( ERRORS . multipleOfType ( token . type ) ) : token ; } if ( isClass ( token ) ) { this . _class = this . _class ? this . throw ( ERRORS . multipleOfType ( token . type ) ) : token ; if ( ! this . _block ) { this . addToken ( { type : "block" , name : DEFAULT_EXPORT } , false ) ; } } if ( isAttribute ( token ) ) { this . _attribute = this . _attribute ? this . throw ( ERRORS . multipleOfType ( token . type ) ) : token ; if ( ! this . _class ) { this . addToken ( { type : "class" , name : ROOT_CLASS } , false ) ; } } if ( isUserProvided ) { this . _tokens . push ( token as Token ) ; } this . parts . push ( token as Token ) ; } param str private tokenize ( ) { let char , working = "" , walker = this . walker , token = { type : "block" } ; while ( char = walker . next ( ) ) { switch ( true ) { case char === PSEUDO_BEGIN : if ( ! isBlock ( token ) ) { return this . throw ( ERRORS . invalidIdent ( PSEUDO_BEGIN ) , working . length ) ; } token . name = working ; this . addToken ( token , true ) ; working = `${ PSEUDO_BEGIN }${ walker . consume ( SEPARATORS ) }` ; if ( working === ROOT_CLASS ) { this . addToken ( { type : "class" , name : ROOT_CLASS } , true ) ; working = "" ; token = { } ; break ; } else { return this . throw ( ERRORS . invalidIdent ( working ) , working . length ) ; } case char === CLASS_BEGIN : if ( isAttribute ( token ) ) { this . throw ( ERRORS . illegalCharInAttribute ( char ) ) ; } if ( ! isIdent ( working ) ) { return this . throw ( ERRORS . invalidIdent ( working ) , working . length ) ; } token . name = working ; this . addToken ( token , true ) ; token = { type : "class" } ; working = "" ; break ; case char === ATTR_BEGIN : if ( isAttribute ( token ) ) { this . throw ( ERRORS . illegalCharInAttribute ( char ) ) ; } if ( ! isIdent ( working ) ) { return this . throw ( ERRORS . invalidIdent ( working ) , working . length ) ; } token . name = working ; this . addToken ( token , true ) ; token = { type : "attribute" } ; working = "" ; break ; case char === NAMESPACE_END : if ( ! isAttribute ( token ) ) { return this . throw ( ERRORS . illegalCharNotInAttribute ( char ) ) ; } if ( ! isIdent ( working ) ) { return this . throw ( ERRORS . invalidIdent ( working ) , working . length ) ; } token . namespace = working ; working = "" ; break ; case char === VALUE_START : if ( ! isAttribute ( token ) ) { this . throw ( ERRORS . illegalCharNotInAttribute ( char ) ) ; } if ( ! working ) { this . throw ( ERRORS . noname ) ; } if ( ! isIdent ( working ) ) { return this . throw ( ERRORS . invalidIdent ( working ) , working . length ) ; } token . name = working ; working = "" ; break ; case char === SINGLE_QUOTE || char === DOUBLE_QUOTE : if ( ! isAttribute ( token ) ) { return this . throw ( ERRORS . illegalCharNotInAttribute ( char ) ) ; } working = walker . consume ( char ) ; token . quoted = true ; if ( walker . peek ( ) !== char ) { this . throw ( ERRORS . mismatchedQuote ) ; } walker . next ( ) ; break ; case char === ATTR_END : if ( ! isAttribute ( token ) ) { return this . throw ( ERRORS . illegalCharNotInAttribute ( char ) ) ; } if ( hasName ( token ) && ! isQuoted ( token ) && ! isIdent ( working ) ) { return this . throw ( ERRORS . invalidIdent ( working ) , working . length ) ; } ( hasName ( token ) ) ? ( token . value = working ) : ( token . name = working ) ; token . value = token . value || ATTR_PRESENT ; this . addToken ( token , true ) ; working = "" ; let next = walker . next ( ) ; if ( next && ! SEPARATORS . has ( next ) ) { this . throw ( ERRORS . expectsSepInsteadRec ( next ) ) ; } token = ( next === ATTR_BEGIN ) ? { type : "attribute" } : { type : "class" } ; break ; case WHITESPACE_REGEXP . test ( char ) : this . throw ( ERRORS . whitespace ) ; default : working += char ; } } if ( isAttribute ( token ) ) { this . throw ( ERRORS . unclosedAttribute ) ; } if ( ! isAttribute ( token ) && working ) { if ( ! isIdent ( working ) ) { return this . throw ( ERRORS . invalidIdent ( working ) , working . length ) ; } token . name = working ; this . addToken ( token , true ) ; } if ( ! this . _class ) { this . addToken ( { type : "class" , name : ROOT_CLASS } , false ) ; } } param path param location constructor ( path , location ? ) { this . _location = location ; this . original = path . toString ( ) ; if ( path instanceof BlockPath ) { this . parts = path . parts ; } else { this . walker . init ( path ) ; this . tokenize ( ) ; } } private static from ( tokens ) { let path = new BlockPath ( "" ) ; path . parts = tokens ; return path ; } /**
     * Get the parsed Style path of this Block Path
     */ get path ( ) : string { return stringify ( this . parts . slice ( 1 ) ) ; } /**
     * Get the parsed block name of this Block Path
     */ get block ( ) : string { return this . _block ? this . _block . name : DEFAULT_EXPORT ; } /**
     * Get the parsed class name of this Block Path
     */ get class ( ) : string { return this . _class && this . _class . name || ROOT_CLASS ; } /**
     * Get the parsed attribute name of this Block Path and return the `AttrInfo`
     */ get attribute ( ) : IAttrToken | undefined { if ( ! this . _attribute ) return ; return this . _attribute ; } /**
     * Return a pretty-printed formatted Block Path string from the tokenized data.
     */ toString ( ) { return stringify ( this . parts ) ; } /**
     * Return a new BlockPath without the parent-most token.
     */ childPath ( ) { return BlockPath . from ( this . parts . slice ( this . _block && this . _block . name !== DEFAULT_EXPORT ? 1 : 2 ) ) ; } /**
     * Return a new BlockPath without the child-most token.
     */ parentPath ( ) { return BlockPath . from ( this . parts . slice ( 0 , - 1 ) ) ; } tokens ( ) { return this . _tokens . slice ( ) . map ( ( t ) => t . name ) ; } }