@@ -8,6 +8,7 @@ export interface Token {
88 type : string ; // token decl name (e.g. 'Ident'), or '' for punctuation literals
99 text : string ;
1010 offset : number ;
11+ newlineBefore ?: boolean ; // a line terminator preceded this token (drives ASI / "no LineTerminator here" rules)
1112}
1213
1314// Build a standalone lexer from the grammar's token definitions + lexer hints.
@@ -84,11 +85,20 @@ export function createLexer(grammar: CstGrammar) {
8485 // that to the regex-vs-division check (consulted only when prev is `)`).
8586 const parenHeadStack : boolean [ ] = [ ] ;
8687 let lastCloseWasParenHead = false ;
88+ // A line terminator was seen since the last emitted token (skipped whitespace
89+ // or comments). Stamped onto the next real token as `newlineBefore`, then
90+ // cleared — so the parser can honor "no LineTerminator here" restrictions
91+ // (e.g. an array/indexed-access type's `[` must be on the same line).
92+ let pendingNl = false ;
93+ function push ( t : Token ) : void {
94+ if ( pendingNl ) { t . newlineBefore = true ; pendingNl = false ; }
95+ tokens . push ( t ) ;
96+ }
8797
8898 while ( pos < source . length ) {
8999 // Skip whitespace
90100 const wsMatch = source . slice ( pos ) . match ( / ^ \s + / ) ;
91- if ( wsMatch ) { pos += wsMatch [ 0 ] . length ; continue ; }
101+ if ( wsMatch ) { if ( wsMatch [ 0 ] . includes ( '\n' ) ) pendingNl = true ; pos += wsMatch [ 0 ] . length ; continue ; }
92102
93103 // Close an interpolation hole (interpClose at baseline depth) → resume the template span.
94104 if ( templateStack . length > 0 && source . startsWith ( tplInterpClose , pos ) ) {
@@ -99,10 +109,10 @@ export function createLexer(grammar: CstGrammar) {
99109 pos += tplInterpClose . length ;
100110 const { endsWithInterp, end } = scanTemplateSpan ( source , pos ) ;
101111 if ( endsWithInterp ) {
102- tokens . push ( { type : '$templateMiddle' , text : source . slice ( startPos , end ) , offset : startPos } ) ;
112+ push ( { type : '$templateMiddle' , text : source . slice ( startPos , end ) , offset : startPos } ) ;
103113 templateStack . push ( 0 ) ;
104114 } else {
105- tokens . push ( { type : '$templateTail' , text : source . slice ( startPos , end ) , offset : startPos } ) ;
115+ push ( { type : '$templateTail' , text : source . slice ( startPos , end ) , offset : startPos } ) ;
106116 }
107117 pos = end ;
108118 continue ;
@@ -122,10 +132,10 @@ export function createLexer(grammar: CstGrammar) {
122132 pos += tplOpen . length ;
123133 const { endsWithInterp, end } = scanTemplateSpan ( source , pos ) ;
124134 if ( endsWithInterp ) {
125- tokens . push ( { type : '$templateHead' , text : source . slice ( startPos , end ) , offset : startPos } ) ;
135+ push ( { type : '$templateHead' , text : source . slice ( startPos , end ) , offset : startPos } ) ;
126136 templateStack . push ( 0 ) ;
127137 } else {
128- tokens . push ( { type : templateTokenName ! , text : source . slice ( startPos , end ) , offset : startPos } ) ;
138+ push ( { type : templateTokenName ! , text : source . slice ( startPos , end ) , offset : startPos } ) ;
129139 }
130140 pos = end ;
131141 continue ;
@@ -152,7 +162,9 @@ export function createLexer(grammar: CstGrammar) {
152162 const m = remaining . match ( tm . regex ) ;
153163 if ( m ) {
154164 if ( ! tm . skip ) {
155- tokens . push ( { type : tm . name , text : m [ 0 ] , offset : pos } ) ;
165+ push ( { type : tm . name , text : m [ 0 ] , offset : pos } ) ;
166+ } else if ( m [ 0 ] . includes ( '\n' ) ) {
167+ pendingNl = true ; // a skipped comment spanning a newline still terminates the previous line
156168 }
157169 pos += m [ 0 ] . length ;
158170 matched = true ;
@@ -178,7 +190,7 @@ export function createLexer(grammar: CstGrammar) {
178190 } else if ( lit === ')' ) {
179191 lastCloseWasParenHead = parenHeadStack . pop ( ) ?? false ;
180192 }
181- tokens . push ( { type : '' , text : lit , offset : pos } ) ;
193+ push ( { type : '' , text : lit , offset : pos } ) ;
182194 pos += lit . length ;
183195 matched = true ;
184196 break ;
@@ -191,7 +203,7 @@ export function createLexer(grammar: CstGrammar) {
191203 // missed (e.g. accented or non-Latin names). Tagged with that token's name.
192204 const identMatch = remaining . match ( / ^ [ \p{ L} \p{ Nl} _ $ ] [ \p{ L} \p{ Nl} \p{ Nd} \p{ Mn} \p{ Mc} \p{ Pc} _ $ ] * / u) ;
193205 if ( identMatch ) {
194- tokens . push ( { type : identTokenName , text : identMatch [ 0 ] , offset : pos } ) ;
206+ push ( { type : identTokenName , text : identMatch [ 0 ] , offset : pos } ) ;
195207 pos += identMatch [ 0 ] . length ;
196208 matched = true ;
197209 }
0 commit comments