@weborigami/language 0.6.0 → 0.6.1

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
@@ -8,7 +8,7 @@ export { default as jsGlobals } from "./src/project/jsGlobals.js";
8
8
  export { default as projectGlobals } from "./src/project/projectGlobals.js";
9
9
  export { default as projectRoot } from "./src/project/projectRoot.js";
10
10
  export * as Protocols from "./src/protocols/protocols.js";
11
- export { formatError } from "./src/runtime/errors.js";
11
+ export { formatError, highlightError } from "./src/runtime/errors.js";
12
12
  export { default as evaluate } from "./src/runtime/evaluate.js";
13
13
  export { default as EventTargetMixin } from "./src/runtime/EventTargetMixin.js";
14
14
  export * as expressionFunction from "./src/runtime/expressionFunction.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weborigami/language",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Web Origami expression language compiler and runtime",
5
5
  "type": "module",
6
6
  "main": "./main.js",
@@ -11,7 +11,7 @@
11
11
  "typescript": "5.9.3"
12
12
  },
13
13
  "dependencies": {
14
- "@weborigami/async-tree": "0.6.0",
14
+ "@weborigami/async-tree": "0.6.1",
15
15
  "exif-parser": "0.1.12",
16
16
  "watcher": "2.3.1",
17
17
  "yaml": "2.8.1"
@@ -2,7 +2,7 @@ import { pathFromKeys, trailingSlash } from "@weborigami/async-tree";
2
2
  import jsGlobals from "../project/jsGlobals.js";
3
3
  import { entryKey } from "../runtime/expressionObject.js";
4
4
  import { ops } from "../runtime/internal.js";
5
- import { annotate, markers } from "./parserHelpers.js";
5
+ import { annotate, markers, spanLocations } from "./parserHelpers.js";
6
6
 
7
7
  export const REFERENCE_PARAM = 1;
8
8
  export const REFERENCE_INHERITED = 2;
@@ -334,6 +334,8 @@ function resolvePath(code, globals, parent, locals, cache) {
334
334
  result[0][0] === ops.inherited);
335
335
  if (extendResult) {
336
336
  result.push(...tail);
337
+ result.location = spanLocations(args);
338
+ result.source = code.source;
337
339
  } else {
338
340
  result = annotate([result, ...tail], code.location);
339
341
  }
@@ -1,4 +1,3 @@
1
- import { trailingSlash } from "@weborigami/async-tree";
2
1
  import * as YAMLModule from "yaml";
3
2
  import codeFragment from "../runtime/codeFragment.js";
4
3
  import * as ops from "../runtime/ops.js";
@@ -380,11 +379,6 @@ export function makePath(keys) {
380
379
  const location = spanLocations(code);
381
380
  code = annotate(code, location);
382
381
 
383
- // Last key has trailing slash implies unpack operation
384
- if (trailingSlash.has(args.at(-1)[1])) {
385
- code = annotate([ops.unpack, code], location);
386
- }
387
-
388
382
  return code;
389
383
  }
390
384
 
@@ -518,7 +512,7 @@ export function makeYamlObject(text, location) {
518
512
 
519
513
  // Create a locations that spans those in the array. This assumes the locations
520
514
  // are in order and non-overlapping.
521
- function spanLocations(code) {
515
+ export function spanLocations(code) {
522
516
  const first = code.find((item) => item.location).location;
523
517
  const last = code[code.findLastIndex((item) => item.location)].location;
524
518
  return {
@@ -0,0 +1,36 @@
1
+ import { getParent, toString } from "@weborigami/async-tree";
2
+
3
+ /**
4
+ * Given packed source text and a handler's options, return a source
5
+ * object that can be passed to the compiler.
6
+ */
7
+ export default function getSource(packed, options = {}) {
8
+ const parent = getParent(packed, options);
9
+
10
+ // Try to determine a URL for error messages
11
+ const sourceName = options.key;
12
+ let url;
13
+ if (sourceName) {
14
+ if (/** @type {any} */ (parent)?.url) {
15
+ let parentHref = /** @type {any} */ (parent).url.href;
16
+ if (!parentHref.endsWith("/")) {
17
+ parentHref += "/";
18
+ }
19
+ url = new URL(sourceName, parentHref);
20
+ } else if (/** @type {any} */ (parent)?.path) {
21
+ let parentHref = new URL(/** @type {any} */ (parent).path, "file:///")
22
+ .href;
23
+ if (!parentHref.endsWith("/")) {
24
+ parentHref += "/";
25
+ }
26
+ url = new URL(sourceName, parentHref);
27
+ }
28
+ }
29
+
30
+ const source = {
31
+ text: toString(packed),
32
+ name: options.key,
33
+ url,
34
+ };
35
+ return source;
36
+ }
@@ -1,6 +1,7 @@
1
- import { getParent, setParent, toString } from "@weborigami/async-tree";
1
+ import { getParent, setParent } from "@weborigami/async-tree";
2
2
  import * as compile from "../compiler/compile.js";
3
3
  import projectGlobals from "../project/projectGlobals.js";
4
+ import getSource from "./getSource.js";
4
5
 
5
6
  /**
6
7
  * An Origami expression file
@@ -13,34 +14,18 @@ export default {
13
14
  /** @type {import("@weborigami/async-tree").UnpackFunction} */
14
15
  async unpack(packed, options = {}) {
15
16
  const parent = getParent(packed, options);
17
+ const source = getSource(packed, options);
16
18
 
17
- // Construct an object to represent the source code.
18
- const sourceName = options.key;
19
- let url;
20
- if (sourceName && /** @type {any} */ (parent)?.url) {
21
- let parentHref = /** @type {any} */ (parent).url.href;
22
- if (!parentHref.endsWith("/")) {
23
- parentHref += "/";
24
- }
25
- url = new URL(sourceName, parentHref);
26
- }
27
-
28
- const source = {
29
- text: toString(packed),
30
- name: options.key,
31
- url,
32
- };
33
-
34
- // Compile the source code as an Origami program and evaluate it.
19
+ // Compile the source code as an Origami program
35
20
  const compiler = options.compiler ?? compile.program;
36
21
  const globals = options.globals ?? (await projectGlobals());
37
-
38
22
  const fn = compiler(source, {
39
23
  globals,
40
24
  mode: "program",
41
25
  parent,
42
26
  });
43
27
 
28
+ // Evaluate the program
44
29
  const result = await fn();
45
30
 
46
31
  if (parent) {
@@ -1,11 +1,7 @@
1
- import {
2
- extension,
3
- getParent,
4
- toString,
5
- trailingSlash,
6
- } from "@weborigami/async-tree";
1
+ import { extension, getParent, trailingSlash } from "@weborigami/async-tree";
7
2
  import * as compile from "../compiler/compile.js";
8
3
  import projectGlobals from "../project/projectGlobals.js";
4
+ import getSource from "./getSource.js";
9
5
 
10
6
  /**
11
7
  * An Origami template document: a plain text file that contains Origami
@@ -17,30 +13,10 @@ export default {
17
13
  /** @type {import("@weborigami/async-tree").UnpackFunction} */
18
14
  async unpack(packed, options = {}) {
19
15
  const parent = getParent(packed, options);
16
+ const source = getSource(packed, options);
20
17
 
21
- // Unpack as a text document
22
- const text = toString(packed);
23
-
24
- // See if we can construct a URL to use in error messages
25
- const key = options.key;
26
- let url;
27
- if (key && /** @type {any} */ (parent)?.url) {
28
- let parentHref = /** @type {any} */ (parent).url.href;
29
- if (!parentHref.endsWith("/")) {
30
- parentHref += "/";
31
- }
32
- url = new URL(key, parentHref);
33
- }
34
-
35
- // Compile the text as an Origami template document
36
- const source = {
37
- name: key,
38
- text,
39
- url,
40
- };
41
-
18
+ // Compile the source code as an Origami template document
42
19
  const globals = options.globals ?? (await projectGlobals());
43
-
44
20
  const defineFn = compile.templateDocument(source, {
45
21
  front: options.front,
46
22
  globals,
@@ -51,6 +27,7 @@ export default {
51
27
  // Invoke the definition to get back the template function
52
28
  const result = await defineFn();
53
29
 
30
+ const key = options.key;
54
31
  const resultExtension = key ? extension.extname(key) : null;
55
32
  if (resultExtension && Object.isExtensible(result)) {
56
33
  // Add sidecar function so this template can be used in a map.
@@ -1,5 +1,14 @@
1
- import { symbols, toString } from "@weborigami/async-tree";
1
+ import {
2
+ getParent,
3
+ isUnpackable,
4
+ symbols,
5
+ toString,
6
+ } from "@weborigami/async-tree";
2
7
  import * as YAMLModule from "yaml";
8
+ import * as compile from "../compiler/compile.js";
9
+ import projectGlobals from "../project/projectGlobals.js";
10
+ import * as expressionFunction from "../runtime/expressionFunction.js";
11
+ import getSource from "./getSource.js";
3
12
 
4
13
  // The "yaml" package doesn't seem to provide a default export that the browser can
5
14
  // recognize, so we have to handle two ways to accommodate Node and the browser.
@@ -16,12 +25,20 @@ export default {
16
25
  mediaType: "application/yaml",
17
26
 
18
27
  /** @type {import("@weborigami/async-tree").UnpackFunction} */
19
- unpack(packed) {
28
+ async unpack(packed, options = {}) {
20
29
  const yaml = toString(packed);
21
30
  if (!yaml) {
22
31
  throw new Error("Tried to parse something as YAML but it wasn't text.");
23
32
  }
24
- const data = YAML.parse(yaml);
33
+ const parent = getParent(packed, options);
34
+ const oriCallTag = await oriCallTagForParent(parent, options);
35
+ const oriTag = await oriTagForParent(parent, options);
36
+ // YAML parser is sync, but top-level !ori or !ori.call tags will return a
37
+ // promise.
38
+ // @ts-ignore TypeScript complains customTags isn't valid here but it is.
39
+ const data = await YAML.parse(yaml, {
40
+ customTags: [oriCallTag, oriTag],
41
+ });
25
42
  if (data && typeof data === "object" && Object.isExtensible(data)) {
26
43
  Object.defineProperty(data, symbols.deep, {
27
44
  enumerable: false,
@@ -31,3 +48,58 @@ export default {
31
48
  return data;
32
49
  },
33
50
  };
51
+
52
+ async function oriCallTagForParent(parent, options) {
53
+ const globals = await projectGlobals();
54
+ return {
55
+ collection: "seq",
56
+
57
+ tag: "!ori.call",
58
+
59
+ identify: (value) => false,
60
+
61
+ async resolve(value) {
62
+ /** @type {any[]} */
63
+ const args = typeof value?.toJSON === "function" ? value.toJSON() : value;
64
+
65
+ // First arg is Origami source
66
+ const text = args.shift();
67
+ const source = getSource(text, options);
68
+
69
+ const codeFn = compile.expression(source, {
70
+ globals,
71
+ parent,
72
+ });
73
+
74
+ // Evaluate the code to get a function
75
+ let fn = await codeFn.call(parent);
76
+
77
+ // Call the function with the rest of the args
78
+ if (isUnpackable(fn)) {
79
+ fn = await fn.unpack();
80
+ }
81
+
82
+ return fn.call(null, ...args);
83
+ },
84
+ };
85
+ }
86
+
87
+ // Define the !ori tag for YAML parsing. This will run in the context of the
88
+ // supplied parent.
89
+ async function oriTagForParent(parent, options) {
90
+ const globals = await projectGlobals();
91
+ return {
92
+ identify: expressionFunction.isExpressionFunction,
93
+
94
+ resolve(text) {
95
+ const source = getSource(text, options);
96
+ const fn = compile.expression(source, {
97
+ globals,
98
+ parent,
99
+ });
100
+ return fn.call(parent);
101
+ },
102
+
103
+ tag: "!ori",
104
+ };
105
+ }
@@ -95,7 +95,9 @@ export function formatError(error) {
95
95
  error.message === "A null or undefined value can't be traversed"
96
96
  ) {
97
97
  // Provide more meaningful message for TraverseError
98
- line = `TraverseError: This part of the path is null or undefined: ${fragment}`;
98
+ line = `TraverseError: This part of the path is null or undefined: ${highlightError(
99
+ fragment
100
+ )}`;
99
101
  fragmentInMessage = true;
100
102
  }
101
103
  if (message) {
@@ -110,7 +112,7 @@ export function formatError(error) {
110
112
  // Add location
111
113
  if (location) {
112
114
  if (!fragmentInMessage) {
113
- message += `\nevaluating: ${fragment}`;
115
+ message += `\nevaluating: ${highlightError(fragment)}`;
114
116
  }
115
117
  message += lineInfo(location);
116
118
  }
@@ -130,6 +132,11 @@ export async function formatScopeTypos(scope, key) {
130
132
  return `Maybe you meant ${list}?`;
131
133
  }
132
134
 
135
+ export function highlightError(text) {
136
+ // ANSI escape sequence to highlight text in red
137
+ return `\x1b[31m${text}\x1b[0m`;
138
+ }
139
+
133
140
  export function maybeOrigamiSourceCode(text) {
134
141
  return origamiSourceSignals.some((signal) => text.includes(signal));
135
142
  }
@@ -36,7 +36,7 @@ export default async function evaluate(code, state = {}) {
36
36
  const error = ReferenceError(
37
37
  `${codeFragment(code[0].location)} is not defined`
38
38
  );
39
- /** @type {any} */ (error).location = code.location;
39
+ /** @type {any} */ (error).location = code[0].location;
40
40
  throw error;
41
41
  }
42
42
 
@@ -41,11 +41,6 @@ export default async function handleExtension(value, key, parent) {
41
41
  handler = await handler.unpack();
42
42
  }
43
43
 
44
- if (hasSlash && handler.unpack) {
45
- // Key like `data.json/` ends in slash -- unpack immediately
46
- return handler.unpack(value, { key, parent });
47
- }
48
-
49
44
  // If the value is a primitive, box it so we can attach data to it.
50
45
  value = box(value);
51
46
 
@@ -233,8 +233,8 @@ describe("Origami parser", () => {
233
233
 
234
234
  test("with paths", () => {
235
235
  assertParse("callExpression", "tree/", [
236
- ops.unpack,
237
- [markers.traverse, [markers.reference, "tree/"]],
236
+ markers.traverse,
237
+ [markers.reference, "tree/"],
238
238
  ]);
239
239
  assertParse("callExpression", "tree/foo/bar", [
240
240
  markers.traverse,
@@ -243,13 +243,10 @@ describe("Origami parser", () => {
243
243
  [ops.literal, "bar"],
244
244
  ]);
245
245
  assertParse("callExpression", "tree/foo/bar/", [
246
- ops.unpack,
247
- [
248
- markers.traverse,
249
- [markers.reference, "tree/"],
250
- [ops.literal, "foo/"],
251
- [ops.literal, "bar/"],
252
- ],
246
+ markers.traverse,
247
+ [markers.reference, "tree/"],
248
+ [ops.literal, "foo/"],
249
+ [ops.literal, "bar/"],
253
250
  ]);
254
251
  // Consecutive slahes in a path are removed
255
252
  assertParse("callExpression", "tree//key", [
@@ -1032,7 +1029,7 @@ Body`,
1032
1029
  ]);
1033
1030
  assertParse("objectEntry", "folder/", [
1034
1031
  "folder/",
1035
- [ops.unpack, [markers.traverse, [markers.reference, "folder/"]]],
1032
+ [markers.traverse, [markers.reference, "folder/"]],
1036
1033
  ]);
1037
1034
  assertParse("objectEntry", "path/to/file.txt", [
1038
1035
  "file.txt",
@@ -1202,8 +1199,8 @@ Body`,
1202
1199
  [markers.reference, "tree"],
1203
1200
  ]);
1204
1201
  assertParse("pathLiteral", "tree/", [
1205
- ops.unpack,
1206
- [markers.traverse, [markers.reference, "tree/"]],
1202
+ markers.traverse,
1203
+ [markers.reference, "tree/"],
1207
1204
  ]);
1208
1205
  assertParse("pathLiteral", "month/12", [
1209
1206
  markers.traverse,
@@ -1211,13 +1208,10 @@ Body`,
1211
1208
  [ops.literal, "12"],
1212
1209
  ]);
1213
1210
  assertParse("pathLiteral", "a/b/c/", [
1214
- ops.unpack,
1215
- [
1216
- markers.traverse,
1217
- [markers.reference, "a/"],
1218
- [ops.literal, "b/"],
1219
- [ops.literal, "c/"],
1220
- ],
1211
+ markers.traverse,
1212
+ [markers.reference, "a/"],
1213
+ [ops.literal, "b/"],
1214
+ [ops.literal, "c/"],
1221
1215
  ]);
1222
1216
  assertParse("pathLiteral", "~/.cshrc", [
1223
1217
  markers.traverse,
@@ -1,3 +1,4 @@
1
+ import { Tree } from "@weborigami/async-tree";
1
2
  import assert from "node:assert";
2
3
  import { describe, test } from "node:test";
3
4
  import yaml_handler from "../../src/handlers/yaml_handler.js";
@@ -8,10 +9,39 @@ describe(".yaml handler", () => {
8
9
  a: 1
9
10
  b: 2
10
11
  `;
11
- const data = yaml_handler.unpack(text);
12
+ const data = await yaml_handler.unpack(text);
12
13
  assert.deepEqual(data, {
13
14
  a: 1,
14
15
  b: 2,
15
16
  });
16
17
  });
18
+
19
+ test("defines !ori tag for Origami expressions", async () => {
20
+ const text = `
21
+ message: Hello
22
+ answer: !ori 1 + 1
23
+ `;
24
+ const data = await yaml_handler.unpack(text);
25
+ const plain = await Tree.plain(data);
26
+ assert.deepEqual(plain, {
27
+ message: "Hello",
28
+ answer: 2,
29
+ });
30
+ });
31
+
32
+ test("defines !ori.call tag for Origami function invocation", async () => {
33
+ const text = `
34
+ message: Hello
35
+ answer: !ori.call
36
+ - (a, b) => a + b
37
+ - 2
38
+ - 3
39
+ `;
40
+ const data = await yaml_handler.unpack(text);
41
+ const plain = await Tree.plain(data);
42
+ assert.deepEqual(plain, {
43
+ message: "Hello",
44
+ answer: 5,
45
+ });
46
+ });
17
47
  });
@@ -15,13 +15,6 @@ describe("handleExtension", () => {
15
15
  const data = await withHandler.unpack();
16
16
  assert.deepEqual(data, { bar: 2 });
17
17
  });
18
-
19
- test("immediately unpacks if key ends in slash", async () => {
20
- const fixture = createFixture();
21
- const jsonFile = await fixture.get("bar.json");
22
- const data = await handleExtension(jsonFile, "bar.json/", fixture);
23
- assert.deepEqual(data, { bar: 2 });
24
- });
25
18
  });
26
19
 
27
20
  function createFixture() {
@@ -1,4 +1,4 @@
1
- import { DeepObjectMap, ObjectMap, Tree } from "@weborigami/async-tree";
1
+ import { ObjectMap, Tree } from "@weborigami/async-tree";
2
2
  import assert from "node:assert";
3
3
  import { describe, test } from "node:test";
4
4
 
@@ -156,11 +156,14 @@ describe("ops", () => {
156
156
  });
157
157
 
158
158
  test("ops.inherited walks up the object parent chain", async () => {
159
- const tree = new DeepObjectMap({
160
- a: {
161
- b: {},
159
+ const tree = new ObjectMap(
160
+ {
161
+ a: {
162
+ b: {},
163
+ },
162
164
  },
163
- });
165
+ { deep: true }
166
+ );
164
167
  const b = await Tree.traverse(tree, "a", "b");
165
168
  assert.equal(await ops.inherited(2, { object: b }), tree);
166
169
  });
@@ -368,12 +371,15 @@ describe("ops", () => {
368
371
 
369
372
  describe("ops.scope", () => {
370
373
  test("returns the scope of the given tree", async () => {
371
- const tree = new DeepObjectMap({
372
- a: {
373
- b: {},
374
+ const tree = new ObjectMap(
375
+ {
376
+ a: {
377
+ b: {},
378
+ },
379
+ c: 1,
374
380
  },
375
- c: 1,
376
- });
381
+ { deep: true }
382
+ );
377
383
  const a = await tree.get("a");
378
384
  const b = await a.get("b");
379
385
  const scope = await ops.scope(b);