@weborigami/language 0.0.64 → 0.0.65-beta.2

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.
@@ -2,6 +2,17 @@ import * as ops from "../runtime/ops.js";
2
2
 
3
3
  // Parser helpers
4
4
 
5
+ /**
6
+ * If a parse result is an object that will be evaluated at runtime, attach the
7
+ * location of the source code that produced it for debugging and error messages.
8
+ */
9
+ export function annotate(parseResult, location) {
10
+ if (typeof parseResult === "object" && parseResult !== null) {
11
+ parseResult.location = location;
12
+ }
13
+ return parseResult;
14
+ }
15
+
5
16
  export function makeArray(entries) {
6
17
  let currentEntries = [];
7
18
  const spreads = [];
@@ -34,18 +45,45 @@ export function makeArray(entries) {
34
45
  }
35
46
  }
36
47
 
48
+ /**
49
+ * @typedef {import("../../index.ts").Code} Code
50
+ *
51
+ * @param {Code} target
52
+ * @param {Code[]} chain
53
+ * @returns
54
+ */
37
55
  export function makeFunctionCall(target, chain) {
38
56
  let value = target;
57
+ const source = target.location.source;
39
58
  // The chain is an array of arguments (which are themselves arrays). We
40
59
  // successively apply the top-level elements of that chain to build up the
41
60
  // function composition.
61
+ let start = target.location.start;
62
+ let end = target.location.end;
42
63
  for (const args of chain) {
43
- if (args[0] === ops.traverse) {
44
- value = [ops.traverse, value, ...args.slice(1)];
45
- } else {
46
- value = [value, ...args];
64
+ /** @type {Code} */
65
+ let fnCall;
66
+
67
+ // @ts-ignore
68
+ fnCall =
69
+ args[0] === ops.traverse
70
+ ? [ops.traverse, value, ...args.slice(1)]
71
+ : [value, ...args];
72
+
73
+ // Create a location spanning the newly-constructed function call.
74
+ if (args instanceof Array) {
75
+ if (args.location) {
76
+ end = args.location.end;
77
+ } else {
78
+ throw "Internal parser error: no location for function call argument";
79
+ }
47
80
  }
81
+
82
+ fnCall.location = { start, source, end };
83
+
84
+ value = fnCall;
48
85
  }
86
+
49
87
  return value;
50
88
  }
51
89
 
@@ -88,7 +88,7 @@ export default async function evaluate(code) {
88
88
  if (!result[scopeSymbol]) {
89
89
  Object.defineProperty(result, scopeSymbol, {
90
90
  get() {
91
- return scope(result).trees;
91
+ return scope(this).trees;
92
92
  },
93
93
  enumerable: false,
94
94
  });
@@ -5,19 +5,20 @@ import { evaluate } from "./internal.js";
5
5
  /**
6
6
  * Given parsed Origami code, return a function that executes that code.
7
7
  *
8
- * @param {import("../../index.js").Code} code - parsed Origami code
8
+ * @param {import("../../index.js").ParseResult} parsed - parsed Origami expression
9
9
  * @param {string} [name] - optional name of the function
10
10
  */
11
- export function createExpressionFunction(code, name) {
11
+ export function createExpressionFunction(parsed, name) {
12
12
  /** @this {AsyncTree|null} */
13
13
  async function fn() {
14
- return evaluate.call(this, code);
14
+ return evaluate.call(this, parsed);
15
15
  }
16
16
  if (name) {
17
17
  Object.defineProperty(fn, "name", { value: name });
18
18
  }
19
- fn.code = code;
20
- fn.toString = () => code.source?.text;
19
+ fn.code = parsed;
20
+ fn.toString = () =>
21
+ parsed instanceof Array ? parsed.location.source.text : parsed;
21
22
  return fn;
22
23
  }
23
24
 
@@ -112,7 +112,7 @@ describe("Origami parser", () => {
112
112
  // Single slash at start of something = absolute file path
113
113
  assertParse("expression", "/path", [[ops.filesRoot], "path"]);
114
114
  // Consecutive slashes at start of something = comment
115
- assertParse("expression", "path //comment", [ops.scope, "path"]);
115
+ assertParse("expression", "path //comment", [ops.scope, "path"], false);
116
116
  });
117
117
 
118
118
  test("functionComposition", () => {
@@ -450,6 +450,7 @@ describe("Origami parser", () => {
450
450
  assertParse("string", `"foo\\"s bar"`, `foo"s bar`);
451
451
  assertParse("string", `'bar\\'s baz'`, `bar's baz`);
452
452
  assertParse("string", `«string»`, "string");
453
+ assertParse("string", `"\\0\\b\\f\\n\\r\\t\\v"`, "\0\b\f\n\r\t\v");
453
454
  });
454
455
 
455
456
  test("templateDocument", () => {
@@ -519,9 +520,24 @@ describe("Origami parser", () => {
519
520
  });
520
521
  });
521
522
 
522
- function assertParse(startRule, source, expected) {
523
- /** @type {any} */
524
- const parseResult = parse(source, { grammarSource: source, startRule });
523
+ function assertParse(startRule, source, expected, checkLocation = true) {
524
+ const parseResult = parse(source, {
525
+ grammarSource: { text: source },
526
+ startRule,
527
+ });
528
+
529
+ // Verify that the parser returned a `location` property and that it spans the
530
+ // entire source. We skip this check in cases where the source starts or ends
531
+ // with a comment; the parser will strip those.
532
+ if (checkLocation && parseResult instanceof Array) {
533
+ assert(parseResult.location);
534
+ const resultSource = parseResult.location.source.text.slice(
535
+ parseResult.location.start.offset,
536
+ parseResult.location.end.offset
537
+ );
538
+ assert.equal(resultSource, source.trim());
539
+ }
540
+
525
541
  const actual = stripLocations(parseResult);
526
542
  assert.deepEqual(actual, expected);
527
543
  }