@weborigami/language 0.0.69 → 0.0.71-beta.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
@@ -11,5 +11,7 @@ export { default as HandleExtensionsTransform } from "./src/runtime/HandleExtens
11
11
  export { default as ImportModulesMixin } from "./src/runtime/ImportModulesMixin.js";
12
12
  export { default as InvokeFunctionsTransform } from "./src/runtime/InvokeFunctionsTransform.js";
13
13
  export { default as OrigamiFiles } from "./src/runtime/OrigamiFiles.js";
14
+ export * as symbols from "./src/runtime/symbols.js";
15
+ export { default as taggedTemplate } from "./src/runtime/taggedTemplate.js";
14
16
  export { default as TreeEvent } from "./src/runtime/TreeEvent.js";
15
17
  export { default as WatchFilesMixin } from "./src/runtime/WatchFilesMixin.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weborigami/language",
3
- "version": "0.0.69",
3
+ "version": "0.0.71-beta.1",
4
4
  "description": "Web Origami expression language compiler and runtime",
5
5
  "type": "module",
6
6
  "main": "./main.js",
@@ -11,8 +11,8 @@
11
11
  "typescript": "5.6.2"
12
12
  },
13
13
  "dependencies": {
14
- "@weborigami/async-tree": "0.0.69",
15
- "@weborigami/types": "0.0.69",
14
+ "@weborigami/async-tree": "0.0.71-beta.1",
15
+ "@weborigami/types": "0.0.71-beta.1",
16
16
  "watcher": "2.3.1"
17
17
  },
18
18
  "scripts": {
@@ -1,18 +1,20 @@
1
+ import { trailingSlash } from "@weborigami/async-tree";
1
2
  import { createExpressionFunction } from "../runtime/expressionFunction.js";
3
+ import { ops } from "../runtime/internal.js";
2
4
  import { parse } from "./parse.js";
3
5
 
4
6
  function compile(source, startRule) {
5
7
  if (typeof source === "string") {
6
8
  source = { text: source };
7
9
  }
8
- // Trim whitespace from template blocks before we begin lexing, as our
9
- // heuristics are non-local and hard to implement in our parser.
10
- const preprocessed = trimTemplateWhitespace(source.text);
11
- const parseResult = parse(preprocessed, {
10
+ const code = parse(source.text, {
12
11
  grammarSource: source,
13
12
  startRule,
14
13
  });
15
- const fn = createExpressionFunction(parseResult);
14
+ const cache = {};
15
+ const modified = cacheNonLocalScopeReferences(code, cache);
16
+ // const modified = code;
17
+ const fn = createExpressionFunction(modified);
16
18
  return fn;
17
19
  }
18
20
 
@@ -20,42 +22,63 @@ export function expression(source) {
20
22
  return compile(source, "expression");
21
23
  }
22
24
 
23
- export function templateDocument(source) {
24
- return compile(source, "templateDocument");
25
+ // Given code containing ops.scope calls, upgrade them to ops.cache calls unless
26
+ // they refer to local variables: variables defined by object literals or lambda
27
+ // parameters.
28
+ export function cacheNonLocalScopeReferences(code, cache, locals = {}) {
29
+ const [fn, ...args] = code;
30
+
31
+ let additionalLocalNames;
32
+ switch (fn) {
33
+ case ops.scope:
34
+ const key = args[0];
35
+ const normalizedKey = trailingSlash.remove(key);
36
+ if (locals[normalizedKey]) {
37
+ return code;
38
+ } else {
39
+ // Upgrade to cached scope lookup
40
+ const modified = [ops.cache, key, cache];
41
+ /** @type {any} */ (modified).location = code.location;
42
+ return modified;
43
+ }
44
+
45
+ case ops.lambda:
46
+ const parameters = args[0];
47
+ additionalLocalNames = parameters;
48
+ break;
49
+
50
+ case ops.object:
51
+ const entries = args;
52
+ additionalLocalNames = entries.map(([key]) => trailingSlash.remove(key));
53
+ break;
54
+ }
55
+
56
+ let updatedLocals = { ...locals };
57
+ if (additionalLocalNames) {
58
+ for (const key of additionalLocalNames) {
59
+ updatedLocals[key] = true;
60
+ }
61
+ }
62
+
63
+ const modified = code.map((child) => {
64
+ if (Array.isArray(child)) {
65
+ // Review: This currently descends into arrays that are not instructions,
66
+ // such as the parameters of a lambda. This should be harmless, but it'd
67
+ // be preferable to only descend into instructions. This would require
68
+ // surrounding ops.lambda parameters with ops.literal, and ops.object
69
+ // entries with ops.array.
70
+ return cacheNonLocalScopeReferences(child, cache, updatedLocals);
71
+ } else {
72
+ return child;
73
+ }
74
+ });
75
+
76
+ if (code.location) {
77
+ modified.location = code.location;
78
+ }
79
+ return modified;
25
80
  }
26
81
 
27
- // Trim the whitespace around and in substitution blocks in a template. There's
28
- // no explicit syntax for blocks, but we infer them as any place where a
29
- // substitution itself contains a multi-line template literal.
30
- //
31
- // Example:
32
- //
33
- // ${ if `
34
- // true text
35
- // `, `
36
- // false text
37
- // ` }
38
- //
39
- // Case 1: a substitution that starts the text or starts a line (there's only
40
- // whitespace before the `${`), and has the line end with the start of a
41
- // template literal (there's only whitespace after the backtick) marks the start
42
- // of a block.
43
- //
44
- // Case 2: a line in the middle that ends one template literal and starts
45
- // another is an internal break in the block. Edge case: three backticks in a
46
- // row, like ```, are common in markdown and are not treated as a break.
47
- //
48
- // Case 3: a line that ends a template literal and ends with `}` or ends the
49
- // text marks the end of the block.
50
- //
51
- // In all three cases, we trim spaces and tabs from the start and end of the
52
- // line. In case 1, we also remove the preceding newline.
53
- function trimTemplateWhitespace(text) {
54
- const regex1 = /(^|\n)[ \t]*((?:{{|\${).*?`)[ \t]*\n/g;
55
- const regex2 = /\n[ \t]*(`(?!`).*?`)[ \t]*\n/g;
56
- const regex3js = /\n[ \t]*(`(?!`).*?(?:}}|[^\\]}))[ \t]*(?:\n|$)/g;
57
- const trimBlockStarts = text.replace(regex1, "$1$2");
58
- const trimBlockBreaks = trimBlockStarts.replace(regex2, "\n$1");
59
- const trimBlockEnds = trimBlockBreaks.replace(regex3js, "\n$1");
60
- return trimBlockEnds;
82
+ export function templateDocument(source) {
83
+ return compile(source, "templateDocument");
61
84
  }
@@ -103,7 +103,7 @@ doubleArrow = "⇒" / "=>"
103
103
 
104
104
  doubleQuoteString "double quote string"
105
105
  = '"' chars:doubleQuoteStringChar* '"' {
106
- return annotate([ops.primitive, chars.join("")], location());
106
+ return annotate([ops.literal, chars.join("")], location());
107
107
  }
108
108
 
109
109
  doubleQuoteStringChar
@@ -132,13 +132,21 @@ expression "Origami expression"
132
132
 
133
133
  float "floating-point number"
134
134
  = sign? digits? "." digits {
135
- return annotate([ops.primitive, parseFloat(text())], location());
135
+ return annotate([ops.literal, parseFloat(text())], location());
136
136
  }
137
137
 
138
138
  // Parse a function and its arguments, e.g. `fn(arg)`, possibly part of a chain
139
139
  // of function calls, like `fn(arg1)(arg2)(arg3)`.
140
140
  functionComposition "function composition"
141
- = target:callTarget chain:args* end:implicitParensArgs? {
141
+ // Function with at least one argument and maybe implicit parens arguments
142
+ = target:callTarget chain:args+ end:implicitParensArgs? {
143
+ if (end) {
144
+ chain.push(end);
145
+ }
146
+ return annotate(makeFunctionCall(target, chain, location()), location());
147
+ }
148
+ // Function with implicit parens arguments after maybe other arguments
149
+ / target:callTarget chain:args* end:implicitParensArgs {
142
150
  if (end) {
143
151
  chain.push(end);
144
152
  }
@@ -153,7 +161,7 @@ group "parenthetical group"
153
161
 
154
162
  guillemetString "guillemet string"
155
163
  = '«' chars:guillemetStringChar* '»' {
156
- return annotate([ops.primitive, chars.join("")], location());
164
+ return annotate([ops.literal, chars.join("")], location());
157
165
  }
158
166
 
159
167
  guillemetStringChar
@@ -166,7 +174,7 @@ host "HTTP/HTTPS host"
166
174
  = identifier:identifier port:(":" @number)? {
167
175
  const portText = port ? `:${port[1]}` : "";
168
176
  const hostText = identifier + portText;
169
- return annotate([ops.primitive, hostText], location());
177
+ return annotate([ops.literal, hostText], location());
170
178
  }
171
179
 
172
180
  identifier "identifier"
@@ -195,13 +203,13 @@ inlineSpace
195
203
 
196
204
  integer "integer"
197
205
  = sign? digits {
198
- return annotate([ops.primitive, parseInt(text())], location());
206
+ return annotate([ops.literal, parseInt(text())], location());
199
207
  }
200
208
 
201
209
  // A lambda expression: `=foo()`
202
210
  lambda "lambda function"
203
211
  = "=" __ expr:expr {
204
- return annotate([ops.lambda, null, expr], location());
212
+ return annotate([ops.lambda, ["_"], expr], location());
205
213
  }
206
214
 
207
215
  // A path that begins with a slash: `/foo/bar`
@@ -277,7 +285,7 @@ objectPublicKey
277
285
  return identifier + (slash ?? "");
278
286
  }
279
287
  / string:string {
280
- // Remove `ops.primitive` from the string code
288
+ // Remove `ops.literal` from the string code
281
289
  return string[1];
282
290
  }
283
291
 
@@ -309,7 +317,7 @@ path "slash-separated path"
309
317
  // A path key followed by a slash
310
318
  pathElement
311
319
  = chars:pathKeyChar* "/" {
312
- return annotate([ops.primitive, chars.join("") + "/"], location());
320
+ return annotate([ops.literal, chars.join("") + "/"], location());
313
321
  }
314
322
 
315
323
  // A single character in a slash-separated path.
@@ -322,7 +330,7 @@ pathKeyChar
322
330
  // A path key without a slash
323
331
  pathTail
324
332
  = chars:pathKeyChar+ {
325
- return annotate([ops.primitive, chars.join("")], location());
333
+ return annotate([ops.literal, chars.join("")], location());
326
334
  }
327
335
 
328
336
  // Parse a protocol call like `fn://foo/bar`.
@@ -354,6 +362,7 @@ scopeReference "scope reference"
354
362
  scopeTraverse
355
363
  = ref:scopeReference "/" path:path {
356
364
  const head = [ops.scope, `${ ref[1] }/`];
365
+ head.location = ref.location;
357
366
  return annotate([ops.traverse, head, ...path], location());
358
367
  }
359
368
 
@@ -374,7 +383,7 @@ singleLineComment
374
383
 
375
384
  singleQuoteString "single quote string"
376
385
  = "'" chars:singleQuoteStringChar* "'" {
377
- return annotate([ops.primitive, chars.join("")], location());
386
+ return annotate([ops.literal, chars.join("")], location());
378
387
  }
379
388
 
380
389
  singleQuoteStringChar
@@ -402,8 +411,10 @@ step
402
411
  / templateLiteral
403
412
  / string
404
413
  / group
405
- // Protocol calls are distinguished by a colon, but it's not at the start.
414
+ // Things that have a distinctive character, but not at the start
406
415
  / protocolCall
416
+ / taggedTemplate
417
+ / scopeTraverse
407
418
  // Least distinctive option is a simple scope reference, so it comes last.
408
419
  / scopeReference
409
420
 
@@ -415,11 +426,16 @@ string "string"
415
426
  / singleQuoteString
416
427
  / guillemetString
417
428
 
429
+ taggedTemplate
430
+ = tag:callTarget "`" contents:templateLiteralContents "`" {
431
+ return annotate(makeTemplate(tag, contents[0], contents[1]), location());
432
+ }
433
+
418
434
  // A top-level document defining a template. This is the same as a template
419
435
  // literal, but can contain backticks at the top level.
420
436
  templateDocument "template"
421
437
  = contents:templateDocumentContents {
422
- return annotate([ops.lambda, null, contents], location());
438
+ return annotate([ops.lambda, ["_"], contents], location());
423
439
  }
424
440
 
425
441
  // Template documents can contain backticks at the top level.
@@ -428,19 +444,19 @@ templateDocumentChar
428
444
 
429
445
  // The contents of a template document containing plain text and substitutions
430
446
  templateDocumentContents
431
- = parts:(templateDocumentText / templateSubstitution)* {
432
- return annotate(makeTemplate(parts), location());
447
+ = head:templateDocumentText tail:(templateSubstitution templateDocumentText)* {
448
+ return annotate(makeTemplate(ops.template, head, tail), location());
433
449
  }
434
450
 
435
451
  templateDocumentText "template text"
436
- = chars:templateDocumentChar+ {
437
- return annotate([ops.primitive, chars.join("")], location());
438
- }
452
+ = chars:templateDocumentChar* {
453
+ return chars.join("");
454
+ }
439
455
 
440
456
  // A backtick-quoted template literal
441
457
  templateLiteral "template literal"
442
458
  = "`" contents:templateLiteralContents "`" {
443
- return annotate(contents, location());
459
+ return annotate(makeTemplate(ops.template, contents[0], contents[1]), location());
444
460
  }
445
461
 
446
462
  templateLiteralChar
@@ -448,21 +464,17 @@ templateLiteralChar
448
464
 
449
465
  // The contents of a template literal containing plain text and substitutions
450
466
  templateLiteralContents
451
- = parts:(templateLiteralText / templateSubstitution)* {
452
- return annotate(makeTemplate(parts), location());
453
- }
467
+ = head:templateLiteralText tail:(templateSubstitution templateLiteralText)*
454
468
 
455
469
  // Plain text in a template literal
456
470
  templateLiteralText
457
- = chars:templateLiteralChar+ {
458
- return annotate([ops.primitive, chars.join("")], location());
459
- }
471
+ = chars:templateLiteralChar* {
472
+ return chars.join("");
473
+ }
460
474
 
461
475
  // A substitution in a template literal: `${x}`
462
476
  templateSubstitution "template substitution"
463
- = "${" __ expr:expr __ "}" {
464
- return annotate(expr, location());
465
- }
477
+ = "${" __ @expr __ "}"
466
478
 
467
479
  textChar
468
480
  = escapedChar