@@ -17,15 +17,18 @@ export interface StateToken {
1717 namespace : string ;
1818 name : string ;
1919 value ?: string ;
20+ quoted : boolean ;
2021}
2122
2223type Token = BlockToken | ClassToken | StateToken ;
2324
24- const isBlock = ( token ?: Token ) : token is BlockToken => ! ! token && token . type === 'block' ;
25- const isClass = ( token ?: Token ) : token is ClassToken => ! ! token && token . type === 'class' ;
26- const isState = ( token ?: Token ) : token is StateToken => ! ! token && token . type === 'state' ;
27- const hasName = ( token ?: Token ) : boolean => ! ! token && ! ! token . name ;
28- const hasNamespace = ( token ?: Token ) : boolean => isState ( token ) && ! ! token . namespace ;
25+ const isBlock = ( token ?: Partial < Token > ) : token is BlockToken => ! ! token && token . type === 'block' ;
26+ const isClass = ( token ?: Partial < Token > ) : token is ClassToken => ! ! token && token . type === 'class' ;
27+ const isState = ( token ?: Partial < Token > ) : token is StateToken => ! ! token && token . type === 'state' ;
28+ const isQuoted = ( token ?: Partial < Token > ) : boolean => isState ( token ) && ! ! token . quoted ;
29+ const isIdent = ( ident ?: string ) : boolean => ! ident || CSS_IDENT . test ( ident ) ;
30+ const hasName = ( token ?: Partial < Token > ) : boolean => ! ! token && ! ! token . name ;
31+ const hasNamespace = ( token ?: Partial < Token > ) : boolean => isState ( token ) && ! ! token . namespace ;
2932
3033const STATE_BEGIN = "[" ;
3134const STATE_END = "]" ;
@@ -43,7 +46,7 @@ export const ERRORS = {
4346 noname : "Block path segments must include a valid name" ,
4447 unclosedState : "Unclosed state selector" ,
4548 mismatchedQuote : "No closing quote found in Block path" ,
46- illegalChar : ( c : string ) => `Unexpected character "${ c } " found in Block path.` ,
49+ invalidIdent : ( i : string ) => `Invalid identifier "${ i } " found in Block path.` ,
4750 expectsSepInsteadRec : ( c : string ) => `Expected separator tokens "[" or ".", instead found \`${ c } \`` ,
4851 illegalCharNotInState : ( c : string ) => `Only state selectors may contain the \`${ c } \` character.` ,
4952 illegalCharInState : ( c : string ) => `State selectors may not contain the \`${ c } \` character.` ,
@@ -75,6 +78,7 @@ class Walker {
7578
7679 next ( ) : string { return this . data [ this . idx ++ ] ; }
7780 peek ( ) : string { return this . data [ this . idx ] ; }
81+ index ( ) : number { return this . idx ; }
7882
7983 /**
8084 * Consume all characters that do not match the provided Set or strings
@@ -101,23 +105,31 @@ export class BlockPath {
101105 private _class : ClassToken ;
102106 private _state : StateToken ;
103107
108+ private walker : Walker ;
104109 private tokens : Token [ ] = [ ] ;
105110
106111 /**
107112 * Throw a new BlockPathError with the given message.
108113 * @param msg The error message.
109114 */
110- private throw ( msg : string ) : never {
111- throw new BlockPathError ( msg , this . _location ) ;
115+ private throw ( msg : string , len = 0 ) : never {
116+ let location ;
117+ if ( this . _location ) {
118+ location = {
119+ ...this . _location ,
120+ column : ( this . _location . column || 0 ) + this . walker . index ( ) - len
121+ } ;
122+ }
123+ throw new BlockPathError ( msg , location ) ;
112124 }
113125
114126 /**
115127 * Used by `tokenize` to insert a newly constructed token.
116128 * @param token The token to insert.
117129 */
118- private addToken ( token : Token ) : void {
130+ private addToken ( token : Partial < Token > ) : void {
119131
120- // Final validation of incoming data.
132+ // Final validation of incoming data. Blocks may have no name. States must have a namespace.
121133 if ( ! isBlock ( token ) && ! hasName ( token ) ) { this . throw ( ERRORS . noname ) ; }
122134 if ( isState ( token ) && ! hasNamespace ( token ) ) { this . throw ( ERRORS . namespace ) ; }
123135
@@ -127,13 +139,17 @@ export class BlockPath {
127139 }
128140 if ( isClass ( token ) ) {
129141 this . _class = this . _class ? this . throw ( ERRORS . multipleOfType ( token . type ) ) : token ;
142+ // If no block has been added yet, automatically inject the `self` block name.
143+ if ( ! this . _block ) { this . addToken ( { type : "block" , name : "" } ) ; }
130144 }
131145 if ( isState ( token ) ) {
132146 this . _state = this . _state ? this . throw ( ERRORS . multipleOfType ( token . type ) ) : token ;
147+ // If no class has been added yet, automatically inject the root class.
148+ if ( ! this . _class ) { this . addToken ( { type : "class" , name : "root" } ) ; }
133149 }
134150
135151 // Add the token.
136- this . tokens . push ( token ) ;
152+ this . tokens . push ( token as Token ) ;
137153 }
138154
139155 /**
@@ -144,8 +160,8 @@ export class BlockPath {
144160 private tokenize ( str : string ) : void {
145161 let char ,
146162 working = "" ,
147- walker = new Walker ( str ) ,
148- token : Token = { type : 'block' , name : ' ' } ;
163+ walker = this . walker = new Walker ( str ) ,
164+ token : Partial < Token > = { type : 'block' } ;
149165
150166 while ( char = walker . next ( ) ) {
151167
@@ -154,38 +170,27 @@ export class BlockPath {
154170 // If a period, we've finished the previous token and are now building a class name.
155171 case char === CLASS_BEGIN :
156172 if ( isState ( token ) ) { this . throw ( ERRORS . illegalCharInState ( char ) ) ; }
173+ if ( ! isIdent ( working ) ) { return this . throw ( ERRORS . invalidIdent ( working ) , working . length ) ; }
157174 token . name = working ;
158175 this . addToken ( token ) ;
159- token = { type : 'class' , name : '' } ;
176+ token = { type : 'class' } ;
160177 working = "" ;
161178 break ;
162179
163180 // If the beginning of a state, we've finished the previous token and are now building a state.
164181 case char === STATE_BEGIN :
165182 if ( isState ( token ) ) { this . throw ( ERRORS . illegalCharInState ( char ) ) ; }
183+ if ( ! isIdent ( working ) ) { return this . throw ( ERRORS . invalidIdent ( working ) , working . length ) ; }
166184 token . name = working ;
167185 this . addToken ( token ) ;
168- token = { type : 'state' , namespace : '' , name : '' } ;
186+ token = { type : 'state' } ;
169187 working = "" ;
170188 break ;
171189
172- // If the end of a state, set the state part we've been working on and finish.
173- case char === STATE_END :
174- if ( ! isState ( token ) ) { return this . throw ( ERRORS . illegalCharNotInState ( char ) ) ; }
175- token . name ? ( token . value = working ) : ( token . name = working ) ;
176- this . addToken ( token ) ;
177- working = "" ;
178-
179- // The character immediately following a `STATE_END` *must* be another `SEPARATORS`
180- // Depending on the next value, seed our token input
181- let next = walker . next ( ) ;
182- if ( next && ! SEPARATORS . has ( next ) ) { this . throw ( ERRORS . expectsSepInsteadRec ( next ) ) ; }
183- token = ( next === STATE_BEGIN ) ? { type : 'state' , namespace : '' , name : '' } : { type : 'class' , name : '' } ;
184- break ;
185-
186190 // When we find a namespace terminator, set the namespace property of the state token we're working on.
187191 case char === NAMESPACE_END :
188192 if ( ! isState ( token ) ) { return this . throw ( ERRORS . illegalCharNotInState ( char ) ) ; }
193+ if ( ! isIdent ( working ) ) { return this . throw ( ERRORS . invalidIdent ( working ) , working . length ) ; }
189194 token . namespace = working ;
190195 working = "" ;
191196 break ;
@@ -194,6 +199,7 @@ export class BlockPath {
194199 case char === VALUE_START :
195200 if ( ! isState ( token ) ) { this . throw ( ERRORS . illegalCharNotInState ( char ) ) ; }
196201 if ( ! working ) { this . throw ( ERRORS . noname ) ; }
202+ if ( ! isIdent ( working ) ) { return this . throw ( ERRORS . invalidIdent ( working ) , working . length ) ; }
197203 token . name = working ;
198204 working = "" ;
199205 break ;
@@ -202,10 +208,28 @@ export class BlockPath {
202208 case char === SINGLE_QUOTE || char === DOUBLE_QUOTE :
203209 if ( ! isState ( token ) ) { return this . throw ( ERRORS . illegalCharNotInState ( char ) ) ; }
204210 working = walker . consume ( char ) ;
211+ token . quoted = true ;
205212 if ( walker . peek ( ) !== char ) { this . throw ( ERRORS . mismatchedQuote ) ; }
206213 walker . next ( ) ; // Throw away the other quote
207214 break ;
208215
216+ // If the end of a state, set the state part we've been working on and finish.
217+ case char === STATE_END :
218+ if ( ! isState ( token ) ) { return this . throw ( ERRORS . illegalCharNotInState ( char ) ) ; }
219+ if ( ( ! hasName ( token ) || ! isQuoted ( token ) ) && ! isIdent ( working ) ) {
220+ console . log ( working ) ; return this . throw ( ERRORS . invalidIdent ( working ) , working . length ) ;
221+ }
222+ ( hasName ( token ) ) ? ( token . value = working ) : ( token . name = working ) ;
223+ this . addToken ( token ) ;
224+ working = "" ;
225+
226+ // The character immediately following a `STATE_END` *must* be another `SEPARATORS`
227+ // Depending on the next value, seed our token input
228+ let next = walker . next ( ) ;
229+ if ( next && ! SEPARATORS . has ( next ) ) { this . throw ( ERRORS . expectsSepInsteadRec ( next ) ) ; }
230+ token = ( next === STATE_BEGIN ) ? { type : 'state' } : { type : 'class' } ;
231+ break ;
232+
209233 // We should never encounter whitespace in this switch statement.
210234 // The only place whitespace is allowed is between quotes, which
211235 // is handled above.
@@ -217,7 +241,6 @@ export class BlockPath {
217241 // TODO: We need to handle invalid character escapes here!
218242 default :
219243 working += char ;
220- if ( ! CSS_IDENT . test ( working ) ) { return this . throw ( ERRORS . illegalChar ( char ) ) ; }
221244
222245 }
223246
@@ -228,11 +251,13 @@ export class BlockPath {
228251 if ( isState ( token ) ) { this . throw ( ERRORS . unclosedState ) ; }
229252
230253 // Class and Block tokens are not explicitly terminated and may be sealed when we
231- // get to the end.
254+ // get to the end. If no class has been discovered, automatically add our root class.
232255 if ( ! isState ( token ) && working ) {
256+ if ( ! isIdent ( working ) ) { return this . throw ( ERRORS . invalidIdent ( working ) , working . length ) ; }
233257 token . name = working ;
234258 this . addToken ( token ) ;
235259 }
260+ if ( ! this . _class ) { this . addToken ( { type : "class" , name : "root" } ) ; }
236261 }
237262
238263 /**
@@ -281,7 +306,7 @@ export class BlockPath {
281306 * Get the parsed state name of this Block Path and return the `StateInfo`
282307 */
283308 get state ( ) : StateInfo | undefined {
284- return {
309+ return this . _state && {
285310 group : this . _state . value ? this . _state . name : undefined ,
286311 name : this . _state . value || this . _state . name ,
287312 } ;
@@ -298,7 +323,7 @@ export class BlockPath {
298323 * Return a new BlockPath without the parent-most token.
299324 */
300325 childPath ( ) {
301- return BlockPath . from ( this . tokens . slice ( 1 ) ) ;
326+ return BlockPath . from ( this . tokens . slice ( this . _block . name ? 1 : 2 ) ) ;
302327 }
303328
304329 /**
0 commit comments