@lango-project/lango-js 0.1.5 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lango-project/lango-js",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "license": "Apache-2.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
package/src/cli.mjs CHANGED
@@ -24,7 +24,9 @@ const buildOptions = (arguments_) => {
24
24
  }
25
25
  const value = arguments_[index + 1];
26
26
  if (!value) return null;
27
- if (option === "--decimal-division-scale")
27
+ if (option === "--target")
28
+ profile.target = value;
29
+ else if (option === "--decimal-division-scale")
28
30
  profile.decimal_division_scale = Number(value);
29
31
  else if (option === "--decimal-division-rounding")
30
32
  profile.decimal_division_rounding = Decimal.Rounding[value] ?? value;
@@ -34,21 +36,22 @@ const buildOptions = (arguments_) => {
34
36
  return { outputPath: arguments_[1], profile };
35
37
  };
36
38
  const build = command === "build" && inputPath ? buildOptions(rest) : null;
37
- const runOptions = (arguments_) => {
38
- const options = { database: null, catalog: null };
39
+ const runOptions = (arguments_, allowBuildDir = false) => {
40
+ const options = { database: null, catalog: null, buildDir: null };
39
41
  let index = 0;
40
- while (arguments_[index] === "--database" || arguments_[index] === "--catalog") {
42
+ while (arguments_[index] === "--database" || arguments_[index] === "--catalog" || (allowBuildDir && arguments_[index] === "--build-dir")) {
41
43
  const option = arguments_[index];
42
44
  const value = arguments_[index + 1];
43
45
  if (!value) return null;
44
46
  if (option === "--database") options.database = value;
45
- else options.catalog = value;
47
+ else if (option === "--catalog") options.catalog = value;
48
+ else options.buildDir = value;
46
49
  index += 2;
47
50
  }
48
51
  if (arguments_[index] === "--") index += 1;
49
52
  return { ...options, args: arguments_.slice(index) };
50
53
  };
51
- const testOptions = command === "test" && inputPath ? runOptions(rest) : null;
54
+ const testOptions = command === "test" && inputPath ? runOptions(rest, true) : null;
52
55
 
53
56
  async function initProject(directory) {
54
57
  const target = path.resolve(directory);
@@ -129,7 +132,9 @@ if (command === "init" && inputPath && rest.length === 0) {
129
132
  try {
130
133
  if (testOptions.args.length)
131
134
  throw new Error("Lango test does not accept program arguments");
132
- const directory = await mkdtemp(path.join(os.tmpdir(), "lango-test-"));
135
+ const directory = testOptions.buildDir
136
+ ? path.resolve(testOptions.buildDir)
137
+ : await mkdtemp(path.join(os.tmpdir(), "lango-test-"));
133
138
  const outputPath = path.join(directory, "main.mjs");
134
139
  await buildFile(path.resolve(inputPath), outputPath, new Set(), true);
135
140
  const runtime = await import(pathToFileURL(path.join(directory, ".lango/runtime.mjs")).href);
@@ -173,6 +178,6 @@ if (command === "init" && inputPath && rest.length === 0) {
173
178
  process.exitCode = error instanceof LangoError ? 1 : 2;
174
179
  }
175
180
  } else {
176
- console.error("Usage: node src/cli.mjs init <directory>\n node src/cli.mjs build <input.lango> --out <output.mjs> [--executable] [--decimal-division-scale <n>] [--decimal-division-rounding <EnumName>]\n node src/cli.mjs run <input.lango> [--database <sqlite-file>] [--catalog <catalog.json>] [-- args...]\n node src/cli.mjs test <input.lango> [--database <sqlite-file>] [--catalog <catalog.json>]\n node src/cli.mjs format <input.lango>");
181
+ console.error("Usage: node src/cli.mjs init <directory>\n node src/cli.mjs build <input.lango> --out <output.mjs> [--target node|browser] [--executable] [--decimal-division-scale <n>] [--decimal-division-rounding <EnumName>]\n node src/cli.mjs run <input.lango> [--database <sqlite-file>] [--catalog <catalog.json>] [-- args...]\n node src/cli.mjs test <input.lango> [--build-dir <directory>] [--database <sqlite-file>] [--catalog <catalog.json>]\n node src/cli.mjs format <input.lango>");
177
182
  process.exitCode = 2;
178
183
  }
package/src/compiler.mjs CHANGED
@@ -32,6 +32,8 @@ export function formatDiagnostics(error) {
32
32
 
33
33
  export async function buildFile(inputPath, outputPath, seen = new Set(), includeTests = false, profile = {}) {
34
34
  const decimalProfile = validateDecimalProfile(profile);
35
+ if (decimalProfile.target === "browser" && profile.executable)
36
+ throw new LangoError("LNG1266", 1, "browser builds cannot use --executable", inputPath);
35
37
  const isEntryPoint = seen.size === 0;
36
38
  if (isEntryPoint)
37
39
  await validateModuleInitializerCycles(inputPath, outputPath);
@@ -87,6 +89,8 @@ export async function buildFile(inputPath, outputPath, seen = new Set(), include
87
89
  }
88
90
 
89
91
  const compiled = compile(source, inputPath, imports, includeTests, decimalProfile);
92
+ if (decimalProfile.target === "browser" && compiled.program.jsImports.some((item) => item.specifier?.startsWith("node:")))
93
+ throw new LangoError("LNG1266", 1, "browser builds cannot import node: modules", inputPath);
90
94
  let output = compiled.javascript.replace(
91
95
  "//# sourceMappingURL=main.mjs.map",
92
96
  `//# sourceMappingURL=${path.basename(outputPath)}.map`,
@@ -122,7 +126,9 @@ export async function buildFile(inputPath, outputPath, seen = new Set(), include
122
126
  "utf8",
123
127
  );
124
128
  const xmlVendor = new URL("./vendor/xmldom/", import.meta.url);
129
+ if (decimalProfile.target === "browser") return;
125
130
  await mkdir(path.join(directory, ".lango/xmldom"), { recursive: true });
131
+ await writeFile(path.join(directory, ".lango/xmldom/package.json"), '{"type":"commonjs"}\n', "utf8");
126
132
  for (const name of ["conventions.js", "dom-parser.js", "dom.js", "entities.js", "errors.js", "grammar.js", "index.js", "sax.js"])
127
133
  await writeFile(
128
134
  path.join(directory, ".lango/xmldom", name),
@@ -185,13 +191,16 @@ function tabDiagnostics(source, sourceName) {
185
191
  }
186
192
 
187
193
  function validateDecimalProfile(profile) {
194
+ const target = profile.target ?? "node";
195
+ if (target !== "node" && target !== "browser")
196
+ throw new LangoError("LNG1266", 1, `unknown build target ${target}`);
188
197
  const decimal_division_scale = profile.decimal_division_scale ?? 6;
189
198
  const decimal_division_rounding = profile.decimal_division_rounding ?? Decimal.Rounding.HalfEven;
190
199
  if (!Number.isInteger(decimal_division_scale) || decimal_division_scale < 0)
191
200
  throw new LangoError("LNG1262", 1, "decimal_division_scale must be a non-negative integer");
192
201
  if (!Object.values(Decimal.Rounding).includes(decimal_division_rounding))
193
202
  throw new LangoError("LNG1263", 1, `unknown decimal_division_rounding ${decimal_division_rounding}`);
194
- return { decimal_division_scale, decimal_division_rounding };
203
+ return { decimal_division_scale, decimal_division_rounding, target };
195
204
  }
196
205
 
197
206
  const BASE64_VLQ = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
@@ -1172,18 +1181,42 @@ function collectSqlParameters(sql) {
1172
1181
  return [...new Set(names)];
1173
1182
  }
1174
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
+
1175
1214
  function topLevelBinary(text, operators, rightAssociative = false) {
1176
- let quote = null;
1177
1215
  let depth = 0;
1178
1216
  let match = null;
1179
1217
  for (let index = 0; index < text.length; index += 1) {
1180
1218
  const character = text[index];
1181
- if (quote !== null) {
1182
- if (character === "\\") { index += 1; continue; }
1183
- if (character === quote) quote = null;
1184
- continue;
1185
- }
1186
- if (character === "'" || character === '"') { quote = character; continue; }
1219
+ if (character === "'" || character === '"') { index = skipString(text, index); continue; }
1187
1220
  if ("([{".includes(character)) { depth += 1; continue; }
1188
1221
  if (")]}".includes(character)) { depth -= 1; continue; }
1189
1222
  if (depth !== 0) continue;
@@ -1199,16 +1232,10 @@ function topLevelBinary(text, operators, rightAssociative = false) {
1199
1232
  }
1200
1233
 
1201
1234
  function curlyDelimiterDepth(text) {
1202
- let quote = null;
1203
1235
  let depth = 0;
1204
1236
  for (let index = 0; index < text.length; index += 1) {
1205
1237
  const character = text[index];
1206
- if (quote !== null) {
1207
- if (character === "\\") { index += 1; continue; }
1208
- if (character === quote) quote = null;
1209
- continue;
1210
- }
1211
- if (character === "'" || character === '"') { quote = character; continue; }
1238
+ if (character === "'" || character === '"') { index = skipString(text, index); continue; }
1212
1239
  if (character === "{") depth += 1;
1213
1240
  if (character === "}") depth -= 1;
1214
1241
  }
@@ -1219,16 +1246,14 @@ const KEBAB_MARKER = "__lango_dash__";
1219
1246
 
1220
1247
  function normalizeKebabIdentifiers(source) {
1221
1248
  let result = "";
1222
- let quote = null;
1223
1249
  for (let index = 0; index < source.length; index += 1) {
1224
1250
  const character = source[index];
1225
- if (quote !== null) {
1226
- result += character;
1227
- if (character === "\\") result += source[++index] ?? "";
1228
- else if (character === quote) quote = null;
1251
+ if (character === "'" || character === '"') {
1252
+ const end = skipString(source, index);
1253
+ result += source.slice(index, end + 1);
1254
+ index = end;
1229
1255
  continue;
1230
1256
  }
1231
- if (character === "'" || character === '"') { quote = character; result += character; continue; }
1232
1257
  const match = /^[A-Za-z_]\w*(?:-[A-Za-z_]\w*)+/.exec(source.slice(index));
1233
1258
  if (match) {
1234
1259
  result += match[0].replaceAll("-", KEBAB_MARKER);
@@ -1261,16 +1286,10 @@ function emitKebabPath(value) {
1261
1286
  function splitTopLevel(text, separator = ", ") {
1262
1287
  const parts = [];
1263
1288
  let start = 0;
1264
- let quote = null;
1265
1289
  let depth = 0;
1266
1290
  for (let index = 0; index < text.length; index += 1) {
1267
1291
  const character = text[index];
1268
- if (quote !== null) {
1269
- if (character === "\\") { index += 1; continue; }
1270
- if (character === quote) quote = null;
1271
- continue;
1272
- }
1273
- if (character === "'" || character === '"') { quote = character; continue; }
1292
+ if (character === "'" || character === '"') { index = skipString(text, index); continue; }
1274
1293
  if ("([{".includes(character)) { depth += 1; continue; }
1275
1294
  if (")]}".includes(character)) { depth -= 1; continue; }
1276
1295
  if (depth === 0 && text.startsWith(separator, index)) {
@@ -1285,16 +1304,10 @@ function splitTopLevel(text, separator = ", ") {
1285
1304
 
1286
1305
  function parenthesizedExpression(text) {
1287
1306
  if (!text.startsWith("(") || !text.endsWith(")")) return null;
1288
- let quote = null;
1289
1307
  let depth = 0;
1290
1308
  for (let index = 0; index < text.length; index += 1) {
1291
1309
  const character = text[index];
1292
- if (quote !== null) {
1293
- if (character === "\\") { index += 1; continue; }
1294
- if (character === quote) quote = null;
1295
- continue;
1296
- }
1297
- if (character === "'" || character === '"') { quote = character; continue; }
1310
+ if (character === "'" || character === '"') { index = skipString(text, index); continue; }
1298
1311
  if (character === "(") depth += 1;
1299
1312
  if (character === ")") {
1300
1313
  depth -= 1;
@@ -1735,23 +1748,13 @@ function xmlTagEnd(source, start) {
1735
1748
  }
1736
1749
 
1737
1750
  function stripComment(line) {
1738
- let quote = null;
1739
- let escaped = false;
1740
1751
  for (let index = 0; index < line.length; index += 1) {
1741
1752
  const character = line[index];
1742
- if (escaped) {
1743
- escaped = false;
1744
- continue;
1745
- }
1746
- if (quote && character === "\\") {
1747
- escaped = true;
1748
- continue;
1749
- }
1750
1753
  if (character === '"' || character === "'") {
1751
- quote = quote === character ? null : quote ?? character;
1754
+ index = skipString(line, index);
1752
1755
  continue;
1753
1756
  }
1754
- if (!quote && character === "#") return line.slice(0, index).trimEnd();
1757
+ if (character === "#") return line.slice(0, index).trimEnd();
1755
1758
  }
1756
1759
  return line;
1757
1760
  }
@@ -1760,20 +1763,25 @@ function literal(value, line, parser) {
1760
1763
  const quote = value[0];
1761
1764
  if (!((quote === "'" || quote === '"') && value.at(-1) === quote))
1762
1765
  parser.fail(line, "LNG1109", "expected a string literal");
1763
- const decoded = value
1764
- .slice(1, -1)
1766
+ const decode = (text) => text
1765
1767
  .replace(/\\n/g, "\n")
1766
1768
  .replace(/\\t/g, "\t")
1767
- .replace(/\\(['"\\])/g, "$1");
1769
+ .replace(/\\(['"\\$])/g, "$1");
1768
1770
  const parts = [];
1769
- let at = 0;
1770
- const regex = /\$\{([A-Za-z_]\w*(?:\.[A-Za-z_]\w*|\[\d+\])?)\}/g;
1771
- let match;
1772
- while ((match = regex.exec(decoded))) {
1773
- parts.push({ text: decoded.slice(at, match.index), value: match[1] });
1774
- at = match.index + match[0].length;
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;
1775
1783
  }
1776
- parts.push({ text: decoded.slice(at), value: null });
1784
+ parts.push({ text: decode(value.slice(at, -1)), value: null });
1777
1785
  return parts;
1778
1786
  }
1779
1787
 
@@ -2473,6 +2481,7 @@ function validate(program, imports = []) {
2473
2481
  const expressionReferences = (node, references) => {
2474
2482
  if (!node || typeof node !== "object") return;
2475
2483
  if (node.type === "raw") {
2484
+ if (["true", "false", "null", "this"].includes(node.value)) return;
2476
2485
  const match = /^(@{0,2}[A-Za-z_]\w*)/.exec(node.value);
2477
2486
  if (match) references.add(match[1]);
2478
2487
  return;
@@ -2510,10 +2519,7 @@ function validate(program, imports = []) {
2510
2519
  if (node.type === "string" || node.type === "translate") {
2511
2520
  node.literal
2512
2521
  .filter((part) => part.value)
2513
- .forEach((part) => {
2514
- const match = /^(@{0,2}[A-Za-z_]\w*)/.exec(part.value);
2515
- if (match) references.add(match[1]);
2516
- });
2522
+ .forEach((part) => expressionReferences(part.value, references));
2517
2523
  return;
2518
2524
  }
2519
2525
  Object.values(node).forEach((value) => {
@@ -3452,6 +3458,8 @@ function emit(program, moduleImports = [], includeTests = false) {
3452
3458
  flags.normalize && "normalize as __normalize",
3453
3459
  flags.finite && "finite as __finite",
3454
3460
  flags.number && "requireNumber as __requireNumber",
3461
+ flags.numeric && "requireNumeric as __requireNumeric",
3462
+ flags.numericAdd && "numericAdd as __numericAdd",
3455
3463
  flags.call && "call as __call",
3456
3464
  flags.callMethod && "callMethod as __callMethod",
3457
3465
  flags.boundary && "boundary as __boundary",
@@ -3703,7 +3711,7 @@ function collectEmitLocals(body, names = new Set(), nested = false) {
3703
3711
  return names;
3704
3712
  }
3705
3713
  function emitDecimalExpression(node, flags) {
3706
- const operand = (value) => {
3714
+ const operand = (value, numericOnly = false) => {
3707
3715
  if (value.type === "decimal") return emitExpr(value, flags);
3708
3716
  if (value.type === "binary" && ["+", "-", "*", "/", "%", "**"].includes(value.operator))
3709
3717
  return emitDecimalExpression(value, flags);
@@ -3713,13 +3721,15 @@ function emitDecimalExpression(node, flags) {
3713
3721
  }
3714
3722
  if (value.type === "raw" && value.value.startsWith("$")) return emitExpr(value, flags);
3715
3723
  flags.decimal = true;
3716
- return `__decimal(${emitExpr(value, flags)})`;
3724
+ if (numericOnly) flags.numeric = true;
3725
+ const emitted = emitExpr(value, flags);
3726
+ return `__decimal(${numericOnly ? `__requireNumeric(${emitted})` : emitted})`;
3717
3727
  };
3718
3728
  if (node.type !== "binary" || !["+", "-", "*", "/", "%", "**"].includes(node.operator))
3719
3729
  return operand(node);
3720
3730
  flags.decimal = true;
3721
3731
  const methods = { "+": "plus", "-": "minus", "*": "times", "/": "div", "%": "mod", "**": "pow" };
3722
- 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 === "+")})`;
3723
3733
  }
3724
3734
  function needsLoopLabel(body, nestedLoopDepth = 0) {
3725
3735
  for (const node of body) {
@@ -3791,6 +3801,10 @@ function emitStatement(node, depth, declared, flags) {
3791
3801
  if (node.type === "indexAssign") {
3792
3802
  const target = `${emitReceiverName(node.receiver, flags)}${(node.indexes ?? [node.index]).map((index) => `[${emitExpr(index, flags)}]`).join("")}`;
3793
3803
  const value = emitExpr(node.value, flags);
3804
+ if (node.operator === "+") {
3805
+ flags.numericAdd = true;
3806
+ return `${pad}${target} = __numericAdd(${target}, ${value});\n`;
3807
+ }
3794
3808
  if (node.operator) flags.finite = true;
3795
3809
  return `${pad}${target} = ${node.operator ? `__finite(${target} ${node.operator} ${value})` : value};\n`;
3796
3810
  }
@@ -3804,7 +3818,10 @@ function emitStatement(node, depth, declared, flags) {
3804
3818
  }
3805
3819
  if (node.type === "superConstructor")
3806
3820
  return `${pad}super(${node.arguments.map((item) => emitExpr(item, flags)).join(", ")});\n`;
3807
- if (node.type === "increment") return `${pad}${node.name} += 1;\n`;
3821
+ if (node.type === "increment") {
3822
+ flags.numericAdd = true;
3823
+ return `${pad}${node.name} = __numericAdd(${node.name}, 1);\n`;
3824
+ }
3808
3825
  if (node.type === "add")
3809
3826
  return `${pad}${node.target}.push(${emitExpr(node.value, flags)});\n`;
3810
3827
  if (node.type === "return")
@@ -4200,8 +4217,16 @@ function emitExpr(node, flags) {
4200
4217
  : value.type === "raw" && /^\d+(?:\.\d+)?$/.test(value.value)
4201
4218
  ? `__decimal(${JSON.stringify(value.value)})`
4202
4219
  : emitExpr(value, flags);
4220
+ if (node.operator === "+") {
4221
+ flags.numericAdd = true;
4222
+ return `__numericAdd(${operand(node.left)}, ${operand(node.right)})`;
4223
+ }
4203
4224
  return `${operand(node.left)}.${methods[node.operator]}(${operand(node.right)})`;
4204
4225
  }
4226
+ if (node.operator === "+") {
4227
+ flags.numericAdd = true;
4228
+ return `__numericAdd(${emitExpr(node.left, flags)}, ${emitExpr(node.right, flags)})`;
4229
+ }
4205
4230
  const value = `${emitExpr(node.left, flags)} ${operators[node.operator] ?? node.operator} ${emitExpr(node.right, flags)}`;
4206
4231
  if (["+", "-", "*", "/", "%", "**"].includes(node.operator)) {
4207
4232
  flags.finite = true;
@@ -4290,16 +4315,16 @@ function emitExpr(node, flags) {
4290
4315
  .join("");
4291
4316
  const values = node.literal
4292
4317
  .filter((item) => item.value)
4293
- .map((item) => item.value);
4318
+ .map((item) => emitExpr(item.value, flags));
4294
4319
  return `__translate(${JSON.stringify(template)}, [${values.join(", ")}])`;
4295
4320
  }
4296
4321
  function emitBoolean(node, flags) {
4297
4322
  flags.boolean = true;
4298
4323
  return `__requireBoolean(${emitExpr(node, flags)})`;
4299
4324
  }
4300
- function emitString(parts) {
4325
+ function emitString(parts, flags) {
4301
4326
  if (parts.length === 1) return JSON.stringify(parts[0].text);
4302
- return `\`${parts.map((item) => `${item.text.replaceAll("`", "\\`")}${item.value === null ? "" : `\${${item.value}}`}`).join("")}\``;
4327
+ return `\`${parts.map((item) => `${JSON.stringify(item.text).slice(1, -1).replaceAll("`", "\\`").replaceAll("${", "\\${")}${item.value === null ? "" : `\${${emitExpr(item.value, flags)}}`}`).join("")}\``;
4303
4328
  }
4304
4329
  function spaces(line) {
4305
4330
  return line.length - line.trimStart().length;
package/src/runtime.mjs CHANGED
@@ -15,6 +15,7 @@ const ROUNDING_MODES = Object.freeze({
15
15
  });
16
16
 
17
17
  export function runtimeSource(profile = {}) {
18
+ const browser = profile.target === "browser";
18
19
  const scale = profile.decimal_division_scale ?? 6;
19
20
  const rounding = profile.decimal_division_rounding ?? Decimal.Rounding.HalfEven;
20
21
  if (!Number.isInteger(scale) || scale < 0)
@@ -22,9 +23,9 @@ export function runtimeSource(profile = {}) {
22
23
  if (!(rounding in ROUNDING_MODES))
23
24
  throw new Error(`unknown decimal_division_rounding ${rounding}`);
24
25
  return `import BigNumber from "./bignumber.mjs";
25
- import { createRequire } from "node:module";
26
+ ${browser ? 'const DOMParser = globalThis.DOMParser;' : `import { createRequire } from "node:module";
26
27
  const require = createRequire(import.meta.url);
27
- const { DOMParser } = require("./xmldom/index.js");
28
+ const { DOMParser } = require("./xmldom/index.js");`}
28
29
 
29
30
  const Decimal = BigNumber.clone({
30
31
  DECIMAL_PLACES: ${scale},
@@ -207,6 +208,8 @@ export function xml2obj(xml) {
207
208
  throw new Error(\`XmlParseError: \${error.message}\`);
208
209
  }
209
210
  if (errors.length) throw new Error(\`XmlParseError: \${errors[0]}\`);
211
+ if (${browser} && document.getElementsByTagName("parsererror").length)
212
+ throw new Error("XmlParseError: invalid XML document");
210
213
  if (!document.documentElement)
211
214
  throw new Error("XmlParseError: XML must contain one root tag");
212
215
  return new Tag(document.documentElement);
@@ -270,10 +273,10 @@ export function println(value) {
270
273
  }
271
274
 
272
275
  const defaultLog = Object.freeze({
273
- debug(value) { return writeLog(process.stdout, "DEBUG", value); },
274
- info(value) { return writeLog(process.stdout, "INFO", value); },
275
- warn(value) { return writeLog(process.stderr, "WARN", value); },
276
- err(value) { return writeLog(process.stderr, "ERROR", value); },
276
+ debug(value) { ${browser ? 'console.debug(String(value)); return null;' : 'return writeLog(process.stdout, "DEBUG", value);'} },
277
+ info(value) { ${browser ? 'console.info(String(value)); return null;' : 'return writeLog(process.stdout, "INFO", value);'} },
278
+ warn(value) { ${browser ? 'console.warn(String(value)); return null;' : 'return writeLog(process.stderr, "WARN", value);'} },
279
+ err(value) { ${browser ? 'console.error(String(value)); return null;' : 'return writeLog(process.stderr, "ERROR", value);'} },
277
280
  });
278
281
 
279
282
  function writeLog(stream, level, value) {
@@ -527,6 +530,19 @@ export function requireNumber(value) {
527
530
  return finite(value);
528
531
  }
529
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
+
530
546
  export function call(value, ...arguments_) {
531
547
  if (typeof value !== "function") throw new Error("NotCallable");
532
548
  return normalize(value(...arguments_));