1
1
import type { Nullable , Recast } from '@glimmer/interfaces' ;
2
2
import type { TokenizerState } from 'simple-html-tokenizer' ;
3
- import { getLast , isPresentArray , unwrap } from '@glimmer/util' ;
3
+ import { assert , getLast , isPresentArray , unwrap } from '@glimmer/util' ;
4
4
5
5
import type { ParserNodeBuilder , Tag } from '../parser' ;
6
+ import type { SourceSpan } from '../source/span' ;
6
7
import type * as ASTv1 from '../v1/api' ;
7
8
import type * as HBS from '../v1/handlebars-ast' ;
8
9
@@ -24,46 +25,62 @@ export abstract class HandlebarsNodeVisitors extends Parser {
24
25
return this . elementStack . length === 0 ;
25
26
}
26
27
27
- Program ( program : HBS . Program ) : ASTv1 . Block ;
28
- Program ( program : HBS . Program ) : ASTv1 . Template ;
29
- Program ( program : HBS . Program ) : ASTv1 . Template | ASTv1 . Block ;
30
- Program ( program : HBS . Program ) : ASTv1 . Block | ASTv1 . Template {
31
- const body : ASTv1 . Statement [ ] = [ ] ;
32
- let node ;
33
-
34
- if ( this . isTopLevel ) {
35
- node = b . template ( {
36
- body,
37
- loc : this . source . spanFor ( program . loc ) ,
38
- } ) ;
39
- } else {
40
- node = b . blockItself ( {
41
- body,
42
- blockParams : program . blockParams ,
43
- chained : program . chained ,
44
- loc : this . source . spanFor ( program . loc ) ,
45
- } ) ;
46
- }
28
+ parse ( program : HBS . Program , locals : string [ ] ) : ASTv1 . Template {
29
+ let node = b . template ( {
30
+ body : [ ] ,
31
+ locals,
32
+ loc : this . source . spanFor ( program . loc ) ,
33
+ } ) ;
47
34
48
- let i ,
49
- l = program . body . length ;
35
+ return this . parseProgram ( node , program ) ;
36
+ }
50
37
51
- this . elementStack . push ( node ) ;
38
+ Program ( program : HBS . Program , blockParams ?: ASTv1 . VarHead [ ] ) : ASTv1 . Block {
39
+ // The abstract signature doesn't have the blockParams argument, but in
40
+ // practice we can only come from this.BlockStatement() which adds the
41
+ // extra argument for us
42
+ assert (
43
+ Array . isArray ( blockParams ) ,
44
+ '[BUG] Program in parser unexpectedly called without block params'
45
+ ) ;
52
46
53
- if ( l === 0 ) {
54
- return this . elementStack . pop ( ) as ASTv1 . Block | ASTv1 . Template ;
47
+ let node = b . blockItself ( {
48
+ body : [ ] ,
49
+ params : blockParams ,
50
+ chained : program . chained ,
51
+ loc : this . source . spanFor ( program . loc ) ,
52
+ } ) ;
53
+
54
+ return this . parseProgram ( node , program ) ;
55
+ }
56
+
57
+ private parseProgram < T extends ASTv1 . ParentNode > ( node : T , program : HBS . Program ) : T {
58
+ if ( program . body . length === 0 ) {
59
+ return node ;
55
60
}
56
61
57
- for ( i = 0 ; i < l ; i ++ ) {
58
- this . acceptNode ( unwrap ( program . body [ i ] ) ) ;
62
+ let poppedNode ;
63
+
64
+ try {
65
+ this . elementStack . push ( node ) ;
66
+
67
+ for ( let child of program . body ) {
68
+ this . acceptNode ( child ) ;
69
+ }
70
+ } finally {
71
+ poppedNode = this . elementStack . pop ( ) ;
59
72
}
60
73
61
74
// Ensure that that the element stack is balanced properly.
62
- const poppedNode = this . elementStack . pop ( ) ;
63
- if ( poppedNode !== node ) {
64
- const elementNode = poppedNode as ASTv1 . ElementNode ;
65
-
66
- throw generateSyntaxError ( `Unclosed element \`${ elementNode . tag } \`` , elementNode . loc ) ;
75
+ if ( node !== poppedNode ) {
76
+ if ( poppedNode ?. type === 'ElementNode' ) {
77
+ throw generateSyntaxError ( `Unclosed element \`${ poppedNode . tag } \`` , poppedNode . loc ) ;
78
+ } else {
79
+ // If the stack is not balanced, then it is likely our own bug, because
80
+ // any unclosed Handlebars blocks should already been caught by now
81
+ assert ( poppedNode !== undefined , '[BUG] empty parser elementStack' ) ;
82
+ assert ( false , `[BUG] mismatched parser elementStack node: ${ node . type } ` ) ;
83
+ }
67
84
}
68
85
69
86
return node ;
@@ -83,6 +100,66 @@ export abstract class HandlebarsNodeVisitors extends Parser {
83
100
}
84
101
85
102
const { path, params, hash } = acceptCallNodes ( this , block ) ;
103
+ const loc = this . source . spanFor ( block . loc ) ;
104
+
105
+ // Backfill block params loc for the default block
106
+ let blockParams : ASTv1 . VarHead [ ] = [ ] ;
107
+
108
+ if ( block . program . blockParams ?. length ) {
109
+ // Start from right after the hash
110
+ let span = hash . loc . collapse ( 'end' ) ;
111
+
112
+ // Extend till the beginning of the block
113
+ if ( block . program . loc ) {
114
+ span = span . withEnd ( this . source . spanFor ( block . program . loc ) . getStart ( ) ) ;
115
+ }
116
+ if ( block . program . body [ 0 ] ) {
117
+ span = span . withEnd ( this . source . spanFor ( block . program . body [ 0 ] . loc ) . getStart ( ) ) ;
118
+ } else {
119
+ // ...or if all else fail, use the end of the block statement
120
+ // this can only happen if the block statement is empty anyway
121
+ span = span . withEnd ( loc . getEnd ( ) ) ;
122
+ }
123
+
124
+ // Now we have a span for something like this:
125
+ //
126
+ // {{#foo bar baz=bat as |wow wat|}}
127
+ // ~~~~~~~~~~~~~~~
128
+ //
129
+ // Or, if we are unlucky:
130
+ //
131
+ // {{#foo bar baz=bat as |wow wat|}}{{/foo}}
132
+ // ~~~~~~~~~~~~~~~~~~~~~~~
133
+ //
134
+ // Either way, within this span, there should be exactly two pipes
135
+ // fencing our block params, neatly whitespace separated and with
136
+ // legal identifiers only
137
+ const content = span . asString ( ) ;
138
+ let skipStart = content . indexOf ( '|' ) + 1 ;
139
+ const limit = content . indexOf ( '|' , skipStart ) ;
140
+
141
+ for ( const name of block . program . blockParams ) {
142
+ let nameStart : number ;
143
+ let loc : SourceSpan ;
144
+
145
+ if ( skipStart >= limit ) {
146
+ nameStart = - 1 ;
147
+ } else {
148
+ nameStart = content . indexOf ( name , skipStart ) ;
149
+ }
150
+
151
+ if ( nameStart === - 1 || nameStart + name . length > limit ) {
152
+ skipStart = limit ;
153
+ loc = this . source . spanFor ( NON_EXISTENT_LOCATION ) ;
154
+ } else {
155
+ skipStart = nameStart ;
156
+ loc = span . sliceStartChars ( { skipStart, chars : name . length } ) ;
157
+ skipStart += name . length ;
158
+ }
159
+
160
+ blockParams . push ( b . var ( { name, loc } ) ) ;
161
+ }
162
+ }
86
163
87
164
// These are bugs in Handlebars upstream
88
165
if ( ! block . program . loc ) {
@@ -93,8 +170,8 @@ export abstract class HandlebarsNodeVisitors extends Parser {
93
170
block . inverse . loc = NON_EXISTENT_LOCATION ;
94
171
}
95
172
96
- const program = this . Program ( block . program ) ;
97
- const inverse = block . inverse ? this . Program ( block . inverse ) : null ;
173
+ const program = this . Program ( block . program , blockParams ) ;
174
+ const inverse = block . inverse ? this . Program ( block . inverse , [ ] ) : null ;
98
175
99
176
const node = b . block ( {
100
177
path,
@@ -126,7 +203,7 @@ export abstract class HandlebarsNodeVisitors extends Parser {
126
203
127
204
if ( isHBSLiteral ( rawMustache . path ) ) {
128
205
mustache = b . mustache ( {
129
- path : this . acceptNode < ASTv1 . Literal > ( rawMustache . path ) ,
206
+ path : this . acceptNode < ( typeof rawMustache . path ) [ 'type' ] > ( rawMustache . path ) ,
130
207
params : [ ] ,
131
208
hash : b . hash ( { pairs : [ ] , loc : this . source . spanFor ( rawMustache . path . loc ) . collapse ( 'end' ) } ) ,
132
209
trusting : ! escaped ,
@@ -388,7 +465,7 @@ export abstract class HandlebarsNodeVisitors extends Parser {
388
465
const pairs = hash . pairs . map ( ( pair ) =>
389
466
b . pair ( {
390
467
key : pair . key ,
391
- value : this . acceptNode ( pair . value ) ,
468
+ value : this . acceptNode < HBS . Expression [ 'type' ] > ( pair . value ) ,
392
469
loc : this . source . spanFor ( pair . loc ) ,
393
470
} )
394
471
) ;
@@ -536,7 +613,7 @@ function acceptCallNodes(
536
613
}
537
614
538
615
const params = node . params
539
- ? node . params . map ( ( e ) => compiler . acceptNode < ASTv1 . Expression > ( e ) )
616
+ ? node . params . map ( ( e ) => compiler . acceptNode < HBS . Expression [ 'type' ] > ( e ) )
540
617
: [ ] ;
541
618
542
619
// if there is no hash, position it as a collapsed node immediately after the last param (or the
0 commit comments