@@ -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.
@@ -73,11 +74,20 @@ export function createLexer(grammar: CstGrammar) {
7374 const tokens : Token [ ] = [ ] ;
7475 let pos = 0 ;
7576 const templateStack : number [ ] = [ ] ;
77+ // A line terminator was seen since the last emitted token (skipped whitespace
78+ // or comments). Stamped onto the next real token as `newlineBefore`, then
79+ // cleared — so the parser can honor "no LineTerminator here" restrictions
80+ // (e.g. an array/indexed-access type's `[` must be on the same line).
81+ let pendingNl = false ;
82+ function push ( t : Token ) : void {
83+ if ( pendingNl ) { t . newlineBefore = true ; pendingNl = false ; }
84+ tokens . push ( t ) ;
85+ }
7686
7787 while ( pos < source . length ) {
7888 // Skip whitespace
7989 const wsMatch = source . slice ( pos ) . match ( / ^ \s + / ) ;
80- if ( wsMatch ) { pos += wsMatch [ 0 ] . length ; continue ; }
90+ if ( wsMatch ) { if ( wsMatch [ 0 ] . includes ( '\n' ) ) pendingNl = true ; pos += wsMatch [ 0 ] . length ; continue ; }
8191
8292 // Close an interpolation hole (interpClose at baseline depth) → resume the template span.
8393 if ( templateStack . length > 0 && source . startsWith ( tplInterpClose , pos ) ) {
@@ -88,10 +98,10 @@ export function createLexer(grammar: CstGrammar) {
8898 pos += tplInterpClose . length ;
8999 const { endsWithInterp, end } = scanTemplateSpan ( source , pos ) ;
90100 if ( endsWithInterp ) {
91- tokens . push ( { type : '$templateMiddle' , text : source . slice ( startPos , end ) , offset : startPos } ) ;
101+ push ( { type : '$templateMiddle' , text : source . slice ( startPos , end ) , offset : startPos } ) ;
92102 templateStack . push ( 0 ) ;
93103 } else {
94- tokens . push ( { type : '$templateTail' , text : source . slice ( startPos , end ) , offset : startPos } ) ;
104+ push ( { type : '$templateTail' , text : source . slice ( startPos , end ) , offset : startPos } ) ;
95105 }
96106 pos = end ;
97107 continue ;
@@ -111,10 +121,10 @@ export function createLexer(grammar: CstGrammar) {
111121 pos += tplOpen . length ;
112122 const { endsWithInterp, end } = scanTemplateSpan ( source , pos ) ;
113123 if ( endsWithInterp ) {
114- tokens . push ( { type : '$templateHead' , text : source . slice ( startPos , end ) , offset : startPos } ) ;
124+ push ( { type : '$templateHead' , text : source . slice ( startPos , end ) , offset : startPos } ) ;
115125 templateStack . push ( 0 ) ;
116126 } else {
117- tokens . push ( { type : templateTokenName ! , text : source . slice ( startPos , end ) , offset : startPos } ) ;
127+ push ( { type : templateTokenName ! , text : source . slice ( startPos , end ) , offset : startPos } ) ;
118128 }
119129 pos = end ;
120130 continue ;
@@ -139,7 +149,9 @@ export function createLexer(grammar: CstGrammar) {
139149 const m = remaining . match ( tm . regex ) ;
140150 if ( m ) {
141151 if ( ! tm . skip ) {
142- tokens . push ( { type : tm . name , text : m [ 0 ] , offset : pos } ) ;
152+ push ( { type : tm . name , text : m [ 0 ] , offset : pos } ) ;
153+ } else if ( m [ 0 ] . includes ( '\n' ) ) {
154+ pendingNl = true ; // a skipped comment spanning a newline still terminates the previous line
143155 }
144156 pos += m [ 0 ] . length ;
145157 matched = true ;
@@ -151,7 +163,7 @@ export function createLexer(grammar: CstGrammar) {
151163 // Try punctuation literals (longest first)
152164 for ( const lit of punctLiterals ) {
153165 if ( remaining . startsWith ( lit ) ) {
154- tokens . push ( { type : '' , text : lit , offset : pos } ) ;
166+ push ( { type : '' , text : lit , offset : pos } ) ;
155167 pos += lit . length ;
156168 matched = true ;
157169 break ;
@@ -164,7 +176,7 @@ export function createLexer(grammar: CstGrammar) {
164176 // missed (e.g. accented or non-Latin names). Tagged with that token's name.
165177 const identMatch = remaining . match ( / ^ [ \p{ L} \p{ Nl} _ $ ] [ \p{ L} \p{ Nl} \p{ Nd} \p{ Mn} \p{ Mc} \p{ Pc} _ $ ] * / u) ;
166178 if ( identMatch ) {
167- tokens . push ( { type : identTokenName , text : identMatch [ 0 ] , offset : pos } ) ;
179+ push ( { type : identTokenName , text : identMatch [ 0 ] , offset : pos } ) ;
168180 pos += identMatch [ 0 ] . length ;
169181 matched = true ;
170182 }
0 commit comments