@@ -206,8 +206,14 @@ class ExprParser {
206
206
} ,
207
207
} ;
208
208
const match = tok . name === 'text' ? tok . contents . match ( tokMatcher ) : null ;
209
+ // the `tok.name !== 'text'` part in the test below is redundant but makes TS happier
209
210
if ( tok . name !== 'text' || match == null ) {
210
- if ( ! ( tok . name === 'text' && tok . contents . length === 0 ) ) {
211
+ const empty =
212
+ ( tok . name === 'text' && tok . contents . length === 0 ) ||
213
+ tok . name === 'tag' ||
214
+ tok . name === 'opaqueTag' ||
215
+ tok . name === 'comment' ;
216
+ if ( ! empty ) {
211
217
currentProse . push ( tok ) ;
212
218
}
213
219
++ this . srcIndex ;
@@ -241,6 +247,36 @@ class ExprParser {
241
247
} ) ;
242
248
}
243
249
250
+ // returns true if this ate a newline
251
+ private eatWhitespace ( ) : boolean {
252
+ let next ;
253
+ let hadNewline = false ;
254
+ while ( ( next = this . peek ( ) ) ?. type === 'prose' ) {
255
+ const firstNonWhitespaceIdx = next . parts . findIndex (
256
+ part => part . name !== 'text' || / \S / . test ( part . contents )
257
+ ) ;
258
+ if ( firstNonWhitespaceIdx === - 1 ) {
259
+ hadNewline ||= next . parts . some (
260
+ part => part . name === 'text' && part . contents . includes ( '\n' )
261
+ ) ;
262
+ this . next . shift ( ) ;
263
+ } else if ( firstNonWhitespaceIdx === 0 ) {
264
+ return hadNewline ;
265
+ } else {
266
+ hadNewline ||= next . parts . some (
267
+ ( part , i ) =>
268
+ i < firstNonWhitespaceIdx && part . name === 'text' && part . contents . includes ( '\n' )
269
+ ) ;
270
+ this . next [ 0 ] = {
271
+ type : 'prose' ,
272
+ parts : next . parts . slice ( firstNonWhitespaceIdx ) ,
273
+ } ;
274
+ return hadNewline ;
275
+ }
276
+ }
277
+ return hadNewline ;
278
+ }
279
+
244
280
// guarantees the next token is an element of close
245
281
parseSeq ( close : CloseTokenType [ ] ) : Seq {
246
282
const items : NonSeq [ ] = [ ] ;
@@ -421,7 +457,20 @@ class ExprParser {
421
457
let type : 'record' | 'record-spec' | null = null ;
422
458
const members : ( { name : string ; value : Seq } | { name : string } ) [ ] = [ ] ;
423
459
while ( true ) {
460
+ const hadNewline = this . eatWhitespace ( ) ;
424
461
const nextTok = this . peek ( ) ;
462
+ if ( nextTok . type === 'crec' ) {
463
+ if ( ! hadNewline ) {
464
+ // ideally this would be a lint failure, or better yet a formatting thing, but whatever
465
+ throw new ParseFailure (
466
+ members . length > 0
467
+ ? 'trailing commas are only allowed when followed by a newline'
468
+ : 'records cannot be empty' ,
469
+ nextTok . offset
470
+ ) ;
471
+ }
472
+ break ;
473
+ }
425
474
if ( nextTok . type !== 'prose' ) {
426
475
throw new ParseFailure ( 'expected to find record field name' , nextTok . offset ) ;
427
476
}
@@ -434,14 +483,6 @@ class ExprParser {
434
483
const { contents } = nextTok . parts [ 0 ] ;
435
484
const nameMatch = contents . match ( / ^ \s * \[ \[ (?< name > \w + ) \] \] \s * (?< colon > : ? ) / ) ;
436
485
if ( nameMatch == null ) {
437
- if ( members . length > 0 && / ^ \s * $ / . test ( contents ) && contents . includes ( '\n' ) ) {
438
- // allow trailing commas when followed by a newline
439
- this . next . shift ( ) ; // eat the whitespace
440
- if ( this . peek ( ) . type === 'crec' ) {
441
- this . next . shift ( ) ;
442
- break ;
443
- }
444
- }
445
486
throw new ParseFailure (
446
487
'expected to find record field' ,
447
488
nextTok . parts [ 0 ] . location . start . offset + contents . match ( / ^ \s * / ) ! [ 0 ] . length
@@ -581,6 +622,8 @@ class ExprParser {
581
622
}
582
623
}
583
624
625
+ // Note: this does not necessarily represent the entire input
626
+ // in particular it may omit some whitespace, tags, and comments
584
627
export function parse ( src : FragmentNode [ ] , opNames : Set < String > ) : Seq | Failure {
585
628
const parser = new ExprParser ( src , opNames ) ;
586
629
try {
0 commit comments