@lango-project/lango-js 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/compiler.mjs +82 -66
- package/src/runtime.mjs +13 -0
package/package.json
CHANGED
package/src/compiler.mjs
CHANGED
|
@@ -1181,18 +1181,42 @@ function collectSqlParameters(sql) {
|
|
|
1181
1181
|
return [...new Set(names)];
|
|
1182
1182
|
}
|
|
1183
1183
|
|
|
1184
|
+
function interpolationEnd(text, start) {
|
|
1185
|
+
let depth = 1;
|
|
1186
|
+
for (let index = start; index < text.length; index += 1) {
|
|
1187
|
+
const character = text[index];
|
|
1188
|
+
if (character === "'" || character === '"') {
|
|
1189
|
+
index = quotedStringEnd(text, index);
|
|
1190
|
+
if (index < 0) return -1;
|
|
1191
|
+
} else if (character === "{") depth += 1;
|
|
1192
|
+
else if (character === "}" && --depth === 0) return index;
|
|
1193
|
+
}
|
|
1194
|
+
return -1;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
function quotedStringEnd(text, start) {
|
|
1198
|
+
const quote = text[start];
|
|
1199
|
+
for (let index = start + 1; index < text.length; index += 1) {
|
|
1200
|
+
if (text[index] === "\\") { index += 1; continue; }
|
|
1201
|
+
if (text.startsWith("${", index)) {
|
|
1202
|
+
index = interpolationEnd(text, index + 2);
|
|
1203
|
+
if (index < 0) return -1;
|
|
1204
|
+
} else if (text[index] === quote) return index;
|
|
1205
|
+
}
|
|
1206
|
+
return -1;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
function skipString(text, start) {
|
|
1210
|
+
const end = quotedStringEnd(text, start);
|
|
1211
|
+
return end < 0 ? text.length : end;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1184
1214
|
function topLevelBinary(text, operators, rightAssociative = false) {
|
|
1185
|
-
let quote = null;
|
|
1186
1215
|
let depth = 0;
|
|
1187
1216
|
let match = null;
|
|
1188
1217
|
for (let index = 0; index < text.length; index += 1) {
|
|
1189
1218
|
const character = text[index];
|
|
1190
|
-
if (
|
|
1191
|
-
if (character === "\\") { index += 1; continue; }
|
|
1192
|
-
if (character === quote) quote = null;
|
|
1193
|
-
continue;
|
|
1194
|
-
}
|
|
1195
|
-
if (character === "'" || character === '"') { quote = character; continue; }
|
|
1219
|
+
if (character === "'" || character === '"') { index = skipString(text, index); continue; }
|
|
1196
1220
|
if ("([{".includes(character)) { depth += 1; continue; }
|
|
1197
1221
|
if (")]}".includes(character)) { depth -= 1; continue; }
|
|
1198
1222
|
if (depth !== 0) continue;
|
|
@@ -1208,16 +1232,10 @@ function topLevelBinary(text, operators, rightAssociative = false) {
|
|
|
1208
1232
|
}
|
|
1209
1233
|
|
|
1210
1234
|
function curlyDelimiterDepth(text) {
|
|
1211
|
-
let quote = null;
|
|
1212
1235
|
let depth = 0;
|
|
1213
1236
|
for (let index = 0; index < text.length; index += 1) {
|
|
1214
1237
|
const character = text[index];
|
|
1215
|
-
if (
|
|
1216
|
-
if (character === "\\") { index += 1; continue; }
|
|
1217
|
-
if (character === quote) quote = null;
|
|
1218
|
-
continue;
|
|
1219
|
-
}
|
|
1220
|
-
if (character === "'" || character === '"') { quote = character; continue; }
|
|
1238
|
+
if (character === "'" || character === '"') { index = skipString(text, index); continue; }
|
|
1221
1239
|
if (character === "{") depth += 1;
|
|
1222
1240
|
if (character === "}") depth -= 1;
|
|
1223
1241
|
}
|
|
@@ -1228,16 +1246,14 @@ const KEBAB_MARKER = "__lango_dash__";
|
|
|
1228
1246
|
|
|
1229
1247
|
function normalizeKebabIdentifiers(source) {
|
|
1230
1248
|
let result = "";
|
|
1231
|
-
let quote = null;
|
|
1232
1249
|
for (let index = 0; index < source.length; index += 1) {
|
|
1233
1250
|
const character = source[index];
|
|
1234
|
-
if (
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1251
|
+
if (character === "'" || character === '"') {
|
|
1252
|
+
const end = skipString(source, index);
|
|
1253
|
+
result += source.slice(index, end + 1);
|
|
1254
|
+
index = end;
|
|
1238
1255
|
continue;
|
|
1239
1256
|
}
|
|
1240
|
-
if (character === "'" || character === '"') { quote = character; result += character; continue; }
|
|
1241
1257
|
const match = /^[A-Za-z_]\w*(?:-[A-Za-z_]\w*)+/.exec(source.slice(index));
|
|
1242
1258
|
if (match) {
|
|
1243
1259
|
result += match[0].replaceAll("-", KEBAB_MARKER);
|
|
@@ -1270,16 +1286,10 @@ function emitKebabPath(value) {
|
|
|
1270
1286
|
function splitTopLevel(text, separator = ", ") {
|
|
1271
1287
|
const parts = [];
|
|
1272
1288
|
let start = 0;
|
|
1273
|
-
let quote = null;
|
|
1274
1289
|
let depth = 0;
|
|
1275
1290
|
for (let index = 0; index < text.length; index += 1) {
|
|
1276
1291
|
const character = text[index];
|
|
1277
|
-
if (
|
|
1278
|
-
if (character === "\\") { index += 1; continue; }
|
|
1279
|
-
if (character === quote) quote = null;
|
|
1280
|
-
continue;
|
|
1281
|
-
}
|
|
1282
|
-
if (character === "'" || character === '"') { quote = character; continue; }
|
|
1292
|
+
if (character === "'" || character === '"') { index = skipString(text, index); continue; }
|
|
1283
1293
|
if ("([{".includes(character)) { depth += 1; continue; }
|
|
1284
1294
|
if (")]}".includes(character)) { depth -= 1; continue; }
|
|
1285
1295
|
if (depth === 0 && text.startsWith(separator, index)) {
|
|
@@ -1294,16 +1304,10 @@ function splitTopLevel(text, separator = ", ") {
|
|
|
1294
1304
|
|
|
1295
1305
|
function parenthesizedExpression(text) {
|
|
1296
1306
|
if (!text.startsWith("(") || !text.endsWith(")")) return null;
|
|
1297
|
-
let quote = null;
|
|
1298
1307
|
let depth = 0;
|
|
1299
1308
|
for (let index = 0; index < text.length; index += 1) {
|
|
1300
1309
|
const character = text[index];
|
|
1301
|
-
if (
|
|
1302
|
-
if (character === "\\") { index += 1; continue; }
|
|
1303
|
-
if (character === quote) quote = null;
|
|
1304
|
-
continue;
|
|
1305
|
-
}
|
|
1306
|
-
if (character === "'" || character === '"') { quote = character; continue; }
|
|
1310
|
+
if (character === "'" || character === '"') { index = skipString(text, index); continue; }
|
|
1307
1311
|
if (character === "(") depth += 1;
|
|
1308
1312
|
if (character === ")") {
|
|
1309
1313
|
depth -= 1;
|
|
@@ -1744,23 +1748,13 @@ function xmlTagEnd(source, start) {
|
|
|
1744
1748
|
}
|
|
1745
1749
|
|
|
1746
1750
|
function stripComment(line) {
|
|
1747
|
-
let quote = null;
|
|
1748
|
-
let escaped = false;
|
|
1749
1751
|
for (let index = 0; index < line.length; index += 1) {
|
|
1750
1752
|
const character = line[index];
|
|
1751
|
-
if (escaped) {
|
|
1752
|
-
escaped = false;
|
|
1753
|
-
continue;
|
|
1754
|
-
}
|
|
1755
|
-
if (quote && character === "\\") {
|
|
1756
|
-
escaped = true;
|
|
1757
|
-
continue;
|
|
1758
|
-
}
|
|
1759
1753
|
if (character === '"' || character === "'") {
|
|
1760
|
-
|
|
1754
|
+
index = skipString(line, index);
|
|
1761
1755
|
continue;
|
|
1762
1756
|
}
|
|
1763
|
-
if (
|
|
1757
|
+
if (character === "#") return line.slice(0, index).trimEnd();
|
|
1764
1758
|
}
|
|
1765
1759
|
return line;
|
|
1766
1760
|
}
|
|
@@ -1769,20 +1763,25 @@ function literal(value, line, parser) {
|
|
|
1769
1763
|
const quote = value[0];
|
|
1770
1764
|
if (!((quote === "'" || quote === '"') && value.at(-1) === quote))
|
|
1771
1765
|
parser.fail(line, "LNG1109", "expected a string literal");
|
|
1772
|
-
const
|
|
1773
|
-
.slice(1, -1)
|
|
1766
|
+
const decode = (text) => text
|
|
1774
1767
|
.replace(/\\n/g, "\n")
|
|
1775
1768
|
.replace(/\\t/g, "\t")
|
|
1776
|
-
.replace(/\\(['"
|
|
1769
|
+
.replace(/\\(['"\\$])/g, "$1");
|
|
1777
1770
|
const parts = [];
|
|
1778
|
-
let at =
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1771
|
+
let at = 1;
|
|
1772
|
+
for (let index = 1; index < value.length - 1; index += 1) {
|
|
1773
|
+
if (value[index] === "\\") { index += 1; continue; }
|
|
1774
|
+
if (!value.startsWith("${", index)) continue;
|
|
1775
|
+
const end = interpolationEnd(value, index + 2);
|
|
1776
|
+
if (end < 0 || end >= value.length - 1)
|
|
1777
|
+
parser.fail(line, "LNG1267", "unterminated string interpolation");
|
|
1778
|
+
const source = value.slice(index + 2, end).trim();
|
|
1779
|
+
if (!source) parser.fail(line, "LNG1267", "string interpolation requires an expression");
|
|
1780
|
+
parts.push({ text: decode(value.slice(at, index)), value: expr(normalizeKebabIdentifiers(source), line, parser) });
|
|
1781
|
+
index = end;
|
|
1782
|
+
at = end + 1;
|
|
1784
1783
|
}
|
|
1785
|
-
parts.push({ text:
|
|
1784
|
+
parts.push({ text: decode(value.slice(at, -1)), value: null });
|
|
1786
1785
|
return parts;
|
|
1787
1786
|
}
|
|
1788
1787
|
|
|
@@ -2482,6 +2481,7 @@ function validate(program, imports = []) {
|
|
|
2482
2481
|
const expressionReferences = (node, references) => {
|
|
2483
2482
|
if (!node || typeof node !== "object") return;
|
|
2484
2483
|
if (node.type === "raw") {
|
|
2484
|
+
if (["true", "false", "null", "this"].includes(node.value)) return;
|
|
2485
2485
|
const match = /^(@{0,2}[A-Za-z_]\w*)/.exec(node.value);
|
|
2486
2486
|
if (match) references.add(match[1]);
|
|
2487
2487
|
return;
|
|
@@ -2519,10 +2519,7 @@ function validate(program, imports = []) {
|
|
|
2519
2519
|
if (node.type === "string" || node.type === "translate") {
|
|
2520
2520
|
node.literal
|
|
2521
2521
|
.filter((part) => part.value)
|
|
2522
|
-
.forEach((part) =>
|
|
2523
|
-
const match = /^(@{0,2}[A-Za-z_]\w*)/.exec(part.value);
|
|
2524
|
-
if (match) references.add(match[1]);
|
|
2525
|
-
});
|
|
2522
|
+
.forEach((part) => expressionReferences(part.value, references));
|
|
2526
2523
|
return;
|
|
2527
2524
|
}
|
|
2528
2525
|
Object.values(node).forEach((value) => {
|
|
@@ -3461,6 +3458,8 @@ function emit(program, moduleImports = [], includeTests = false) {
|
|
|
3461
3458
|
flags.normalize && "normalize as __normalize",
|
|
3462
3459
|
flags.finite && "finite as __finite",
|
|
3463
3460
|
flags.number && "requireNumber as __requireNumber",
|
|
3461
|
+
flags.numeric && "requireNumeric as __requireNumeric",
|
|
3462
|
+
flags.numericAdd && "numericAdd as __numericAdd",
|
|
3464
3463
|
flags.call && "call as __call",
|
|
3465
3464
|
flags.callMethod && "callMethod as __callMethod",
|
|
3466
3465
|
flags.boundary && "boundary as __boundary",
|
|
@@ -3712,7 +3711,7 @@ function collectEmitLocals(body, names = new Set(), nested = false) {
|
|
|
3712
3711
|
return names;
|
|
3713
3712
|
}
|
|
3714
3713
|
function emitDecimalExpression(node, flags) {
|
|
3715
|
-
const operand = (value) => {
|
|
3714
|
+
const operand = (value, numericOnly = false) => {
|
|
3716
3715
|
if (value.type === "decimal") return emitExpr(value, flags);
|
|
3717
3716
|
if (value.type === "binary" && ["+", "-", "*", "/", "%", "**"].includes(value.operator))
|
|
3718
3717
|
return emitDecimalExpression(value, flags);
|
|
@@ -3722,13 +3721,15 @@ function emitDecimalExpression(node, flags) {
|
|
|
3722
3721
|
}
|
|
3723
3722
|
if (value.type === "raw" && value.value.startsWith("$")) return emitExpr(value, flags);
|
|
3724
3723
|
flags.decimal = true;
|
|
3725
|
-
|
|
3724
|
+
if (numericOnly) flags.numeric = true;
|
|
3725
|
+
const emitted = emitExpr(value, flags);
|
|
3726
|
+
return `__decimal(${numericOnly ? `__requireNumeric(${emitted})` : emitted})`;
|
|
3726
3727
|
};
|
|
3727
3728
|
if (node.type !== "binary" || !["+", "-", "*", "/", "%", "**"].includes(node.operator))
|
|
3728
3729
|
return operand(node);
|
|
3729
3730
|
flags.decimal = true;
|
|
3730
3731
|
const methods = { "+": "plus", "-": "minus", "*": "times", "/": "div", "%": "mod", "**": "pow" };
|
|
3731
|
-
return `${operand(node.left)}.${methods[node.operator]}(${operand(node.right)})`;
|
|
3732
|
+
return `${operand(node.left, node.operator === "+")}.${methods[node.operator]}(${operand(node.right, node.operator === "+")})`;
|
|
3732
3733
|
}
|
|
3733
3734
|
function needsLoopLabel(body, nestedLoopDepth = 0) {
|
|
3734
3735
|
for (const node of body) {
|
|
@@ -3800,6 +3801,10 @@ function emitStatement(node, depth, declared, flags) {
|
|
|
3800
3801
|
if (node.type === "indexAssign") {
|
|
3801
3802
|
const target = `${emitReceiverName(node.receiver, flags)}${(node.indexes ?? [node.index]).map((index) => `[${emitExpr(index, flags)}]`).join("")}`;
|
|
3802
3803
|
const value = emitExpr(node.value, flags);
|
|
3804
|
+
if (node.operator === "+") {
|
|
3805
|
+
flags.numericAdd = true;
|
|
3806
|
+
return `${pad}${target} = __numericAdd(${target}, ${value});\n`;
|
|
3807
|
+
}
|
|
3803
3808
|
if (node.operator) flags.finite = true;
|
|
3804
3809
|
return `${pad}${target} = ${node.operator ? `__finite(${target} ${node.operator} ${value})` : value};\n`;
|
|
3805
3810
|
}
|
|
@@ -3813,7 +3818,10 @@ function emitStatement(node, depth, declared, flags) {
|
|
|
3813
3818
|
}
|
|
3814
3819
|
if (node.type === "superConstructor")
|
|
3815
3820
|
return `${pad}super(${node.arguments.map((item) => emitExpr(item, flags)).join(", ")});\n`;
|
|
3816
|
-
if (node.type === "increment")
|
|
3821
|
+
if (node.type === "increment") {
|
|
3822
|
+
flags.numericAdd = true;
|
|
3823
|
+
return `${pad}${node.name} = __numericAdd(${node.name}, 1);\n`;
|
|
3824
|
+
}
|
|
3817
3825
|
if (node.type === "add")
|
|
3818
3826
|
return `${pad}${node.target}.push(${emitExpr(node.value, flags)});\n`;
|
|
3819
3827
|
if (node.type === "return")
|
|
@@ -4209,8 +4217,16 @@ function emitExpr(node, flags) {
|
|
|
4209
4217
|
: value.type === "raw" && /^\d+(?:\.\d+)?$/.test(value.value)
|
|
4210
4218
|
? `__decimal(${JSON.stringify(value.value)})`
|
|
4211
4219
|
: emitExpr(value, flags);
|
|
4220
|
+
if (node.operator === "+") {
|
|
4221
|
+
flags.numericAdd = true;
|
|
4222
|
+
return `__numericAdd(${operand(node.left)}, ${operand(node.right)})`;
|
|
4223
|
+
}
|
|
4212
4224
|
return `${operand(node.left)}.${methods[node.operator]}(${operand(node.right)})`;
|
|
4213
4225
|
}
|
|
4226
|
+
if (node.operator === "+") {
|
|
4227
|
+
flags.numericAdd = true;
|
|
4228
|
+
return `__numericAdd(${emitExpr(node.left, flags)}, ${emitExpr(node.right, flags)})`;
|
|
4229
|
+
}
|
|
4214
4230
|
const value = `${emitExpr(node.left, flags)} ${operators[node.operator] ?? node.operator} ${emitExpr(node.right, flags)}`;
|
|
4215
4231
|
if (["+", "-", "*", "/", "%", "**"].includes(node.operator)) {
|
|
4216
4232
|
flags.finite = true;
|
|
@@ -4299,7 +4315,7 @@ function emitExpr(node, flags) {
|
|
|
4299
4315
|
.join("");
|
|
4300
4316
|
const values = node.literal
|
|
4301
4317
|
.filter((item) => item.value)
|
|
4302
|
-
.map((item) => item.value);
|
|
4318
|
+
.map((item) => emitExpr(item.value, flags));
|
|
4303
4319
|
return `__translate(${JSON.stringify(template)}, [${values.join(", ")}])`;
|
|
4304
4320
|
}
|
|
4305
4321
|
function emitBoolean(node, flags) {
|
|
@@ -4308,7 +4324,7 @@ function emitBoolean(node, flags) {
|
|
|
4308
4324
|
}
|
|
4309
4325
|
function emitString(parts, flags) {
|
|
4310
4326
|
if (parts.length === 1) return JSON.stringify(parts[0].text);
|
|
4311
|
-
return `\`${parts.map((item) => `${item.text.replaceAll("`", "\\`")}${item.value === null ? "" : `\${${
|
|
4327
|
+
return `\`${parts.map((item) => `${JSON.stringify(item.text).slice(1, -1).replaceAll("`", "\\`").replaceAll("${", "\\${")}${item.value === null ? "" : `\${${emitExpr(item.value, flags)}}`}`).join("")}\``;
|
|
4312
4328
|
}
|
|
4313
4329
|
function spaces(line) {
|
|
4314
4330
|
return line.length - line.trimStart().length;
|
package/src/runtime.mjs
CHANGED
|
@@ -530,6 +530,19 @@ export function requireNumber(value) {
|
|
|
530
530
|
return finite(value);
|
|
531
531
|
}
|
|
532
532
|
|
|
533
|
+
export function requireNumeric(value) {
|
|
534
|
+
if (isDecimal(value) && value.isFinite()) return value;
|
|
535
|
+
return requireNumber(value);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export function numericAdd(left, right) {
|
|
539
|
+
requireNumeric(left);
|
|
540
|
+
requireNumeric(right);
|
|
541
|
+
if (isDecimal(left) || isDecimal(right))
|
|
542
|
+
return decimal(left).plus(right);
|
|
543
|
+
return finite(left + right);
|
|
544
|
+
}
|
|
545
|
+
|
|
533
546
|
export function call(value, ...arguments_) {
|
|
534
547
|
if (typeof value !== "function") throw new Error("NotCallable");
|
|
535
548
|
return normalize(value(...arguments_));
|