@weborigami/language 0.6.11 → 0.6.12

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/main.js CHANGED
@@ -13,11 +13,9 @@ export { default as projectRoot } from "./src/project/projectRoot.js";
13
13
  export { default as projectRootFromPath } from "./src/project/projectRootFromPath.js";
14
14
  export * as Protocols from "./src/protocols/protocols.js";
15
15
  export { formatError, highlightError, lineInfo } from "./src/runtime/errors.js";
16
+ export { default as evaluate } from "./src/runtime/evaluate.js";
16
17
  export { default as EventTargetMixin } from "./src/runtime/EventTargetMixin.js";
17
- export {
18
- default as evaluate,
19
- default as execute,
20
- } from "./src/runtime/execute.js";
18
+ export { default as execute } from "./src/runtime/execute.js";
21
19
  export * as expressionFunction from "./src/runtime/expressionFunction.js";
22
20
  export { default as expressionObject } from "./src/runtime/expressionObject.js";
23
21
  export * from "./src/runtime/handleExtension.js";
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "@weborigami/language",
3
- "version": "0.6.11",
3
+ "version": "0.6.12",
4
4
  "description": "Web Origami expression language compiler and runtime",
5
5
  "type": "module",
6
6
  "main": "./main.js",
7
7
  "types": "./index.ts",
8
8
  "devDependencies": {
9
- "@types/node": "24.10.1",
9
+ "@types/node": "25.3.2",
10
10
  "peggy": "5.0.6",
11
11
  "typescript": "5.9.3"
12
12
  },
13
13
  "dependencies": {
14
- "@weborigami/async-tree": "0.6.11",
14
+ "@weborigami/async-tree": "0.6.12",
15
15
  "exif-parser": "0.1.12",
16
16
  "watcher": "2.3.1",
17
- "yaml": "2.8.1"
17
+ "yaml": "2.8.2"
18
18
  },
19
19
  "scripts": {
20
20
  "build": "peggy --allowed-start-rules=\"*\" --format es src/compiler/origami.pegjs --output src/compiler/parse.js",
@@ -845,8 +845,10 @@ relationalExpression
845
845
  // We disallow a newline before the relational operator to support a newline
846
846
  // as a separator in an object literal that has an object shorthand property
847
847
  // with an angle bracket path. Otherwise the opening angle bracket would be
848
- // interpreted as a relational operator.
849
- = head:shiftExpression tail:(inlineSpace @relationalOperator __ @shiftExpression)* {
848
+ // interpreted as a relational operator. In shell mode we require a space to
849
+ // avoid ambiguity with an angle bracket literal as an argument in an implicit
850
+ // parentheses call.
851
+ = head:shiftExpression tail:(inlineSpace @relationalOperator whitespaceRequiredForShell @shiftExpression)* {
850
852
  return tail.reduce(makeBinaryOperation, head);
851
853
  }
852
854
 
@@ -1039,9 +1041,9 @@ uriKey
1039
1041
 
1040
1042
  // A single character in a URI key
1041
1043
  uriKeyChar
1042
- // Accept anything that doesn't end the URI key or path
1043
- // Reject whitespace; see notes for `whitespace` term
1044
- = char:[^/,\)\]\}] !&{ return /\s/.test(char); } { return char; }
1044
+ // Accept anything except whitespace (see notes for `whitespace` term),
1045
+ // brackets, comma, or slash
1046
+ = char:[^/,\(\[\{\)\]\}] !&{ return /\s/.test(char); } { return char; }
1045
1047
  / escapedChar
1046
1048
 
1047
1049
  // A slash-separated path of keys: `a/b/c`
@@ -1071,6 +1073,10 @@ whitespaceOptionalForProgram
1071
1073
  = programMode __
1072
1074
  / shellMode
1073
1075
 
1076
+ whitespaceRequiredForShell
1077
+ = shellMode whitespace
1078
+ / programMode __
1079
+
1074
1080
  whitespaceOrParenthesis
1075
1081
  = whitespace
1076
1082
  / "("
@@ -357,6 +357,7 @@ function peg$parse(input, options) {
357
357
  whitespace: peg$parsewhitespace,
358
358
  whitespaceChar: peg$parsewhitespaceChar,
359
359
  whitespaceOptionalForProgram: peg$parsewhitespaceOptionalForProgram,
360
+ whitespaceRequiredForShell: peg$parsewhitespaceRequiredForShell,
360
361
  whitespaceOrParenthesis: peg$parsewhitespaceOrParenthesis,
361
362
  whitespaceWithNewLine: peg$parsewhitespaceWithNewLine,
362
363
  };
@@ -459,7 +460,7 @@ function peg$parse(input, options) {
459
460
  const peg$r19 = /^[^\n\r]/;
460
461
  const peg$r20 = /^[!+]/;
461
462
  const peg$r21 = /^[\/)\]}]/;
462
- const peg$r22 = /^[^\/,)\]}]/;
463
+ const peg$r22 = /^[^\/,([{)\]}]/;
463
464
  const peg$r23 = /^[a-z]/;
464
465
  const peg$r24 = /^[a-z0-9+-.]/;
465
466
  const peg$r25 = /^[:]/;
@@ -589,7 +590,7 @@ function peg$parse(input, options) {
589
590
  const peg$e122 = peg$literalExpectation("await", false);
590
591
  const peg$e123 = peg$literalExpectation("typeof", false);
591
592
  const peg$e124 = peg$literalExpectation("void", false);
592
- const peg$e125 = peg$classExpectation(["/", ",", ")", "]", "}"], true, false, false);
593
+ const peg$e125 = peg$classExpectation(["/", ",", "(", "[", "{", ")", "]", "}"], true, false, false);
593
594
  const peg$e126 = peg$otherExpectation("slash-separated path");
594
595
  const peg$e127 = peg$classExpectation([["a", "z"]], false, false, false);
595
596
  const peg$e128 = peg$classExpectation([["a", "z"], ["0", "9"], ["+", "."]], false, false, false);
@@ -6313,10 +6314,15 @@ function peg$parse(input, options) {
6313
6314
  if (s4 !== peg$FAILED) {
6314
6315
  s5 = peg$parserelationalOperator();
6315
6316
  if (s5 !== peg$FAILED) {
6316
- s6 = peg$parse__();
6317
- s7 = peg$parseshiftExpression();
6318
- if (s7 !== peg$FAILED) {
6319
- s3 = [ s5, s7 ];
6317
+ s6 = peg$parsewhitespaceRequiredForShell();
6318
+ if (s6 !== peg$FAILED) {
6319
+ s7 = peg$parseshiftExpression();
6320
+ if (s7 !== peg$FAILED) {
6321
+ s3 = [ s5, s7 ];
6322
+ } else {
6323
+ peg$currPos = s3;
6324
+ s3 = peg$FAILED;
6325
+ }
6320
6326
  } else {
6321
6327
  peg$currPos = s3;
6322
6328
  s3 = peg$FAILED;
@@ -6336,10 +6342,15 @@ function peg$parse(input, options) {
6336
6342
  if (s4 !== peg$FAILED) {
6337
6343
  s5 = peg$parserelationalOperator();
6338
6344
  if (s5 !== peg$FAILED) {
6339
- s6 = peg$parse__();
6340
- s7 = peg$parseshiftExpression();
6341
- if (s7 !== peg$FAILED) {
6342
- s3 = [ s5, s7 ];
6345
+ s6 = peg$parsewhitespaceRequiredForShell();
6346
+ if (s6 !== peg$FAILED) {
6347
+ s7 = peg$parseshiftExpression();
6348
+ if (s7 !== peg$FAILED) {
6349
+ s3 = [ s5, s7 ];
6350
+ } else {
6351
+ peg$currPos = s3;
6352
+ s3 = peg$FAILED;
6353
+ }
6343
6354
  } else {
6344
6355
  peg$currPos = s3;
6345
6356
  s3 = peg$FAILED;
@@ -7834,6 +7845,40 @@ function peg$parse(input, options) {
7834
7845
  return s0;
7835
7846
  }
7836
7847
 
7848
+ function peg$parsewhitespaceRequiredForShell() {
7849
+ let s0, s1, s2;
7850
+
7851
+ s0 = peg$currPos;
7852
+ s1 = peg$parseshellMode();
7853
+ if (s1 !== peg$FAILED) {
7854
+ s2 = peg$parsewhitespace();
7855
+ if (s2 !== peg$FAILED) {
7856
+ s1 = [s1, s2];
7857
+ s0 = s1;
7858
+ } else {
7859
+ peg$currPos = s0;
7860
+ s0 = peg$FAILED;
7861
+ }
7862
+ } else {
7863
+ peg$currPos = s0;
7864
+ s0 = peg$FAILED;
7865
+ }
7866
+ if (s0 === peg$FAILED) {
7867
+ s0 = peg$currPos;
7868
+ s1 = peg$parseprogramMode();
7869
+ if (s1 !== peg$FAILED) {
7870
+ s2 = peg$parse__();
7871
+ s1 = [s1, s2];
7872
+ s0 = s1;
7873
+ } else {
7874
+ peg$currPos = s0;
7875
+ s0 = peg$FAILED;
7876
+ }
7877
+ }
7878
+
7879
+ return s0;
7880
+ }
7881
+
7837
7882
  function peg$parsewhitespaceOrParenthesis() {
7838
7883
  let s0;
7839
7884
 
@@ -8075,6 +8120,7 @@ const peg$allowedStartRules = [
8075
8120
  "whitespace",
8076
8121
  "whitespaceChar",
8077
8122
  "whitespaceOptionalForProgram",
8123
+ "whitespaceRequiredForShell",
8078
8124
  "whitespaceOrParenthesis",
8079
8125
  "whitespaceWithNewLine"
8080
8126
  ];
@@ -26,10 +26,15 @@ export default {
26
26
  * Supports multiple commands, pipelines, redirects, etc.
27
27
  *
28
28
  * @param {string} scriptText - Shell code (may contain newlines/side effects)
29
- * @param {string} inputText - Text to pipe to the script's stdin
29
+ * @param {import("@weborigami/async-tree").Stringlike} inputText - Text to pipe to the script's stdin
30
30
  * @returns {Promise<string>}
31
31
  */
32
32
  function runShellScript(scriptText, inputText) {
33
+ if (inputText instanceof Function) {
34
+ throw new Error(
35
+ "A .sh file expects text input but got a function instead. Did you mean to invoke the function?",
36
+ );
37
+ }
33
38
  return new Promise((resolve, reject) => {
34
39
  // Use sh -c "<scriptText>" so stdin is free for inputText
35
40
  const child = spawn("sh", ["-c", scriptText], {
@@ -49,7 +54,7 @@ function runShellScript(scriptText, inputText) {
49
54
  if (code !== 0) {
50
55
  /** @type {any} */
51
56
  const err = new Error(
52
- `Shell exited with code ${code}${stderr ? `: ${stderr}` : ""}`
57
+ `Shell exited with code ${code}${stderr ? `: ${stderr}` : ""}`,
53
58
  );
54
59
  err.code = code;
55
60
  err.stdout = stdout;
@@ -1,5 +1,7 @@
1
1
  import { Mixin } from "../../index.ts";
2
2
 
3
- declare const WatchFilesMixin: Mixin<{}>;
3
+ declare const WatchFilesMixin: Mixin<{
4
+ watch(): Promise<void>;
5
+ }>;
4
6
 
5
7
  export default WatchFilesMixin;
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs";
2
+ import path from "node:path";
2
3
  import Watcher from "watcher";
3
4
  import TreeEvent from "./TreeEvent.js";
4
5
 
@@ -17,6 +18,12 @@ export default function WatchFilesMixin(Base) {
17
18
  onChange(filePath) {
18
19
  // Reset cached values.
19
20
  this.subfoldersMap = new Map();
21
+
22
+ // Special case: ignore events in .git folder
23
+ if (filePath.includes(`${path.sep}.git${path.sep}`)) {
24
+ return;
25
+ }
26
+
20
27
  this.dispatchEvent(new TreeEvent("change", { filePath }));
21
28
  }
22
29
 
@@ -19,23 +19,13 @@ export default async function execute(code, state = {}) {
19
19
  return code;
20
20
  }
21
21
 
22
- let evaluated;
23
- if (code[0]?.unevaluatedArgs) {
24
- // Don't evaluate instructions, use as is.
25
- evaluated = code;
26
- } else {
27
- // Evaluate each instruction in the code.
28
- evaluated = await Promise.all(
29
- code.map((instruction) => execute(instruction, state)),
30
- );
31
- }
32
-
33
22
  // Add the code to the runtime state
34
23
  /** @type {import("../../index.ts").CodeContext} */
35
24
  const context = { state, code };
36
25
 
37
- // The head of the array is a function or a map; the rest are args or keys.
38
- let [fn, ...args] = evaluated;
26
+ // Start by evaluating the head of the instruction
27
+ const [head, ...tail] = code;
28
+ let fn = await execute(head, state);
39
29
 
40
30
  if (!fn) {
41
31
  // The code wants to invoke something that's couldn't be found in scope.
@@ -53,6 +43,17 @@ export default async function execute(code, state = {}) {
53
43
  fn = await fn.unpack();
54
44
  }
55
45
 
46
+ let args;
47
+ if (fn?.unevaluatedArgs) {
48
+ // Don't evaluate instructions, use as is.
49
+ args = tail;
50
+ } else {
51
+ // Evaluate each instruction in the code.
52
+ args = await Promise.all(
53
+ tail.map((instruction) => execute(instruction, state)),
54
+ );
55
+ }
56
+
56
57
  if (fn.needsState) {
57
58
  // The function is an op that wants the runtime state
58
59
  args.push(state);