@@ -20,11 +20,22 @@ export enum BlockType {
2020 File
2121}
2222
23- const keywords = [ 'if' , 'while' , 'for' , 'do' , 'return' , 'function' , 'require' , 'end' ] ;
23+ const keywords = [ 'if' , 'while' , 'for' , 'do' , 'return' , 'function' , 'require' , 'end' , 'local' , 'else' , 'elseif' ] ;
2424function isKeyWord ( word : string ) : boolean {
2525 return keywords . includes ( word ) ;
2626}
2727
28+ function isIdentifierCharacter ( char : string ) : boolean {
29+ return / [ A - Z a - z 0 - 9 _ ] / . test ( char ) ;
30+ }
31+
32+ /**
33+ * Trims trailing spaces and tabs, but preserves newlines
34+ */
35+ function trimEndPreserveNewlines ( str : string ) : string {
36+ return str . replace ( / [ \t ] + $ / gm, '' ) ;
37+ }
38+
2839export abstract class CodeBlock {
2940 protected childBlocks : CodeBlock [ ] = [ ] ;
3041 private parentBlock ?: CodeBlock ;
@@ -66,7 +77,11 @@ export abstract class CodeBlock {
6677
6778 while ( leftCursor < fileContent . length ) {
6879 const currentChar : string = fileContent [ leftCursor ] ;
69- currentWord += currentChar ;
80+ if ( isIdentifierCharacter ( currentChar ) ) {
81+ currentWord += currentChar ;
82+ } else {
83+ currentWord = '' ;
84+ }
7085 currentBlockString += currentChar ;
7186 const nextChar = leftCursor < fileContent . length - 1 ? fileContent [ leftCursor + 1 ] : '' ;
7287
@@ -121,8 +136,8 @@ export abstract class CodeBlock {
121136 // }
122137 else if ( currentChar === "\n" ) {
123138 // Process line
124- currentBlockString = currentBlockString . trimEnd ( ) ; // Remove trailing whitespace
125- currentBlockString += '\n' ; // Add the newline back for the line block
139+ currentBlockString = trimEndPreserveNewlines ( currentBlockString ) ; // Remove trailing spaces/tabs
140+ // currentBlockString += '\n'; // Add the newline back for the line block
126141 if ( currentBlockString !== '' ) {
127142 const lineBlock = new CodeTextBlock ( lineCounter , currentBlockString , currentBlock ) ;
128143 currentBlock . childBlocks . push ( lineBlock ) ;
@@ -131,8 +146,8 @@ export abstract class CodeBlock {
131146 }
132147 }
133148 else if ( currentChar === ")" && currentBlock . blockType === BlockType . Require ) {
134- currentBlockString = currentBlockString . trimEnd ( ) ; // Remove trailing whitespace
135- currentBlockString += '\n' ; // Add the newline back for the line block as it will be skipped otherwise
149+ currentBlockString = trimEndPreserveNewlines ( currentBlockString ) ; // Remove trailing spaces/tabs
150+ // currentBlockString += '\n'; // Add the newline back for the line block as it will be skipped otherwise
136151 const lineBlock = new CodeTextBlock ( lineCounter , currentBlockString , currentBlock ) ;
137152 currentBlock . childBlocks . push ( lineBlock ) ;
138153 currentBlockString = '' ;
@@ -142,8 +157,8 @@ export abstract class CodeBlock {
142157 //Table blocks
143158 else if ( currentChar === "{" ) {
144159 // Only create LineBlock if there's content before the brace
145- if ( currentBlockString . trimEnd ( ) !== '' && currentBlockString . trimEnd ( ) !== '{' ) {
146- const currentLineBlock = new CodeTextBlock ( lineCounter , currentBlockString . slice ( 0 , - 1 ) . trimEnd ( ) , currentBlock ) ;
160+ if ( trimEndPreserveNewlines ( currentBlockString ) !== '' && trimEndPreserveNewlines ( currentBlockString ) !== '{' ) {
161+ const currentLineBlock = new CodeTextBlock ( lineCounter , trimEndPreserveNewlines ( currentBlockString . slice ( 0 , - 1 ) ) , currentBlock ) ;
147162 currentBlock . childBlocks . push ( currentLineBlock ) ;
148163 }
149164
@@ -174,8 +189,7 @@ export abstract class CodeBlock {
174189 }
175190
176191 if ( char === '\n' ) {
177- let block = currentBlockString . trimEnd ( ) ;
178- block += '\n' ;
192+ let block = trimEndPreserveNewlines ( currentBlockString ) ;
179193 const lineBlock = new CodeTextBlock ( lineCounter , block , currentBlock ) ;
180194 currentBlock . childBlocks . push ( lineBlock ) ;
181195 currentBlockString = '' ;
@@ -194,11 +208,13 @@ export abstract class CodeBlock {
194208 if ( tempCursor < fileContent . length && fileContent [ tempCursor ] === '\n' ) {
195209 currentBlockString += '\n' ;
196210 lineCounter ++ ;
197- } else {
198- leftCursor = tempCursor - 1 ; // Set leftCursor to the last character before the newline or non-whitespace character
211+ } else if ( tempCursor > leftCursor ) {
212+ // We found whitespace but no newline, so position cursor at last whitespace
213+ leftCursor = tempCursor - 1 ;
199214 }
215+ // else: no whitespace after table, leave leftCursor where it is
200216
201- if ( currentBlockString !== '' ) {
217+ if ( trimEndPreserveNewlines ( currentBlockString ) !== '' ) {
202218 const lineBlock = new CodeTextBlock ( lineCounter , currentBlockString , currentBlock ) ;
203219 currentBlock . childBlocks . push ( lineBlock ) ;
204220 currentBlockString = '' ;
@@ -208,10 +224,22 @@ export abstract class CodeBlock {
208224 currentWord = '' ; // Reset word accumulator after table block
209225 continue ; // Skip the leftCursor++ at the end of the loop since we already incremented it
210226 }
211- else if ( nextChar . trim ( ) === '' || nextChar === '(' ) {
227+ else if ( currentWord !== '' && ! isIdentifierCharacter ( nextChar ) ) {
212228 // End of a word, check for keywords
213229 const trimmedWord = currentWord . trim ( ) ;
230+
231+
232+ if ( currentBlock . blockType === BlockType . Return && isKeyWord ( trimmedWord ) && trimmedWord !== 'return' ) {
233+ // We're in a ReturnBlock and hit a keyword at the statement boundary
234+ // Extract return params and exit the block
235+ ( currentBlock as ReturnBlock ) . extractReturnParams ( ) ;
236+ currentBlock = currentBlock . getParentBlock ( ) ! ;
237+ // Don't clear currentBlockString - let normal flow handle the newline finalization
238+ // Continue to process this keyword normally
239+ }
240+
214241 let blockToAdd : CodeBlock | null = null ;
242+
215243 if ( trimmedWord === 'if' ) {
216244 blockToAdd = new IfBlock ( lineCounter , currentBlock ) ;
217245 }
@@ -222,20 +250,39 @@ export abstract class CodeBlock {
222250 blockToAdd = new ForBlock ( lineCounter , currentBlock ) ;
223251 }
224252 else if ( trimmedWord === 'do' ) {
225- blockToAdd = new DoBlock ( lineCounter , currentBlock ) ;
253+ const isForOrWhile = currentBlock . blockType === BlockType . While || currentBlock . blockType === BlockType . For ;
254+ if ( isForOrWhile && ( currentBlock as WhileBlock | ForBlock ) . passedDoStatement == false ) {
255+ // If we're in a While or For block and we've not yet passed a 'do' statement, mark it as passed and don't create a new block
256+ ( currentBlock as WhileBlock | ForBlock ) . passedDoStatement = true ;
257+ } else {
258+ blockToAdd = new DoBlock ( lineCounter , currentBlock ) ;
259+ }
226260 }
227261 else if ( trimmedWord === 'return' ) {
228- const line = new CodeTextBlock ( lineCounter , currentBlockString . trimEnd ( ) . slice ( 0 , - trimmedWord . length ) , currentBlock ) ;
229- currentBlock . childBlocks . push ( line ) ;
230- currentBlockString = 'return' ;
262+ // Add any text before 'return' to parent block, but preserve newlines
263+ const beforeReturn = currentBlockString . slice ( 0 , - trimmedWord . length ) ;
264+ if ( beforeReturn . trim ( ) ) {
265+ const line = new CodeTextBlock ( lineCounter , beforeReturn , currentBlock ) ;
266+ currentBlock . childBlocks . push ( line ) ;
267+ }
231268 blockToAdd = new ReturnBlock ( lineCounter , currentBlock ) ;
269+ // Start the return block content with 'return' keyword
270+ currentBlockString = trimmedWord ;
232271 }
233272 else if ( trimmedWord === 'function' ) {
234273 blockToAdd = new FunctionBlock ( lineCounter , currentBlock ) ;
235274 }
236275 else if ( trimmedWord === 'require' ) {
276+ const beforeRequire = currentBlockString . slice ( 0 , - trimmedWord . length ) ;
277+ if ( beforeRequire . trim ( ) ) {
278+ const line = new CodeTextBlock ( lineCounter , beforeRequire , currentBlock ) ;
279+ currentBlock . childBlocks . push ( line ) ;
280+ }
281+
237282 blockToAdd = new RequireBlock ( lineCounter , currentBlock , leftCursor - currentWord . length , leftCursor ) ;
238- } else if ( trimmedWord === 'end' ) {
283+ currentBlockString = trimmedWord ; // Start the require block content with 'require' keyword
284+ }
285+ else if ( trimmedWord === 'end' ) {
239286 const parent = currentBlock . getParentBlock ( ) ;
240287 if ( ! parent ) {
241288 onError ?.( {
@@ -257,6 +304,14 @@ export abstract class CodeBlock {
257304 }
258305 leftCursor ++ ;
259306 }
307+ // Handle case where file ends while in a ReturnBlock
308+ if ( currentBlock . blockType === BlockType . Return ) {
309+ if ( trimEndPreserveNewlines ( currentBlockString ) !== '' ) {
310+ const lineBlock = new CodeTextBlock ( lineCounter , currentBlockString , currentBlock ) ;
311+ currentBlock . childBlocks . push ( lineBlock ) ;
312+ }
313+ ( currentBlock as ReturnBlock ) . extractReturnParams ( ) ;
314+ }
260315 return file ;
261316 }
262317}
@@ -289,6 +344,8 @@ export class IfBlock extends CodeBlock {
289344
290345export class WhileBlock extends CodeBlock {
291346
347+ public passedDoStatement : boolean = false ;
348+
292349 constructor ( sourceLineNumber : number , parent : CodeBlock ) {
293350 super ( sourceLineNumber , BlockType . While , parent ) ;
294351 }
@@ -304,6 +361,8 @@ export class WhileBlock extends CodeBlock {
304361
305362export class ForBlock extends CodeBlock {
306363
364+ public passedDoStatement : boolean = false ;
365+
307366 constructor ( sourceLineNumber : number , parent : CodeBlock ) {
308367 super ( sourceLineNumber , BlockType . For , parent ) ;
309368 }
@@ -350,6 +409,8 @@ export class DoBlock extends CodeBlock {
350409
351410export class ReturnBlock extends CodeBlock {
352411
412+ public readonly returnParams : string [ ] = [ ] ;
413+
353414 constructor ( sourceLineNumber : number , parent : CodeBlock ) {
354415 super ( sourceLineNumber , BlockType . Return , parent ) ;
355416 }
@@ -361,6 +422,98 @@ export class ReturnBlock extends CodeBlock {
361422 }
362423 return lines ;
363424 }
425+
426+ /**
427+ * Extracts return parameters from the return statement.
428+ * Splits by commas while respecting nesting of {} and ().
429+ * Populates the returnParams array.
430+ */
431+ extractReturnParams ( ) : void {
432+ // Reconstruct the return content from all children
433+ let content = this . childBlocks
434+ . map ( child => {
435+ if ( child instanceof CodeTextBlock ) {
436+ return child . toLines ( ) [ 0 ] ;
437+ } else if ( child instanceof TableBlock ) {
438+ // For tables, use their full content
439+ return child . toLines ( ) . join ( '' ) ;
440+ } else {
441+ // For other block types, use their full content
442+ return child . toLines ( ) . join ( '' ) ;
443+ }
444+ } )
445+ . join ( '' )
446+ . trim ( ) ;
447+
448+ if ( ! content ) {
449+ this . returnParams . length = 0 ;
450+ return ;
451+ }
452+
453+ content = content . replace ( / ^ r e t u r n \s + / , '' ) . trim ( ) ; // Remove the 'return' keyword if present
454+
455+ // Split by commas while respecting nesting
456+ const params : string [ ] = [ ] ;
457+ let currentParam = '' ;
458+ let braceDepth = 0 ;
459+ let parenDepth = 0 ;
460+
461+ for ( let i = 0 ; i < content . length ; i ++ ) {
462+ const char = content [ i ] ;
463+ const nextChar = i < content . length - 1 ? content [ i + 1 ] : '' ;
464+
465+ // Skip strings to avoid counting delimiters inside them
466+ if ( char === '"' || char === "'" ) {
467+ const quoteType = char ;
468+ currentParam += char ;
469+ i ++ ;
470+ while ( i < content . length && content [ i ] !== quoteType ) {
471+ if ( content [ i ] === '\\' && i + 1 < content . length ) {
472+ currentParam += content [ i ] ;
473+ i ++ ;
474+ currentParam += content [ i ] ;
475+ } else {
476+ currentParam += content [ i ] ;
477+ }
478+ i ++ ;
479+ }
480+ if ( i < content . length ) {
481+ currentParam += content [ i ] ;
482+ }
483+ continue ;
484+ }
485+
486+ // Track nesting depth
487+ if ( char === '{' ) {
488+ braceDepth ++ ;
489+ } else if ( char === '}' ) {
490+ braceDepth -- ;
491+ } else if ( char === '(' ) {
492+ parenDepth ++ ;
493+ } else if ( char === ')' ) {
494+ parenDepth -- ;
495+ } else if ( char === ',' && braceDepth === 0 && parenDepth === 0 ) {
496+ // This is a param separator
497+ const param = currentParam . trim ( ) ;
498+ if ( param ) {
499+ params . push ( param ) ;
500+ }
501+ currentParam = '' ;
502+ continue ;
503+ }
504+
505+ currentParam += char ;
506+ }
507+
508+ // Add the last parameter
509+ const lastParam = currentParam . trim ( ) ;
510+ if ( lastParam ) {
511+ params . push ( lastParam ) ;
512+ }
513+
514+ this . returnParams . length = 0 ;
515+ this . returnParams . push ( ...params ) ;
516+ }
364517}
365518
366519export class TableBlock extends CodeBlock {
0 commit comments