Fixed keywords and end blocks
This commit is contained in:
+172
-19
@@ -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 {
|
||||
|
||||
@@ -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++;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user