@@ -17,15 +17,18 @@ export interface StateToken {
17
17
namespace : string ;
18
18
name : string ;
19
19
value ?: string ;
20
+ quoted : boolean ;
20
21
}
21
22
22
23
type Token = BlockToken | ClassToken | StateToken ;
23
24
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 ;
29
32
30
33
const STATE_BEGIN = "[" ;
31
34
const STATE_END = "]" ;
@@ -43,7 +46,7 @@ export const ERRORS = {
43
46
noname : "Block path segments must include a valid name" ,
44
47
unclosedState : "Unclosed state selector" ,
45
48
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.` ,
47
50
expectsSepInsteadRec : ( c : string ) => `Expected separator tokens "[" or ".", instead found \`${ c } \`` ,
48
51
illegalCharNotInState : ( c : string ) => `Only state selectors may contain the \`${ c } \` character.` ,
49
52
illegalCharInState : ( c : string ) => `State selectors may not contain the \`${ c } \` character.` ,
@@ -75,6 +78,7 @@ class Walker {
75
78
76
79
next ( ) : string { return this . data [ this . idx ++ ] ; }
77
80
peek ( ) : string { return this . data [ this . idx ] ; }
81
+ index ( ) : number { return this . idx ; }
78
82
79
83
/**
80
84
* Consume all characters that do not match the provided Set or strings
@@ -101,23 +105,31 @@ export class BlockPath {
101
105
private _class : ClassToken ;
102
106
private _state : StateToken ;
103
107
108
+ private walker : Walker ;
104
109
private tokens : Token [ ] = [ ] ;
105
110
106
111
/**
107
112
* Throw a new BlockPathError with the given message.
108
113
* @param msg The error message.
109
114
*/
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 ) ;
112
124
}
113
125
114
126
/**
115
127
* Used by `tokenize` to insert a newly constructed token.
116
128
* @param token The token to insert.
117
129
*/
118
- private addToken ( token : Token ) : void {
130
+ private addToken ( token : Partial < Token > ) : void {
119
131
120
- // Final validation of incoming data.
132
+ // Final validation of incoming data. Blocks may have no name. States must have a namespace.
121
133
if ( ! isBlock ( token ) && ! hasName ( token ) ) { this . throw ( ERRORS . noname ) ; }
122
134
if ( isState ( token ) && ! hasNamespace ( token ) ) { this . throw ( ERRORS . namespace ) ; }
123
135
@@ -127,13 +139,17 @@ export class BlockPath {
127
139
}
128
140
if ( isClass ( token ) ) {
129
141
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 : "" } ) ; }
130
144
}
131
145
if ( isState ( token ) ) {
132
146
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" } ) ; }
133
149
}
134
150
135
151
// Add the token.
136
- this . tokens . push ( token ) ;
152
+ this . tokens . push ( token as Token ) ;
137
153
}
138
154
139
155
/**
@@ -144,8 +160,8 @@ export class BlockPath {
144
160
private tokenize ( str : string ) : void {
145
161
let char ,
146
162
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' } ;
149
165
150
166
while ( char = walker . next ( ) ) {
151
167
@@ -154,38 +170,27 @@ export class BlockPath {
154
170
// If a period, we've finished the previous token and are now building a class name.
155
171
case char === CLASS_BEGIN :
156
172
if ( isState ( token ) ) { this . throw ( ERRORS . illegalCharInState ( char ) ) ; }
173
+ if ( ! isIdent ( working ) ) { return this . throw ( ERRORS . invalidIdent ( working ) , working . length ) ; }
157
174
token . name = working ;
158
175
this . addToken ( token ) ;
159
- token = { type : 'class' , name : '' } ;
176
+ token = { type : 'class' } ;
160
177
working = "" ;
161
178
break ;
162
179
163
180
// If the beginning of a state, we've finished the previous token and are now building a state.
164
181
case char === STATE_BEGIN :
165
182
if ( isState ( token ) ) { this . throw ( ERRORS . illegalCharInState ( char ) ) ; }
183
+ if ( ! isIdent ( working ) ) { return this . throw ( ERRORS . invalidIdent ( working ) , working . length ) ; }
166
184
token . name = working ;
167
185
this . addToken ( token ) ;
168
- token = { type : 'state' , namespace : '' , name : '' } ;
186
+ token = { type : 'state' } ;
169
187
working = "" ;
170
188
break ;
171
189
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
-
186
190
// When we find a namespace terminator, set the namespace property of the state token we're working on.
187
191
case char === NAMESPACE_END :
188
192
if ( ! isState ( token ) ) { return this . throw ( ERRORS . illegalCharNotInState ( char ) ) ; }
193
+ if ( ! isIdent ( working ) ) { return this . throw ( ERRORS . invalidIdent ( working ) , working . length ) ; }
189
194
token . namespace = working ;
190
195
working = "" ;
191
196
break ;
@@ -194,6 +199,7 @@ export class BlockPath {
194
199
case char === VALUE_START :
195
200
if ( ! isState ( token ) ) { this . throw ( ERRORS . illegalCharNotInState ( char ) ) ; }
196
201
if ( ! working ) { this . throw ( ERRORS . noname ) ; }
202
+ if ( ! isIdent ( working ) ) { return this . throw ( ERRORS . invalidIdent ( working ) , working . length ) ; }
197
203
token . name = working ;
198
204
working = "" ;
199
205
break ;
@@ -202,10 +208,28 @@ export class BlockPath {
202
208
case char === SINGLE_QUOTE || char === DOUBLE_QUOTE :
203
209
if ( ! isState ( token ) ) { return this . throw ( ERRORS . illegalCharNotInState ( char ) ) ; }
204
210
working = walker . consume ( char ) ;
211
+ token . quoted = true ;
205
212
if ( walker . peek ( ) !== char ) { this . throw ( ERRORS . mismatchedQuote ) ; }
206
213
walker . next ( ) ; // Throw away the other quote
207
214
break ;
208
215
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
+
209
233
// We should never encounter whitespace in this switch statement.
210
234
// The only place whitespace is allowed is between quotes, which
211
235
// is handled above.
@@ -217,7 +241,6 @@ export class BlockPath {
217
241
// TODO: We need to handle invalid character escapes here!
218
242
default :
219
243
working += char ;
220
- if ( ! CSS_IDENT . test ( working ) ) { return this . throw ( ERRORS . illegalChar ( char ) ) ; }
221
244
222
245
}
223
246
@@ -228,11 +251,13 @@ export class BlockPath {
228
251
if ( isState ( token ) ) { this . throw ( ERRORS . unclosedState ) ; }
229
252
230
253
// 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.
232
255
if ( ! isState ( token ) && working ) {
256
+ if ( ! isIdent ( working ) ) { return this . throw ( ERRORS . invalidIdent ( working ) , working . length ) ; }
233
257
token . name = working ;
234
258
this . addToken ( token ) ;
235
259
}
260
+ if ( ! this . _class ) { this . addToken ( { type : "class" , name : "root" } ) ; }
236
261
}
237
262
238
263
/**
@@ -281,7 +306,7 @@ export class BlockPath {
281
306
* Get the parsed state name of this Block Path and return the `StateInfo`
282
307
*/
283
308
get state ( ) : StateInfo | undefined {
284
- return {
309
+ return this . _state && {
285
310
group : this . _state . value ? this . _state . name : undefined ,
286
311
name : this . _state . value || this . _state . name ,
287
312
} ;
@@ -298,7 +323,7 @@ export class BlockPath {
298
323
* Return a new BlockPath without the parent-most token.
299
324
*/
300
325
childPath ( ) {
301
- return BlockPath . from ( this . tokens . slice ( 1 ) ) ;
326
+ return BlockPath . from ( this . tokens . slice ( this . _block . name ? 1 : 2 ) ) ;
302
327
}
303
328
304
329
/**
0 commit comments