This patch fixes the Lua arithmetic model at parser level only. Runtime level fixtures are left for future work. MOTIVATION Space Lua treats all numbers as IEEE-754 doubles (JS semantics), so it preserves negative zero. Lua uses integers and floats. The literal "-0" in Lua is parsed as unary minus applied to the integer literal "0", which yields the integer "0" (no sign). This difference flips the sign of the divisor in "1/-0" and "-1/-0". Although it could be fixed without touching the parser by changing only unary minus evaluation, this commit goes a little further and provides integer vs float differenciating at the parser level as well uses it to fix the unary minus evaluation. Extensive tests are added to test this exact change works and the Space Lua arithmetic model is aligned with Lua. Note that things like `-(expr)` in general may still produce results inconsistent with Lua. This cannot be fixed at parser level and must be fixed at runtime level which will need far more complex change to the Space Lua interpreter code. Therefor these cases we omitted from the tests. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz>
770 lines
22 KiB
TypeScript
770 lines
22 KiB
TypeScript
import { lezerToParseTree } from "../../client/markdown_parser/parse_tree.ts";
|
|
import {
|
|
cleanTree,
|
|
type ParseTree,
|
|
} from "@silverbulletmd/silverbullet/lib/tree";
|
|
import { parser } from "./parse-lua.js";
|
|
import { styleTags, tags as t } from "@lezer/highlight";
|
|
import { indentNodeProp, LRLanguage } from "@codemirror/language";
|
|
import type {
|
|
ASTCtx,
|
|
LuaAttName,
|
|
LuaBlock,
|
|
LuaExpression,
|
|
LuaFunctionBody,
|
|
LuaFunctionCallExpression,
|
|
LuaFunctionCallStatement,
|
|
LuaFunctionName,
|
|
LuaLValue,
|
|
LuaOrderBy,
|
|
LuaPrefixExpression,
|
|
LuaQueryClause,
|
|
LuaStatement,
|
|
LuaTableField,
|
|
} from "./ast.ts";
|
|
|
|
const luaStyleTags = styleTags({
|
|
Name: t.variableName,
|
|
LiteralString: t.string,
|
|
Number: t.number,
|
|
CompareOp: t.operator,
|
|
"true false": t.bool,
|
|
Comment: t.lineComment,
|
|
"return break goto do end while repeat until function local if then else elseif in for nil or and not query from where limit select order by desc":
|
|
t.keyword,
|
|
});
|
|
|
|
const customIndent = indentNodeProp.add({
|
|
"IfStatement FuncBody WhileStatement ForStatement TableConstructor": (
|
|
context,
|
|
) => {
|
|
return context.lineIndent(context.node.from) + context.unit;
|
|
},
|
|
});
|
|
|
|
// Use the customIndent in your language support
|
|
export const luaLanguage = LRLanguage.define({
|
|
name: "space-lua",
|
|
parser: parser.configure({
|
|
props: [
|
|
luaStyleTags,
|
|
customIndent,
|
|
],
|
|
}),
|
|
});
|
|
|
|
function context(t: ParseTree, ctx: Record<string, any>): ASTCtx {
|
|
return { ...ctx, from: t.from, to: t.to };
|
|
}
|
|
|
|
function parseChunk(t: ParseTree, ctx: ASTCtx): LuaBlock {
|
|
if (t.type !== "Chunk") {
|
|
throw new Error(`Expected Chunk, got ${t.type}`);
|
|
}
|
|
return parseBlock(t.children![0], ctx);
|
|
}
|
|
|
|
function parseBlock(t: ParseTree, ctx: ASTCtx): LuaBlock {
|
|
if (t.type !== "Block") {
|
|
throw new Error(`Expected Block, got ${t.type}`);
|
|
}
|
|
const statements = t.children!.map((s) => parseStatement(s, ctx));
|
|
return { type: "Block", statements, ctx: context(t, ctx) };
|
|
}
|
|
|
|
function parseStatement(t: ParseTree, ctx: ASTCtx): LuaStatement {
|
|
switch (t.type) {
|
|
case "Block":
|
|
return parseChunk(t.children![0], ctx);
|
|
case "Semicolon":
|
|
return { type: "Semicolon", ctx: context(t, ctx) };
|
|
case "Label":
|
|
return {
|
|
type: "Label",
|
|
name: t.children![1].children![0].text!,
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "Break":
|
|
return { type: "Break", ctx: context(t, ctx) };
|
|
case "Goto":
|
|
return {
|
|
type: "Goto",
|
|
name: t.children![1].children![0].text!,
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "Scope":
|
|
return parseBlock(t.children![1], ctx);
|
|
case ";":
|
|
return { type: "Semicolon", ctx: context(t, ctx) };
|
|
case "WhileStatement":
|
|
return {
|
|
type: "While",
|
|
condition: parseExpression(t.children![1], ctx),
|
|
block: parseBlock(t.children![3], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "RepeatStatement":
|
|
return {
|
|
type: "Repeat",
|
|
block: parseBlock(t.children![1], ctx),
|
|
condition: parseExpression(t.children![3], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "IfStatement": {
|
|
const conditions: {
|
|
condition: LuaExpression;
|
|
block: LuaBlock;
|
|
from?: number;
|
|
to?: number;
|
|
}[] = [];
|
|
let elseBlock: LuaBlock | undefined = undefined;
|
|
for (let i = 0; i < t.children!.length; i += 4) {
|
|
const child = t.children![i];
|
|
if (
|
|
child.children![0].text === "if" ||
|
|
child.children![0].text === "elseif"
|
|
) {
|
|
conditions.push({
|
|
condition: parseExpression(t.children![i + 1], ctx),
|
|
block: parseBlock(t.children![i + 3], ctx),
|
|
from: child.from,
|
|
to: child.to,
|
|
});
|
|
} else if (child.children![0].text === "else") {
|
|
elseBlock = parseBlock(t.children![i + 1], ctx);
|
|
} else if (child.children![0].text === "end") {
|
|
break;
|
|
} else {
|
|
throw new Error(
|
|
`Unknown if clause type: ${child.children![0].text}`,
|
|
);
|
|
}
|
|
}
|
|
return {
|
|
type: "If",
|
|
conditions,
|
|
elseBlock,
|
|
ctx: context(t, ctx),
|
|
};
|
|
}
|
|
case "ForStatement":
|
|
if (t.children![1].type === "ForNumeric") {
|
|
const forNumeric = t.children![1];
|
|
return {
|
|
type: "For",
|
|
name: forNumeric.children![0].children![0].text!,
|
|
start: parseExpression(forNumeric.children![2], ctx),
|
|
end: parseExpression(forNumeric.children![4], ctx),
|
|
step: forNumeric.children![5]
|
|
? parseExpression(forNumeric.children![6], ctx)
|
|
: undefined,
|
|
block: parseBlock(t.children![3], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
} else {
|
|
const forGeneric = t.children![1];
|
|
return {
|
|
type: "ForIn",
|
|
names: parseNameList(forGeneric.children![0]),
|
|
expressions: parseExpList(forGeneric.children![2], ctx),
|
|
block: parseBlock(t.children![3], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
}
|
|
case "Function":
|
|
return {
|
|
type: "Function",
|
|
name: parseFunctionName(t.children![1], ctx),
|
|
body: parseFunctionBody(t.children![2], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "LocalFunction":
|
|
return {
|
|
type: "LocalFunction",
|
|
name: t.children![2].children![0].text!,
|
|
body: parseFunctionBody(t.children![3], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "FunctionCall":
|
|
return {
|
|
type: "FunctionCallStatement",
|
|
call: parseFunctionCall(t, ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "Assign":
|
|
return {
|
|
type: "Assignment",
|
|
variables: t.children![0].children!.filter((t) => t.type !== ",").map(
|
|
(lvalue) => parseLValue(lvalue, ctx),
|
|
),
|
|
expressions: parseExpList(t.children![2], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "Local":
|
|
return {
|
|
type: "Local",
|
|
names: parseAttNames(t.children![1], ctx),
|
|
expressions: t.children![3] ? parseExpList(t.children![3], ctx) : [],
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "ReturnStatement": {
|
|
const expressions = t.children![1]
|
|
? parseExpList(t.children![1], ctx)
|
|
: [];
|
|
return { type: "Return", expressions, ctx: context(t, ctx) };
|
|
}
|
|
case "break":
|
|
return { type: "Break", ctx: context(t, ctx) };
|
|
default:
|
|
console.error(t);
|
|
throw new Error(`Unknown statement type: ${t.children![0].text}`);
|
|
}
|
|
}
|
|
|
|
function parseFunctionCall(
|
|
t: ParseTree,
|
|
ctx: ASTCtx,
|
|
): LuaFunctionCallExpression {
|
|
if (t.children![1].type === ":") {
|
|
return {
|
|
type: "FunctionCall",
|
|
prefix: parsePrefixExpression(t.children![0], ctx),
|
|
name: t.children![2].children![0].text!,
|
|
args: parseFunctionArgs(t.children!.slice(3), ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
}
|
|
return {
|
|
type: "FunctionCall",
|
|
prefix: parsePrefixExpression(t.children![0], ctx),
|
|
args: parseFunctionArgs(t.children!.slice(1), ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
}
|
|
|
|
function parseAttNames(t: ParseTree, ctx: ASTCtx): LuaAttName[] {
|
|
if (t.type !== "AttNameList") {
|
|
throw new Error(`Expected AttNameList, got ${t.type}`);
|
|
}
|
|
return t.children!.filter((t) => t.type !== ",").map((att) =>
|
|
parseAttName(att, ctx)
|
|
);
|
|
}
|
|
|
|
function parseAttName(t: ParseTree, ctx: ASTCtx): LuaAttName {
|
|
if (t.type !== "AttName") {
|
|
throw new Error(`Expected AttName, got ${t.type}`);
|
|
}
|
|
return {
|
|
type: "AttName",
|
|
name: t.children![0].children![0].text!,
|
|
attribute: t.children![1].children![1]
|
|
? t.children![1].children![1].children![0].text!
|
|
: undefined,
|
|
ctx: context(t, ctx),
|
|
};
|
|
}
|
|
|
|
function parseLValue(t: ParseTree, ctx: ASTCtx): LuaLValue {
|
|
switch (t.type) {
|
|
case "Name":
|
|
return {
|
|
type: "Variable",
|
|
name: t.children![0].text!,
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "Property":
|
|
return {
|
|
type: "PropertyAccess",
|
|
object: parsePrefixExpression(t.children![0], ctx),
|
|
property: t.children![2].children![0].text!,
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "MemberExpression":
|
|
return {
|
|
type: "TableAccess",
|
|
object: parsePrefixExpression(t.children![0], ctx),
|
|
key: parseExpression(t.children![2], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
default:
|
|
console.error(t);
|
|
throw new Error(`Unknown lvalue type: ${t.type}`);
|
|
}
|
|
}
|
|
|
|
function parseFunctionName(t: ParseTree, ctx: ASTCtx): LuaFunctionName {
|
|
if (t.type !== "FuncName") {
|
|
throw new Error(`Expected FunctionName, got ${t.type}`);
|
|
}
|
|
const propNames: string[] = [];
|
|
let colonName: string | undefined = undefined;
|
|
for (let i = 0; i < t.children!.length; i += 2) {
|
|
const prop = t.children![i];
|
|
propNames.push(prop.children![0].text!);
|
|
if (t.children![i + 1] && t.children![i + 1].type === ":") {
|
|
colonName = t.children![i + 2].children![0].text!;
|
|
break;
|
|
}
|
|
}
|
|
return {
|
|
type: "FunctionName",
|
|
propNames,
|
|
colonName,
|
|
ctx: context(t, ctx),
|
|
};
|
|
}
|
|
|
|
function parseNameList(t: ParseTree): string[] {
|
|
if (t.type !== "NameList") {
|
|
throw new Error(`Expected NameList, got ${t.type}`);
|
|
}
|
|
return t.children!.filter((t) => t.type === "Name").map((t) =>
|
|
t.children![0].text!
|
|
);
|
|
}
|
|
|
|
function parseExpList(t: ParseTree, ctx: ASTCtx): LuaExpression[] {
|
|
if (t.type !== "ExpList") {
|
|
throw new Error(`Expected ExpList, got ${t.type}`);
|
|
}
|
|
return t.children!.filter((t) => t.type !== ",").map((e) =>
|
|
parseExpression(e, ctx)
|
|
);
|
|
}
|
|
|
|
const delimiterRegex = /^(\[=*\[)([\s\S]*)(\]=*\])$/;
|
|
|
|
// In case of quoted strings, remove the quotes and unescape the string
|
|
// In case of a [[ type ]] literal string, remove the brackets
|
|
function parseString(s: string): string {
|
|
// Handle long strings with delimiters
|
|
const delimiterMatch = s.match(delimiterRegex);
|
|
if (delimiterMatch) {
|
|
let text = delimiterMatch[2];
|
|
// According to Lua semantics, whenever a [[ formatted string starts with a newline, that newline should be skipped
|
|
if (text[0] === "\n") {
|
|
text = text.slice(1);
|
|
}
|
|
return text;
|
|
}
|
|
return s.slice(1, -1).replace(
|
|
/\\(x[0-9a-fA-F]{2}|u\{[0-9a-fA-F]+\}|[abfnrtv\\'"n])/g,
|
|
(match, capture) => {
|
|
switch (capture) {
|
|
case "a":
|
|
return "\x07"; // Bell
|
|
case "b":
|
|
return "\b"; // Backspace
|
|
case "f":
|
|
return "\f"; // Form feed
|
|
case "n":
|
|
return "\n"; // Newline
|
|
case "r":
|
|
return "\r"; // Carriage return
|
|
case "t":
|
|
return "\t"; // Horizontal tab
|
|
case "v":
|
|
return "\v"; // Vertical tab
|
|
case "\\":
|
|
return "\\"; // Backslash
|
|
case '"':
|
|
return '"'; // Double quote
|
|
case "'":
|
|
return "'"; // Single quote
|
|
default:
|
|
// Handle hexadecimal \x00
|
|
if (capture.startsWith("x")) {
|
|
return String.fromCharCode(parseInt(capture.slice(1), 16));
|
|
}
|
|
// Handle unicode \u{XXXX}
|
|
if (capture.startsWith("u{")) {
|
|
const codePoint = parseInt(capture.slice(2, -1), 16);
|
|
return String.fromCodePoint(codePoint);
|
|
}
|
|
return match; // return the original match if nothing fits
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
function parseExpression(t: ParseTree, ctx: ASTCtx): LuaExpression {
|
|
switch (t.type) {
|
|
case "LiteralString": {
|
|
const cleanString = parseString(t.children![0].text!);
|
|
return {
|
|
type: "String",
|
|
value: cleanString,
|
|
ctx: context(t, ctx),
|
|
};
|
|
}
|
|
case "Number": {
|
|
const text = t.children![0].text!.toLowerCase();
|
|
return {
|
|
type: "Number",
|
|
// Use the integer parser fox 0x literals
|
|
value: text.includes("x") ? parseInt(text) : parseFloat(text),
|
|
numericType: /[\.eEpP]/.test(text) ? "float" : "int",
|
|
ctx: context(t, ctx),
|
|
};
|
|
}
|
|
case "BinaryExpression":
|
|
return {
|
|
type: "Binary",
|
|
operator: t.children![1].children![0].text!,
|
|
left: parseExpression(t.children![0], ctx),
|
|
right: parseExpression(t.children![2], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "UnaryExpression":
|
|
return {
|
|
type: "Unary",
|
|
operator: t.children![0].children![0].text!,
|
|
argument: parseExpression(t.children![1], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "Property":
|
|
return {
|
|
type: "PropertyAccess",
|
|
object: parsePrefixExpression(t.children![0], ctx),
|
|
property: t.children![2].children![0].text!,
|
|
ctx: context(t, ctx),
|
|
};
|
|
|
|
case "MemberExpression":
|
|
return {
|
|
type: "TableAccess",
|
|
object: parsePrefixExpression(t.children![0], ctx),
|
|
key: parseExpression(t.children![2], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
|
|
case "Parens":
|
|
return parseExpression(t.children![1], ctx);
|
|
case "FunctionCall": {
|
|
return parseFunctionCall(t, ctx);
|
|
}
|
|
case "FunctionDef": {
|
|
const body = parseFunctionBody(t.children![1], ctx);
|
|
return {
|
|
type: "FunctionDefinition",
|
|
body,
|
|
ctx: context(t, ctx),
|
|
};
|
|
}
|
|
case "Name":
|
|
return {
|
|
type: "Variable",
|
|
name: t.children![0].text!,
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "Ellipsis":
|
|
return { type: "Variable", name: "...", ctx: context(t, ctx) };
|
|
case "true":
|
|
return { type: "Boolean", value: true, ctx: context(t, ctx) };
|
|
case "false":
|
|
return { type: "Boolean", value: false, ctx: context(t, ctx) };
|
|
case "TableConstructor":
|
|
return {
|
|
type: "TableConstructor",
|
|
fields: t.children!.slice(1, -1).filter((t) =>
|
|
["FieldExp", "FieldProp", "FieldDynamic"].includes(t.type!)
|
|
).map((tf) => parseTableField(tf, ctx)),
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "nil":
|
|
return { type: "Nil", ctx: context(t, ctx) };
|
|
case "Query":
|
|
return {
|
|
type: "Query",
|
|
clauses: t.children!.slice(2, -1).map((c) => parseQueryClause(c, ctx)),
|
|
ctx: context(t, ctx),
|
|
};
|
|
default:
|
|
console.error(t);
|
|
throw new Error(`Unknown expression type: ${t.type}`);
|
|
}
|
|
}
|
|
|
|
function parseQueryClause(t: ParseTree, ctx: ASTCtx): LuaQueryClause {
|
|
if (t.type !== "QueryClause") {
|
|
throw new Error(`Expected QueryClause, got ${t.type}`);
|
|
}
|
|
t = t.children![0];
|
|
switch (t.type) {
|
|
case "FromClause": {
|
|
if (t.children!.length === 4) {
|
|
// From clause with a name
|
|
return {
|
|
type: "From",
|
|
name: t.children![1].children![0].text!,
|
|
expression: parseExpression(t.children![3], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
} else {
|
|
return {
|
|
type: "From",
|
|
expression: parseExpression(t.children![1], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
}
|
|
}
|
|
case "WhereClause":
|
|
return {
|
|
type: "Where",
|
|
expression: parseExpression(t.children![1], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "LimitClause": {
|
|
const limit = parseExpression(t.children![1], ctx);
|
|
const offset = t.children![2]
|
|
? parseExpression(t.children![3], ctx)
|
|
: undefined;
|
|
return {
|
|
type: "Limit",
|
|
limit,
|
|
offset,
|
|
ctx: context(t, ctx),
|
|
};
|
|
}
|
|
case "OrderByClause": {
|
|
const orderBy: LuaOrderBy[] = [];
|
|
for (const child of t.children!) {
|
|
if (child.type === "OrderBy") {
|
|
orderBy.push({
|
|
type: "Order",
|
|
expression: parseExpression(child.children![0], ctx),
|
|
direction: child.children![1]?.type === "desc" ? "desc" : "asc",
|
|
ctx: context(child, ctx),
|
|
});
|
|
}
|
|
}
|
|
return {
|
|
type: "OrderBy",
|
|
orderBy,
|
|
ctx: context(t, ctx),
|
|
};
|
|
}
|
|
case "SelectClause": {
|
|
return {
|
|
type: "Select",
|
|
expression: parseExpression(t.children![1], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
}
|
|
default:
|
|
console.error(t);
|
|
throw new Error(`Unknown query clause type: ${t.type}`);
|
|
}
|
|
}
|
|
|
|
function parseFunctionArgs(ts: ParseTree[], ctx: ASTCtx): LuaExpression[] {
|
|
return ts.filter((t) => ![",", "(", ")"].includes(t.type!)).map(
|
|
(e) => parseExpression(e, ctx),
|
|
);
|
|
}
|
|
|
|
function parseFunctionBody(t: ParseTree, ctx: ASTCtx): LuaFunctionBody {
|
|
if (t.type !== "FuncBody") {
|
|
throw new Error(`Expected FunctionBody, got ${t.type}`);
|
|
}
|
|
return {
|
|
type: "FunctionBody",
|
|
parameters: t.children![1].children!.filter((t) =>
|
|
["Name", "Ellipsis"].includes(t.type!)
|
|
)
|
|
.map((t) => t.children![0].text!),
|
|
block: parseBlock(t.children![3], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
}
|
|
|
|
function parsePrefixExpression(t: ParseTree, ctx: ASTCtx): LuaPrefixExpression {
|
|
switch (t.type) {
|
|
case "Name":
|
|
return {
|
|
type: "Variable",
|
|
name: t.children![0].text!,
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "Property":
|
|
return {
|
|
type: "PropertyAccess",
|
|
object: parsePrefixExpression(t.children![0], ctx),
|
|
property: t.children![2].children![0].text!,
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "MemberExpression":
|
|
return {
|
|
type: "TableAccess",
|
|
object: parsePrefixExpression(t.children![0], ctx),
|
|
key: parseExpression(t.children![2], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "Parens":
|
|
return {
|
|
type: "Parenthesized",
|
|
expression: parseExpression(t.children![1], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "FunctionCall": {
|
|
return parseFunctionCall(t, ctx);
|
|
}
|
|
default:
|
|
console.error(t);
|
|
throw new Error(`Unknown prefix expression type: ${t.type}`);
|
|
}
|
|
}
|
|
|
|
function parseTableField(t: ParseTree, ctx: ASTCtx): LuaTableField {
|
|
switch (t.type) {
|
|
case "FieldExp":
|
|
return {
|
|
type: "ExpressionField",
|
|
value: parseExpression(t.children![0], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "FieldProp":
|
|
return {
|
|
type: "PropField",
|
|
key: t.children![0].children![0].text!,
|
|
value: parseExpression(t.children![2], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "FieldDynamic":
|
|
return {
|
|
type: "DynamicField",
|
|
key: parseExpression(t.children![1], ctx),
|
|
value: parseExpression(t.children![4], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
default:
|
|
console.error(t);
|
|
throw new Error(`Unknown table field type: ${t.type}`);
|
|
}
|
|
}
|
|
|
|
export function stripLuaComments(s: string): string {
|
|
let result = "";
|
|
let i = 0;
|
|
|
|
while (i < s.length) {
|
|
// Check for long string
|
|
if (s[i] === "[") {
|
|
let j = i + 1;
|
|
let equalsCount = 0;
|
|
while (s[j] === "=") {
|
|
equalsCount++;
|
|
j++;
|
|
}
|
|
if (s[j] === "[") {
|
|
// Found long string start
|
|
const openBracket = s.substring(i, j + 1);
|
|
const closeBracket = "]" + "=".repeat(equalsCount) + "]";
|
|
result += openBracket;
|
|
i = j + 1;
|
|
|
|
// Find matching closing bracket
|
|
const content = s.substring(i);
|
|
const closeIndex = content.indexOf(closeBracket);
|
|
if (closeIndex !== -1) {
|
|
// Copy string content verbatim, including any comment-like sequences
|
|
result += content.substring(0, closeIndex) + closeBracket;
|
|
i += closeIndex + closeBracket.length;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for single quoted string
|
|
if (s[i] === '"' || s[i] === "'") {
|
|
const quote = s[i];
|
|
result += quote;
|
|
i++;
|
|
while (i < s.length && s[i] !== quote) {
|
|
if (s[i] === "\\") {
|
|
result += s[i] + s[i + 1];
|
|
i += 2;
|
|
} else {
|
|
result += s[i];
|
|
i++;
|
|
}
|
|
}
|
|
if (i < s.length) {
|
|
result += s[i]; // closing quote
|
|
i++;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Check for comments
|
|
if (s[i] === "-" && s[i + 1] === "-") {
|
|
// Replace the -- with spaces
|
|
result += " ";
|
|
i += 2;
|
|
|
|
// Check for long comment
|
|
if (s[i] === "[") {
|
|
let j = i + 1;
|
|
let equalsCount = 0;
|
|
while (s[j] === "=") {
|
|
equalsCount++;
|
|
j++;
|
|
}
|
|
if (s[j] === "[") {
|
|
// Found long comment start
|
|
const closeBracket = "]" + "=".repeat(equalsCount) + "]";
|
|
// Replace opening bracket with spaces
|
|
result += " ".repeat(j - i + 1);
|
|
i = j + 1;
|
|
|
|
// Find matching closing bracket
|
|
const content = s.substring(i);
|
|
const closeIndex = content.indexOf(closeBracket);
|
|
if (closeIndex !== -1) {
|
|
// Replace comment content and closing bracket with spaces
|
|
result += " ".repeat(closeIndex) + " ".repeat(closeBracket.length);
|
|
i += closeIndex + closeBracket.length;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Single line comment - replace rest of line with spaces
|
|
while (i < s.length && s[i] !== "\n") {
|
|
result += " ";
|
|
i++;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
result += s[i];
|
|
i++;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
export function parse(s: string, ctx: ASTCtx = {}): LuaBlock {
|
|
const t = parseToCrudeAST(stripLuaComments(s));
|
|
// console.log("Clean tree", JSON.stringify(t, null, 2));
|
|
const result = parseChunk(t, ctx);
|
|
// console.log("Parsed AST", JSON.stringify(result, null, 2));
|
|
return result;
|
|
}
|
|
|
|
export function parseToCrudeAST(t: string): ParseTree {
|
|
const n = lezerToParseTree(t, parser.parse(t).topNode);
|
|
return cleanTree(n, true);
|
|
}
|
|
|
|
/**
|
|
* Helper function to parse a Lua expression string
|
|
*/
|
|
export function parseExpressionString(
|
|
expr: string,
|
|
): LuaExpression {
|
|
const parsedLua = parse(`_(${expr})`) as LuaBlock;
|
|
return (parsedLua.statements[0] as LuaFunctionCallStatement).call.args[0];
|
|
}
|