Fixed keywords and end blocks

This commit is contained in:
dutchie031
2026-07-08 15:48:16 +02:00
parent df79538491
commit 377284ac59
2 changed files with 266 additions and 25 deletions
+172 -19
View File
@@ -20,11 +20,22 @@ export enum BlockType {
File
}
const keywords = ['if', 'while', 'for', 'do', 'return', 'function', 'require', 'end'];
const keywords = ['if', 'while', 'for', 'do', 'return', 'function', 'require', 'end', 'local', 'else', 'elseif'];
function isKeyWord(word: string): boolean {
return keywords.includes(word);
}
function isIdentifierCharacter(char: string): boolean {
return /[A-Za-z0-9_]/.test(char);
}
/**
* Trims trailing spaces and tabs, but preserves newlines
*/
function trimEndPreserveNewlines(str: string): string {
return str.replace(/[ \t]+$/gm, '');
}
export abstract class CodeBlock {
protected childBlocks: CodeBlock[] = [];
private parentBlock?: CodeBlock;
@@ -66,7 +77,11 @@ export abstract class CodeBlock {
while (leftCursor < fileContent.length) {
const currentChar: string = fileContent[leftCursor];
currentWord += currentChar;
if (isIdentifierCharacter(currentChar)) {
currentWord += currentChar;
} else {
currentWord = '';
}
currentBlockString += currentChar;
const nextChar = leftCursor < fileContent.length - 1 ? fileContent[leftCursor + 1] : '';
@@ -121,8 +136,8 @@ export abstract class CodeBlock {
// }
else if (currentChar === "\n") {
// Process line
currentBlockString = currentBlockString.trimEnd(); // Remove trailing whitespace
currentBlockString += '\n'; // Add the newline back for the line block
currentBlockString = trimEndPreserveNewlines(currentBlockString); // Remove trailing spaces/tabs
//currentBlockString += '\n'; // Add the newline back for the line block
if (currentBlockString !== '') {
const lineBlock = new CodeTextBlock(lineCounter, currentBlockString, currentBlock);
currentBlock.childBlocks.push(lineBlock);
@@ -131,8 +146,8 @@ export abstract class CodeBlock {
}
}
else if (currentChar === ")" && currentBlock.blockType === BlockType.Require) {
currentBlockString = currentBlockString.trimEnd(); // Remove trailing whitespace
currentBlockString += '\n'; // Add the newline back for the line block as it will be skipped otherwise
currentBlockString = trimEndPreserveNewlines(currentBlockString); // Remove trailing spaces/tabs
//currentBlockString += '\n'; // Add the newline back for the line block as it will be skipped otherwise
const lineBlock = new CodeTextBlock(lineCounter, currentBlockString, currentBlock);
currentBlock.childBlocks.push(lineBlock);
currentBlockString = '';
@@ -142,8 +157,8 @@ export abstract class CodeBlock {
//Table blocks
else if (currentChar === "{") {
// Only create LineBlock if there's content before the brace
if (currentBlockString.trimEnd() !== '' && currentBlockString.trimEnd() !== '{') {
const currentLineBlock = new CodeTextBlock(lineCounter, currentBlockString.slice(0, -1).trimEnd(), currentBlock);
if (trimEndPreserveNewlines(currentBlockString) !== '' && trimEndPreserveNewlines(currentBlockString) !== '{') {
const currentLineBlock = new CodeTextBlock(lineCounter, trimEndPreserveNewlines(currentBlockString.slice(0, -1)), currentBlock);
currentBlock.childBlocks.push(currentLineBlock);
}
@@ -174,8 +189,7 @@ export abstract class CodeBlock {
}
if (char === '\n') {
let block = currentBlockString.trimEnd();
block += '\n';
let block = trimEndPreserveNewlines(currentBlockString);
const lineBlock = new CodeTextBlock(lineCounter, block, currentBlock);
currentBlock.childBlocks.push(lineBlock);
currentBlockString = '';
@@ -194,11 +208,13 @@ export abstract class CodeBlock {
if (tempCursor < fileContent.length && fileContent[tempCursor] === '\n') {
currentBlockString += '\n';
lineCounter++;
} else {
leftCursor = tempCursor - 1; // Set leftCursor to the last character before the newline or non-whitespace character
} else if (tempCursor > leftCursor) {
// We found whitespace but no newline, so position cursor at last whitespace
leftCursor = tempCursor - 1;
}
// else: no whitespace after table, leave leftCursor where it is
if (currentBlockString !== '') {
if (trimEndPreserveNewlines(currentBlockString) !== '') {
const lineBlock = new CodeTextBlock(lineCounter, currentBlockString, currentBlock);
currentBlock.childBlocks.push(lineBlock);
currentBlockString = '';
@@ -208,10 +224,22 @@ export abstract class CodeBlock {
currentWord = ''; // Reset word accumulator after table block
continue; // Skip the leftCursor++ at the end of the loop since we already incremented it
}
else if (nextChar.trim() === '' || nextChar === '(') {
else if (currentWord !== '' && !isIdentifierCharacter(nextChar)) {
// End of a word, check for keywords
const trimmedWord = currentWord.trim();
if (currentBlock.blockType === BlockType.Return && isKeyWord(trimmedWord) && trimmedWord !== 'return') {
// We're in a ReturnBlock and hit a keyword at the statement boundary
// Extract return params and exit the block
(currentBlock as ReturnBlock).extractReturnParams();
currentBlock = currentBlock.getParentBlock()!;
// Don't clear currentBlockString - let normal flow handle the newline finalization
// Continue to process this keyword normally
}
let blockToAdd: CodeBlock | null = null;
if (trimmedWord === 'if') {
blockToAdd = new IfBlock(lineCounter, currentBlock);
}
@@ -222,20 +250,39 @@ export abstract class CodeBlock {
blockToAdd = new ForBlock(lineCounter, currentBlock);
}
else if (trimmedWord === 'do') {
blockToAdd = new DoBlock(lineCounter, currentBlock);
const isForOrWhile = currentBlock.blockType === BlockType.While || currentBlock.blockType === BlockType.For;
if(isForOrWhile && (currentBlock as WhileBlock | ForBlock).passedDoStatement == false) {
// 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
(currentBlock as WhileBlock | ForBlock).passedDoStatement = true;
} else {
blockToAdd = new DoBlock(lineCounter, currentBlock);
}
}
else if (trimmedWord === 'return') {
const line = new CodeTextBlock(lineCounter, currentBlockString.trimEnd().slice(0, -trimmedWord.length), currentBlock);
currentBlock.childBlocks.push(line);
currentBlockString = 'return';
// Add any text before 'return' to parent block, but preserve newlines
const beforeReturn = currentBlockString.slice(0, -trimmedWord.length);
if (beforeReturn.trim()) {
const line = new CodeTextBlock(lineCounter, beforeReturn, currentBlock);
currentBlock.childBlocks.push(line);
}
blockToAdd = new ReturnBlock(lineCounter, currentBlock);
// Start the return block content with 'return' keyword
currentBlockString = trimmedWord;
}
else if (trimmedWord === 'function') {
blockToAdd = new FunctionBlock(lineCounter, currentBlock);
}
else if (trimmedWord === 'require') {
const beforeRequire = currentBlockString.slice(0, -trimmedWord.length);
if (beforeRequire.trim()) {
const line = new CodeTextBlock(lineCounter, beforeRequire, currentBlock);
currentBlock.childBlocks.push(line);
}
blockToAdd = new RequireBlock(lineCounter, currentBlock, leftCursor - currentWord.length, leftCursor);
} else if (trimmedWord === 'end') {
currentBlockString = trimmedWord; // Start the require block content with 'require' keyword
}
else if (trimmedWord === 'end') {
const parent = currentBlock.getParentBlock();
if (!parent) {
onError?.({
@@ -257,6 +304,14 @@ export abstract class CodeBlock {
}
leftCursor++;
}
// Handle case where file ends while in a ReturnBlock
if (currentBlock.blockType === BlockType.Return) {
if (trimEndPreserveNewlines(currentBlockString) !== '') {
const lineBlock = new CodeTextBlock(lineCounter, currentBlockString, currentBlock);
currentBlock.childBlocks.push(lineBlock);
}
(currentBlock as ReturnBlock).extractReturnParams();
}
return file;
}
}
@@ -289,6 +344,8 @@ export class IfBlock extends CodeBlock {
export class WhileBlock extends CodeBlock {
public passedDoStatement: boolean = false;
constructor(sourceLineNumber: number, parent: CodeBlock) {
super(sourceLineNumber, BlockType.While, parent);
}
@@ -304,6 +361,8 @@ export class WhileBlock extends CodeBlock {
export class ForBlock extends CodeBlock {
public passedDoStatement: boolean = false;
constructor(sourceLineNumber: number, parent: CodeBlock) {
super(sourceLineNumber, BlockType.For, parent);
}
@@ -350,6 +409,8 @@ export class DoBlock extends CodeBlock {
export class ReturnBlock extends CodeBlock {
public readonly returnParams: string[] = [];
constructor(sourceLineNumber: number, parent: CodeBlock) {
super(sourceLineNumber, BlockType.Return, parent);
}
@@ -361,6 +422,98 @@ export class ReturnBlock extends CodeBlock {
}
return lines;
}
/**
* Extracts return parameters from the return statement.
* Splits by commas while respecting nesting of {} and ().
* Populates the returnParams array.
*/
extractReturnParams(): void {
// Reconstruct the return content from all children
let content = this.childBlocks
.map(child => {
if (child instanceof CodeTextBlock) {
return child.toLines()[0];
} else if (child instanceof TableBlock) {
// For tables, use their full content
return child.toLines().join('');
} else {
// For other block types, use their full content
return child.toLines().join('');
}
})
.join('')
.trim();
if (!content) {
this.returnParams.length = 0;
return;
}
content = content.replace(/^return\s+/, '').trim(); // Remove the 'return' keyword if present
// Split by commas while respecting nesting
const params: string[] = [];
let currentParam = '';
let braceDepth = 0;
let parenDepth = 0;
for (let i = 0; i < content.length; i++) {
const char = content[i];
const nextChar = i < content.length - 1 ? content[i + 1] : '';
// Skip strings to avoid counting delimiters inside them
if (char === '"' || char === "'") {
const quoteType = char;
currentParam += char;
i++;
while (i < content.length && content[i] !== quoteType) {
if (content[i] === '\\' && i + 1 < content.length) {
currentParam += content[i];
i++;
currentParam += content[i];
} else {
currentParam += content[i];
}
i++;
}
if (i < content.length) {
currentParam += content[i];
}
continue;
}
// Track nesting depth
if (char === '{') {
braceDepth++;
} else if (char === '}') {
braceDepth--;
} else if (char === '(') {
parenDepth++;
} else if (char === ')') {
parenDepth--;
} else if (char === ',' && braceDepth === 0 && parenDepth === 0) {
// This is a param separator
const param = currentParam.trim();
if (param) {
params.push(param);
}
currentParam = '';
continue;
}
currentParam += char;
}
// Add the last parameter
const lastParam = currentParam.trim();
if (lastParam) {
params.push(lastParam);
}
this.returnParams.length = 0;
this.returnParams.push(...params);
}
}
export class TableBlock extends CodeBlock {
+94 -6
View File
@@ -1,6 +1,6 @@
import * as fs from 'fs';
import * as path from 'path';
import { LuaFile, BlockType, CodeBlock, RequireBlock } from './CodeBlocks';
import { LuaFile, BlockType, CodeBlock, RequireBlock, ReturnBlock, FunctionBlock, TableBlock } from './CodeBlocks';
import { CompilationError } from './CompilationError';
export interface ScriptCompilerOptions {
@@ -130,7 +130,7 @@ function fileReferenceToLuaVariable(fileReference: string): string {
// Split by / to get path parts
const parts = fileReference.split('/');
// Convert to ScriptGlobals.folder.FileName format
let result = LUA_SCRIPT_GLOBAL_KEYWORD;
for (let i = 0; i < parts.length; i++) {
@@ -174,6 +174,64 @@ class ParsedFile {
searchBlock(luaFile);
return dependencies;
}
public replaceRequireWithGlobal(): void {
function replaceRequireInBlock(block: CodeBlock) {
if (block instanceof RequireBlock) {
const requiredString = block.getRequiredString();
if (requiredString) {
const luaReference = fileReferenceToLuaVariable(requiredString);
block.toLines = () => [`${luaReference}\n`];
}
}
for (const child of block.getChildren()) {
replaceRequireInBlock(child);
}
}
for (const child of this.luaFile.getChildren()) {
replaceRequireInBlock(child);
}
}
public replaceModuleReturnWithGlobalAssignement(): void {
const replaceReturnInBlock = (block: CodeBlock) => {
if (block instanceof ReturnBlock) {
const parent = block.getParentBlock();
if (parent) {
const luaReference = fileReferenceToLuaVariable(this.fileKey);
const resultLines : string[] = [];
const splitCount = luaReference.split('.').length;
for(let i = 2; i <= splitCount -1 ; i++){
const partialReference = luaReference.split('.').slice(0, i).join('.');
resultLines.push(`if not ${partialReference} then ${partialReference} = {} end`);
}
resultLines.push(`${luaReference} = ${block.returnParams.join(', ')}`);
block.toLines = () => resultLines.map(line => line + '\n');
}
}
if(block instanceof FunctionBlock || block instanceof TableBlock){
return; // Do not traverse into FunctionBlock or TableBlock
}
for (const child of block.getChildren()) {
replaceReturnInBlock(child);
}
}
for (const child of this.luaFile.getChildren()) {
replaceReturnInBlock(child);
}
}
}
class Writer {
@@ -291,11 +349,41 @@ class Writer {
}
}
outputLines.push("do -- " + parsedFile.fileKey + "\n");
parsedFile.replaceRequireWithGlobal();
parsedFile.replaceModuleReturnWithGlobalAssignement();
const lines = parsedFile.luaFile.toLines();
metrics.totalLinesWritten += lines.length;
outputLines.push(...lines.filter(line => line.trim() !== '')); // Filter out empty lines
outputLines.push("end -- " + parsedFile.fileKey + "\n");
const newLineFilteredLines : string[] = [];
function trimEndPreserveNewlines(str: string): string {
return str.replace(/[ \t]+$/gm, '');
}
let wasLastEmpty = false;
let lastEndedWithNewline = false;
for(const line of lines){
if(trimEndPreserveNewlines(line) !== ''){
if(line.trim() === ''){
if(!wasLastEmpty && !lastEndedWithNewline){
newLineFilteredLines.push(line);
wasLastEmpty = true;
}
} else {
newLineFilteredLines.push(line);
wasLastEmpty = false;
lastEndedWithNewline = line.endsWith('\n');
}
}
}
outputLines.push("do -- " + parsedFile.fileKey + "\n");
metrics.totalLinesWritten += newLineFilteredLines.length;
outputLines.push(...newLineFilteredLines);
outputLines.push("\nend -- " + parsedFile.fileKey + "\n");
writtenFiles.add(parsedFile.fileKey);
metrics.filesWritten++;
};