@weborigami/language 0.2.2 → 0.2.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Jan Miksovsky and other contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/main.js CHANGED
@@ -13,5 +13,6 @@ export { default as InvokeFunctionsTransform } from "./src/runtime/InvokeFunctio
13
13
  export { default as OrigamiFiles } from "./src/runtime/OrigamiFiles.js";
14
14
  export * as symbols from "./src/runtime/symbols.js";
15
15
  export { default as taggedTemplate } from "./src/runtime/taggedTemplate.js";
16
+ export { default as taggedTemplateIndent } from "./src/runtime/taggedTemplateIndent.js";
16
17
  export { default as TreeEvent } from "./src/runtime/TreeEvent.js";
17
18
  export { default as WatchFilesMixin } from "./src/runtime/WatchFilesMixin.js";
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "@weborigami/language",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
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": "22.7.4",
10
- "peggy": "4.0.3",
11
- "typescript": "5.6.2",
12
- "yaml": "2.5.1"
9
+ "@types/node": "22.10.2",
10
+ "peggy": "4.2.0.",
11
+ "typescript": "5.7.2",
12
+ "yaml": "2.6.1"
13
13
  },
14
14
  "dependencies": {
15
- "@weborigami/async-tree": "0.2.2",
16
- "@weborigami/types": "0.2.2",
15
+ "@weborigami/async-tree": "0.2.3",
16
+ "@weborigami/types": "0.2.3",
17
17
  "watcher": "2.3.1"
18
18
  },
19
19
  "scripts": {
@@ -5,7 +5,7 @@ import { parse } from "./parse.js";
5
5
  import { annotate, undetermined } from "./parserHelpers.js";
6
6
 
7
7
  function compile(source, options) {
8
- const { startRule } = options;
8
+ const { macros, startRule } = options;
9
9
  const enableCaching = options.scopeCaching ?? true;
10
10
  if (typeof source === "string") {
11
11
  source = { text: source };
@@ -15,7 +15,7 @@ function compile(source, options) {
15
15
  startRule,
16
16
  });
17
17
  const cache = {};
18
- const modified = transformScopeReferences(code, cache, enableCaching);
18
+ const modified = transformReferences(code, cache, enableCaching, macros);
19
19
  const fn = createExpressionFunction(modified);
20
20
  return fn;
21
21
  }
@@ -27,14 +27,34 @@ export function expression(source, options = {}) {
27
27
  });
28
28
  }
29
29
 
30
- // Transform any remaining undetermined references to scope references. At the
31
- // same time, transform those or explicit ops.scope calls to ops.external calls
32
- // unless they refer to local variables (variables defined by object literals or
33
- // lambda parameters).
34
- export function transformScopeReferences(
30
+ export function program(source, options = {}) {
31
+ return compile(source, {
32
+ ...options,
33
+ startRule: "program",
34
+ });
35
+ }
36
+
37
+ export function templateDocument(source, options = {}) {
38
+ return compile(source, {
39
+ ...options,
40
+ startRule: "templateDocument",
41
+ });
42
+ }
43
+
44
+ /**
45
+ * Transform any remaining undetermined references to scope references.
46
+ *
47
+ * At the same time, transform those or explicit ops.scope calls to ops.external
48
+ * calls unless they refer to local variables (variables defined by object
49
+ * literals or lambda parameters).
50
+ *
51
+ * Also apply any macros to the code.
52
+ */
53
+ export function transformReferences(
35
54
  code,
36
55
  cache,
37
56
  enableCaching,
57
+ macros,
38
58
  locals = {}
39
59
  ) {
40
60
  const [fn, ...args] = code;
@@ -45,7 +65,20 @@ export function transformScopeReferences(
45
65
  case ops.scope:
46
66
  const key = args[0];
47
67
  const normalizedKey = trailingSlash.remove(key);
48
- if (enableCaching && !locals[normalizedKey]) {
68
+ if (macros?.[normalizedKey]) {
69
+ // Apply macro
70
+ const macroBody = macros[normalizedKey];
71
+ const modified = transformReferences(
72
+ macroBody,
73
+ cache,
74
+ enableCaching,
75
+ macros,
76
+ locals
77
+ );
78
+ // @ts-ignore
79
+ annotate(modified, code.location);
80
+ return modified;
81
+ } else if (enableCaching && !locals[normalizedKey]) {
49
82
  // Upgrade to cached external reference
50
83
  const modified = [ops.external, key, cache];
51
84
  // @ts-ignore
@@ -87,10 +120,11 @@ export function transformScopeReferences(
87
120
  // be preferable to only descend into instructions. This would require
88
121
  // surrounding ops.lambda parameters with ops.literal, and ops.object
89
122
  // entries with ops.array.
90
- return transformScopeReferences(
123
+ return transformReferences(
91
124
  child,
92
125
  cache,
93
126
  enableCaching,
127
+ macros,
94
128
  updatedLocals
95
129
  );
96
130
  } else {
@@ -103,17 +137,3 @@ export function transformScopeReferences(
103
137
  }
104
138
  return modified;
105
139
  }
106
-
107
- export function program(source, options = {}) {
108
- return compile(source, {
109
- ...options,
110
- startRule: "program",
111
- });
112
- }
113
-
114
- export function templateDocument(source, options = {}) {
115
- return compile(source, {
116
- ...options,
117
- startRule: "templateDocument",
118
- });
119
- }
@@ -571,20 +571,17 @@ stringLiteral "string"
571
571
  // A top-level document defining a template. This is the same as a template
572
572
  // literal, but can contain backticks at the top level.
573
573
  templateDocument "template"
574
- = contents:templateDocumentContents {
575
- return annotate([ops.lambda, ["_"], contents], location());
574
+ = head:templateDocumentText tail:(templateSubstitution templateDocumentText)* {
575
+ return annotate(
576
+ [ops.lambda, ["_"], makeTemplate(ops.templateIndent, head, tail)],
577
+ location()
578
+ );
576
579
  }
577
580
 
578
581
  // Template documents can contain backticks at the top level.
579
582
  templateDocumentChar
580
583
  = !("${") @textChar
581
584
 
582
- // The contents of a template document containing plain text and substitutions
583
- templateDocumentContents
584
- = head:templateDocumentText tail:(templateSubstitution templateDocumentText)* {
585
- return annotate(makeTemplate(ops.template, head, tail), location());
586
- }
587
-
588
585
  templateDocumentText "template text"
589
586
  = chars:templateDocumentChar* {
590
587
  return chars.join("");
@@ -592,17 +589,13 @@ templateDocumentText "template text"
592
589
 
593
590
  // A backtick-quoted template literal
594
591
  templateLiteral "template literal"
595
- = "`" contents:templateLiteralContents "`" {
596
- return annotate(makeTemplate(ops.template, contents[0], contents[1]), location());
592
+ = "`" head:templateLiteralText tail:(templateSubstitution templateLiteralText)* "`" {
593
+ return annotate(makeTemplate(ops.template, head, tail), location());
597
594
  }
598
595
 
599
596
  templateLiteralChar
600
597
  = !("`" / "${") @textChar
601
598
 
602
- // The contents of a template literal containing plain text and substitutions
603
- templateLiteralContents
604
- = head:templateLiteralText tail:(templateSubstitution templateLiteralText)*
605
-
606
599
  // Plain text in a template literal
607
600
  templateLiteralText
608
601
  = chars:templateLiteralChar* {
@@ -202,7 +202,7 @@ function peg$parse(input, options) {
202
202
  var peg$FAILED = {};
203
203
  var peg$source = options.grammarSource;
204
204
 
205
- var peg$startRuleFunctions = { __: peg$parse__, additiveExpression: peg$parseadditiveExpression, additiveOperator: peg$parseadditiveOperator, arguments: peg$parsearguments, arrayLiteral: peg$parsearrayLiteral, arrayEntries: peg$parsearrayEntries, arrayEntry: peg$parsearrayEntry, arrowFunction: peg$parsearrowFunction, bitwiseAndExpression: peg$parsebitwiseAndExpression, bitwiseAndOperator: peg$parsebitwiseAndOperator, bitwiseOrExpression: peg$parsebitwiseOrExpression, bitwiseOrOperator: peg$parsebitwiseOrOperator, bitwiseXorExpression: peg$parsebitwiseXorExpression, bitwiseXorOperator: peg$parsebitwiseXorOperator, callExpression: peg$parsecallExpression, closingBrace: peg$parseclosingBrace, closingBracket: peg$parseclosingBracket, closingParenthesis: peg$parseclosingParenthesis, commaExpression: peg$parsecommaExpression, comment: peg$parsecomment, conditionalExpression: peg$parseconditionalExpression, digits: peg$parsedigits, doubleArrow: peg$parsedoubleArrow, doubleQuoteString: peg$parsedoubleQuoteString, doubleQuoteStringChar: peg$parsedoubleQuoteStringChar, ellipsis: peg$parseellipsis, equalityExpression: peg$parseequalityExpression, equalityOperator: peg$parseequalityOperator, escapedChar: peg$parseescapedChar, exponentiationExpression: peg$parseexponentiationExpression, expression: peg$parseexpression, floatLiteral: peg$parsefloatLiteral, group: peg$parsegroup, guillemetString: peg$parseguillemetString, guillemetStringChar: peg$parseguillemetStringChar, homeDirectory: peg$parsehomeDirectory, host: peg$parsehost, identifier: peg$parseidentifier, identifierChar: peg$parseidentifierChar, identifierList: peg$parseidentifierList, implicitParenthesesCallExpression: peg$parseimplicitParenthesesCallExpression, implicitParensthesesArguments: peg$parseimplicitParensthesesArguments, inlineSpace: peg$parseinlineSpace, integerLiteral: peg$parseintegerLiteral, list: peg$parselist, literal: peg$parseliteral, logicalAndExpression: peg$parselogicalAndExpression, logicalOrExpression: peg$parselogicalOrExpression, multiLineComment: peg$parsemultiLineComment, multiplicativeExpression: peg$parsemultiplicativeExpression, multiplicativeOperator: peg$parsemultiplicativeOperator, namespace: peg$parsenamespace, newLine: peg$parsenewLine, numericLiteral: peg$parsenumericLiteral, nullishCoalescingExpression: peg$parsenullishCoalescingExpression, objectLiteral: peg$parseobjectLiteral, objectEntries: peg$parseobjectEntries, objectEntry: peg$parseobjectEntry, objectGetter: peg$parseobjectGetter, objectHiddenKey: peg$parseobjectHiddenKey, objectKey: peg$parseobjectKey, objectProperty: peg$parseobjectProperty, objectShorthandProperty: peg$parseobjectShorthandProperty, objectPublicKey: peg$parseobjectPublicKey, parenthesesArguments: peg$parseparenthesesArguments, path: peg$parsepath, pathArguments: peg$parsepathArguments, pathKey: peg$parsepathKey, pathSegment: peg$parsepathSegment, pathSegmentChar: peg$parsepathSegmentChar, pipelineExpression: peg$parsepipelineExpression, primary: peg$parseprimary, program: peg$parseprogram, protocolExpression: peg$parseprotocolExpression, qualifiedReference: peg$parsequalifiedReference, reference: peg$parsereference, relationalExpression: peg$parserelationalExpression, relationalOperator: peg$parserelationalOperator, rootDirectory: peg$parserootDirectory, scopeReference: peg$parsescopeReference, separator: peg$parseseparator, slashFollows: peg$parseslashFollows, shebang: peg$parseshebang, shiftExpression: peg$parseshiftExpression, shiftOperator: peg$parseshiftOperator, shorthandFunction: peg$parseshorthandFunction, singleArrow: peg$parsesingleArrow, singleLineComment: peg$parsesingleLineComment, singleQuoteString: peg$parsesingleQuoteString, singleQuoteStringChar: peg$parsesingleQuoteStringChar, spreadElement: peg$parsespreadElement, stringLiteral: peg$parsestringLiteral, templateDocument: peg$parsetemplateDocument, templateDocumentChar: peg$parsetemplateDocumentChar, templateDocumentContents: peg$parsetemplateDocumentContents, templateDocumentText: peg$parsetemplateDocumentText, templateLiteral: peg$parsetemplateLiteral, templateLiteralChar: peg$parsetemplateLiteralChar, templateLiteralContents: peg$parsetemplateLiteralContents, templateLiteralText: peg$parsetemplateLiteralText, templateSubstitution: peg$parsetemplateSubstitution, textChar: peg$parsetextChar, unaryExpression: peg$parseunaryExpression, unaryOperator: peg$parseunaryOperator, whitespaceWithNewLine: peg$parsewhitespaceWithNewLine };
205
+ var peg$startRuleFunctions = { __: peg$parse__, additiveExpression: peg$parseadditiveExpression, additiveOperator: peg$parseadditiveOperator, arguments: peg$parsearguments, arrayLiteral: peg$parsearrayLiteral, arrayEntries: peg$parsearrayEntries, arrayEntry: peg$parsearrayEntry, arrowFunction: peg$parsearrowFunction, bitwiseAndExpression: peg$parsebitwiseAndExpression, bitwiseAndOperator: peg$parsebitwiseAndOperator, bitwiseOrExpression: peg$parsebitwiseOrExpression, bitwiseOrOperator: peg$parsebitwiseOrOperator, bitwiseXorExpression: peg$parsebitwiseXorExpression, bitwiseXorOperator: peg$parsebitwiseXorOperator, callExpression: peg$parsecallExpression, closingBrace: peg$parseclosingBrace, closingBracket: peg$parseclosingBracket, closingParenthesis: peg$parseclosingParenthesis, commaExpression: peg$parsecommaExpression, comment: peg$parsecomment, conditionalExpression: peg$parseconditionalExpression, digits: peg$parsedigits, doubleArrow: peg$parsedoubleArrow, doubleQuoteString: peg$parsedoubleQuoteString, doubleQuoteStringChar: peg$parsedoubleQuoteStringChar, ellipsis: peg$parseellipsis, equalityExpression: peg$parseequalityExpression, equalityOperator: peg$parseequalityOperator, escapedChar: peg$parseescapedChar, exponentiationExpression: peg$parseexponentiationExpression, expression: peg$parseexpression, floatLiteral: peg$parsefloatLiteral, group: peg$parsegroup, guillemetString: peg$parseguillemetString, guillemetStringChar: peg$parseguillemetStringChar, homeDirectory: peg$parsehomeDirectory, host: peg$parsehost, identifier: peg$parseidentifier, identifierChar: peg$parseidentifierChar, identifierList: peg$parseidentifierList, implicitParenthesesCallExpression: peg$parseimplicitParenthesesCallExpression, implicitParensthesesArguments: peg$parseimplicitParensthesesArguments, inlineSpace: peg$parseinlineSpace, integerLiteral: peg$parseintegerLiteral, list: peg$parselist, literal: peg$parseliteral, logicalAndExpression: peg$parselogicalAndExpression, logicalOrExpression: peg$parselogicalOrExpression, multiLineComment: peg$parsemultiLineComment, multiplicativeExpression: peg$parsemultiplicativeExpression, multiplicativeOperator: peg$parsemultiplicativeOperator, namespace: peg$parsenamespace, newLine: peg$parsenewLine, numericLiteral: peg$parsenumericLiteral, nullishCoalescingExpression: peg$parsenullishCoalescingExpression, objectLiteral: peg$parseobjectLiteral, objectEntries: peg$parseobjectEntries, objectEntry: peg$parseobjectEntry, objectGetter: peg$parseobjectGetter, objectHiddenKey: peg$parseobjectHiddenKey, objectKey: peg$parseobjectKey, objectProperty: peg$parseobjectProperty, objectShorthandProperty: peg$parseobjectShorthandProperty, objectPublicKey: peg$parseobjectPublicKey, parenthesesArguments: peg$parseparenthesesArguments, path: peg$parsepath, pathArguments: peg$parsepathArguments, pathKey: peg$parsepathKey, pathSegment: peg$parsepathSegment, pathSegmentChar: peg$parsepathSegmentChar, pipelineExpression: peg$parsepipelineExpression, primary: peg$parseprimary, program: peg$parseprogram, protocolExpression: peg$parseprotocolExpression, qualifiedReference: peg$parsequalifiedReference, reference: peg$parsereference, relationalExpression: peg$parserelationalExpression, relationalOperator: peg$parserelationalOperator, rootDirectory: peg$parserootDirectory, scopeReference: peg$parsescopeReference, separator: peg$parseseparator, slashFollows: peg$parseslashFollows, shebang: peg$parseshebang, shiftExpression: peg$parseshiftExpression, shiftOperator: peg$parseshiftOperator, shorthandFunction: peg$parseshorthandFunction, singleArrow: peg$parsesingleArrow, singleLineComment: peg$parsesingleLineComment, singleQuoteString: peg$parsesingleQuoteString, singleQuoteStringChar: peg$parsesingleQuoteStringChar, spreadElement: peg$parsespreadElement, stringLiteral: peg$parsestringLiteral, templateDocument: peg$parsetemplateDocument, templateDocumentChar: peg$parsetemplateDocumentChar, templateDocumentText: peg$parsetemplateDocumentText, templateLiteral: peg$parsetemplateLiteral, templateLiteralChar: peg$parsetemplateLiteralChar, templateLiteralText: peg$parsetemplateLiteralText, templateSubstitution: peg$parsetemplateSubstitution, textChar: peg$parsetextChar, unaryExpression: peg$parseunaryExpression, unaryOperator: peg$parseunaryOperator, whitespaceWithNewLine: peg$parsewhitespaceWithNewLine };
206
206
  var peg$startRuleFunction = peg$parse__;
207
207
 
208
208
  var peg$c0 = "[";
@@ -603,25 +603,25 @@ function peg$parse(input, options) {
603
603
  var peg$f68 = function(value) {
604
604
  return annotate([ops.spread, value], location());
605
605
  };
606
- var peg$f69 = function(contents) {
607
- return annotate([ops.lambda, ["_"], contents], location());
608
- };
609
- var peg$f70 = function(head, tail) {
610
- return annotate(makeTemplate(ops.template, head, tail), location());
606
+ var peg$f69 = function(head, tail) {
607
+ return annotate(
608
+ [ops.lambda, ["_"], makeTemplate(ops.templateIndent, head, tail)],
609
+ location()
610
+ );
611
611
  };
612
- var peg$f71 = function(chars) {
612
+ var peg$f70 = function(chars) {
613
613
  return chars.join("");
614
614
  };
615
- var peg$f72 = function(contents) {
616
- return annotate(makeTemplate(ops.template, contents[0], contents[1]), location());
615
+ var peg$f71 = function(head, tail) {
616
+ return annotate(makeTemplate(ops.template, head, tail), location());
617
617
  };
618
- var peg$f73 = function(chars) {
618
+ var peg$f72 = function(chars) {
619
619
  return chars.join("");
620
620
  };
621
- var peg$f74 = function(expression) {
621
+ var peg$f73 = function(expression) {
622
622
  return annotate(expression, location());
623
623
  };
624
- var peg$f75 = function(operator, expression) {
624
+ var peg$f74 = function(operator, expression) {
625
625
  return annotate(makeUnaryOperation(operator, expression), location());
626
626
  };
627
627
  var peg$currPos = options.peg$currPos | 0;
@@ -4358,14 +4358,37 @@ function peg$parse(input, options) {
4358
4358
  }
4359
4359
 
4360
4360
  function peg$parsetemplateDocument() {
4361
- var s0, s1;
4361
+ var s0, s1, s2, s3, s4, s5;
4362
4362
 
4363
4363
  peg$silentFails++;
4364
4364
  s0 = peg$currPos;
4365
- s1 = peg$parsetemplateDocumentContents();
4365
+ s1 = peg$parsetemplateDocumentText();
4366
+ s2 = [];
4367
+ s3 = peg$currPos;
4368
+ s4 = peg$parsetemplateSubstitution();
4369
+ if (s4 !== peg$FAILED) {
4370
+ s5 = peg$parsetemplateDocumentText();
4371
+ s4 = [s4, s5];
4372
+ s3 = s4;
4373
+ } else {
4374
+ peg$currPos = s3;
4375
+ s3 = peg$FAILED;
4376
+ }
4377
+ while (s3 !== peg$FAILED) {
4378
+ s2.push(s3);
4379
+ s3 = peg$currPos;
4380
+ s4 = peg$parsetemplateSubstitution();
4381
+ if (s4 !== peg$FAILED) {
4382
+ s5 = peg$parsetemplateDocumentText();
4383
+ s4 = [s4, s5];
4384
+ s3 = s4;
4385
+ } else {
4386
+ peg$currPos = s3;
4387
+ s3 = peg$FAILED;
4388
+ }
4389
+ }
4366
4390
  peg$savedPos = s0;
4367
- s1 = peg$f69(s1);
4368
- s0 = s1;
4391
+ s0 = peg$f69(s1, s2);
4369
4392
  peg$silentFails--;
4370
4393
  s1 = peg$FAILED;
4371
4394
  if (peg$silentFails === 0) { peg$fail(peg$e94); }
@@ -4409,41 +4432,6 @@ function peg$parse(input, options) {
4409
4432
  return s0;
4410
4433
  }
4411
4434
 
4412
- function peg$parsetemplateDocumentContents() {
4413
- var s0, s1, s2, s3, s4, s5;
4414
-
4415
- s0 = peg$currPos;
4416
- s1 = peg$parsetemplateDocumentText();
4417
- s2 = [];
4418
- s3 = peg$currPos;
4419
- s4 = peg$parsetemplateSubstitution();
4420
- if (s4 !== peg$FAILED) {
4421
- s5 = peg$parsetemplateDocumentText();
4422
- s4 = [s4, s5];
4423
- s3 = s4;
4424
- } else {
4425
- peg$currPos = s3;
4426
- s3 = peg$FAILED;
4427
- }
4428
- while (s3 !== peg$FAILED) {
4429
- s2.push(s3);
4430
- s3 = peg$currPos;
4431
- s4 = peg$parsetemplateSubstitution();
4432
- if (s4 !== peg$FAILED) {
4433
- s5 = peg$parsetemplateDocumentText();
4434
- s4 = [s4, s5];
4435
- s3 = s4;
4436
- } else {
4437
- peg$currPos = s3;
4438
- s3 = peg$FAILED;
4439
- }
4440
- }
4441
- peg$savedPos = s0;
4442
- s0 = peg$f70(s1, s2);
4443
-
4444
- return s0;
4445
- }
4446
-
4447
4435
  function peg$parsetemplateDocumentText() {
4448
4436
  var s0, s1, s2;
4449
4437
 
@@ -4456,7 +4444,7 @@ function peg$parse(input, options) {
4456
4444
  s2 = peg$parsetemplateDocumentChar();
4457
4445
  }
4458
4446
  peg$savedPos = s0;
4459
- s1 = peg$f71(s1);
4447
+ s1 = peg$f70(s1);
4460
4448
  s0 = s1;
4461
4449
  peg$silentFails--;
4462
4450
  s1 = peg$FAILED;
@@ -4466,7 +4454,7 @@ function peg$parse(input, options) {
4466
4454
  }
4467
4455
 
4468
4456
  function peg$parsetemplateLiteral() {
4469
- var s0, s1, s2, s3;
4457
+ var s0, s1, s2, s3, s4, s5, s6;
4470
4458
 
4471
4459
  peg$silentFails++;
4472
4460
  s0 = peg$currPos;
@@ -4478,17 +4466,41 @@ function peg$parse(input, options) {
4478
4466
  if (peg$silentFails === 0) { peg$fail(peg$e98); }
4479
4467
  }
4480
4468
  if (s1 !== peg$FAILED) {
4481
- s2 = peg$parsetemplateLiteralContents();
4469
+ s2 = peg$parsetemplateLiteralText();
4470
+ s3 = [];
4471
+ s4 = peg$currPos;
4472
+ s5 = peg$parsetemplateSubstitution();
4473
+ if (s5 !== peg$FAILED) {
4474
+ s6 = peg$parsetemplateLiteralText();
4475
+ s5 = [s5, s6];
4476
+ s4 = s5;
4477
+ } else {
4478
+ peg$currPos = s4;
4479
+ s4 = peg$FAILED;
4480
+ }
4481
+ while (s4 !== peg$FAILED) {
4482
+ s3.push(s4);
4483
+ s4 = peg$currPos;
4484
+ s5 = peg$parsetemplateSubstitution();
4485
+ if (s5 !== peg$FAILED) {
4486
+ s6 = peg$parsetemplateLiteralText();
4487
+ s5 = [s5, s6];
4488
+ s4 = s5;
4489
+ } else {
4490
+ peg$currPos = s4;
4491
+ s4 = peg$FAILED;
4492
+ }
4493
+ }
4482
4494
  if (input.charCodeAt(peg$currPos) === 96) {
4483
- s3 = peg$c59;
4495
+ s4 = peg$c59;
4484
4496
  peg$currPos++;
4485
4497
  } else {
4486
- s3 = peg$FAILED;
4498
+ s4 = peg$FAILED;
4487
4499
  if (peg$silentFails === 0) { peg$fail(peg$e98); }
4488
4500
  }
4489
- if (s3 !== peg$FAILED) {
4501
+ if (s4 !== peg$FAILED) {
4490
4502
  peg$savedPos = s0;
4491
- s0 = peg$f72(s2);
4503
+ s0 = peg$f71(s2, s3);
4492
4504
  } else {
4493
4505
  peg$currPos = s0;
4494
4506
  s0 = peg$FAILED;
@@ -4551,41 +4563,6 @@ function peg$parse(input, options) {
4551
4563
  return s0;
4552
4564
  }
4553
4565
 
4554
- function peg$parsetemplateLiteralContents() {
4555
- var s0, s1, s2, s3, s4, s5;
4556
-
4557
- s0 = peg$currPos;
4558
- s1 = peg$parsetemplateLiteralText();
4559
- s2 = [];
4560
- s3 = peg$currPos;
4561
- s4 = peg$parsetemplateSubstitution();
4562
- if (s4 !== peg$FAILED) {
4563
- s5 = peg$parsetemplateLiteralText();
4564
- s4 = [s4, s5];
4565
- s3 = s4;
4566
- } else {
4567
- peg$currPos = s3;
4568
- s3 = peg$FAILED;
4569
- }
4570
- while (s3 !== peg$FAILED) {
4571
- s2.push(s3);
4572
- s3 = peg$currPos;
4573
- s4 = peg$parsetemplateSubstitution();
4574
- if (s4 !== peg$FAILED) {
4575
- s5 = peg$parsetemplateLiteralText();
4576
- s4 = [s4, s5];
4577
- s3 = s4;
4578
- } else {
4579
- peg$currPos = s3;
4580
- s3 = peg$FAILED;
4581
- }
4582
- }
4583
- s1 = [s1, s2];
4584
- s0 = s1;
4585
-
4586
- return s0;
4587
- }
4588
-
4589
4566
  function peg$parsetemplateLiteralText() {
4590
4567
  var s0, s1, s2;
4591
4568
 
@@ -4597,7 +4574,7 @@ function peg$parse(input, options) {
4597
4574
  s2 = peg$parsetemplateLiteralChar();
4598
4575
  }
4599
4576
  peg$savedPos = s0;
4600
- s1 = peg$f73(s1);
4577
+ s1 = peg$f72(s1);
4601
4578
  s0 = s1;
4602
4579
 
4603
4580
  return s0;
@@ -4627,7 +4604,7 @@ function peg$parse(input, options) {
4627
4604
  }
4628
4605
  if (s3 !== peg$FAILED) {
4629
4606
  peg$savedPos = s0;
4630
- s0 = peg$f74(s2);
4607
+ s0 = peg$f73(s2);
4631
4608
  } else {
4632
4609
  peg$currPos = s0;
4633
4610
  s0 = peg$FAILED;
@@ -4676,7 +4653,7 @@ function peg$parse(input, options) {
4676
4653
  s3 = peg$parseunaryExpression();
4677
4654
  if (s3 !== peg$FAILED) {
4678
4655
  peg$savedPos = s0;
4679
- s0 = peg$f75(s1, s3);
4656
+ s0 = peg$f74(s1, s3);
4680
4657
  } else {
4681
4658
  peg$currPos = s0;
4682
4659
  s0 = peg$FAILED;
@@ -4856,11 +4833,9 @@ const peg$allowedStartRules = [
4856
4833
  "stringLiteral",
4857
4834
  "templateDocument",
4858
4835
  "templateDocumentChar",
4859
- "templateDocumentContents",
4860
4836
  "templateDocumentText",
4861
4837
  "templateLiteral",
4862
4838
  "templateLiteralChar",
4863
- "templateLiteralContents",
4864
4839
  "templateLiteralText",
4865
4840
  "templateSubstitution",
4866
4841
  "textChar",
@@ -76,22 +76,6 @@ export function downgradeReference(code) {
76
76
  }
77
77
  }
78
78
 
79
- // Return true if the code will generate an async object.
80
- function isCodeForAsyncObject(code) {
81
- if (!(code instanceof Array)) {
82
- return false;
83
- }
84
- if (code[0] !== ops.object) {
85
- return false;
86
- }
87
- // Are any of the properties getters?
88
- const entries = code.slice(1);
89
- const hasGetter = entries.some(([key, value]) => {
90
- return value instanceof Array && value[0] === ops.getter;
91
- });
92
- return hasGetter;
93
- }
94
-
95
79
  export function makeArray(entries) {
96
80
  let currentEntries = [];
97
81
  const spreads = [];
@@ -260,17 +244,24 @@ export function makeObject(entries, op) {
260
244
  continue;
261
245
  }
262
246
 
263
- if (
264
- value instanceof Array &&
265
- value[0] === ops.getter &&
266
- value[1] instanceof Array &&
267
- value[1][0] === ops.literal
268
- ) {
269
- // Simplify a getter for a primitive value to a regular property
270
- value = value[1];
271
- } else if (isCodeForAsyncObject(value)) {
272
- // Add a trailing slash to key to indicate value is a subtree
273
- key = key + "/";
247
+ if (value instanceof Array) {
248
+ if (
249
+ value[0] === ops.getter &&
250
+ value[1] instanceof Array &&
251
+ value[1][0] === ops.literal
252
+ ) {
253
+ // Optimize a getter for a primitive value to a regular property
254
+ value = value[1];
255
+ }
256
+ // else if (
257
+ // value[0] === ops.object ||
258
+ // (value[0] === ops.getter &&
259
+ // value[1] instanceof Array &&
260
+ // (value[1][0] === ops.object || value[1][0] === ops.merge))
261
+ // ) {
262
+ // // Add a trailing slash to key to indicate value is a subtree
263
+ // key = trailingSlash.add(key);
264
+ // }
274
265
  }
275
266
 
276
267
  currentEntries.push([key, value]);
@@ -1,4 +1,10 @@
1
- import { extension, ObjectTree, symbols, Tree } from "@weborigami/async-tree";
1
+ import {
2
+ extension,
3
+ ObjectTree,
4
+ symbols,
5
+ trailingSlash,
6
+ Tree,
7
+ } from "@weborigami/async-tree";
2
8
  import { handleExtension } from "./handlers.js";
3
9
  import { evaluate, ops } from "./internal.js";
4
10
 
@@ -11,8 +17,8 @@ import { evaluate, ops } from "./internal.js";
11
17
  *
12
18
  * 1. A primitive value (string, etc.). This will be defined directly as an
13
19
  * object property.
14
- * 1. An immediate code entry. This will be evaluated during this call and its
15
- * result defined as an object property.
20
+ * 1. An eager (as opposed to lazy) code entry. This will be evaluated during
21
+ * this call and its result defined as an object property.
16
22
  * 1. A code entry that starts with ops.getter. This will be defined as a
17
23
  * property getter on the object.
18
24
  *
@@ -25,15 +31,9 @@ export default async function expressionObject(entries, parent) {
25
31
  if (parent !== null && !Tree.isAsyncTree(parent)) {
26
32
  throw new TypeError(`Parent must be an AsyncTree or null`);
27
33
  }
28
- Object.defineProperty(object, symbols.parent, {
29
- configurable: true,
30
- enumerable: false,
31
- value: parent,
32
- writable: true,
33
- });
34
34
 
35
35
  let tree;
36
- const immediateProperties = [];
36
+ const eagerProperties = [];
37
37
  for (let [key, value] of entries) {
38
38
  // Determine if we need to define a getter or a regular property. If the key
39
39
  // has an extension, we need to define a getter. If the value is code (an
@@ -61,7 +61,6 @@ export default async function expressionObject(entries, parent) {
61
61
 
62
62
  if (defineProperty) {
63
63
  // Define simple property
64
- // object[key] = value;
65
64
  Object.defineProperty(object, key, {
66
65
  configurable: true,
67
66
  enumerable,
@@ -74,7 +73,7 @@ export default async function expressionObject(entries, parent) {
74
73
  if (value[0] === ops.getter) {
75
74
  code = value[1];
76
75
  } else {
77
- immediateProperties.push(key);
76
+ eagerProperties.push(key);
78
77
  code = value;
79
78
  }
80
79
 
@@ -102,9 +101,25 @@ export default async function expressionObject(entries, parent) {
102
101
  }
103
102
  }
104
103
 
104
+ // Attach a keys method
105
+ Object.defineProperty(object, symbols.keys, {
106
+ configurable: true,
107
+ enumerable: false,
108
+ value: () => keys(object, eagerProperties, entries),
109
+ writable: true,
110
+ });
111
+
112
+ // Attach the parent
113
+ Object.defineProperty(object, symbols.parent, {
114
+ configurable: true,
115
+ enumerable: false,
116
+ value: parent,
117
+ writable: true,
118
+ });
119
+
105
120
  // Evaluate any properties that were declared as immediate: get their value
106
121
  // and overwrite the property getter with the actual value.
107
- for (const key of immediateProperties) {
122
+ for (const key of eagerProperties) {
108
123
  const value = await object[key];
109
124
  // @ts-ignore Unclear why TS thinks `object` might be undefined here
110
125
  const enumerable = Object.getOwnPropertyDescriptor(object, key).enumerable;
@@ -118,3 +133,31 @@ export default async function expressionObject(entries, parent) {
118
133
 
119
134
  return object;
120
135
  }
136
+
137
+ function entryKey(object, eagerProperties, entry) {
138
+ const [key, value] = entry;
139
+
140
+ const hasExplicitSlash = trailingSlash.has(key);
141
+ if (hasExplicitSlash) {
142
+ // Return key as is
143
+ return key;
144
+ }
145
+
146
+ // If eager property value is treelike, add slash to the key
147
+ if (eagerProperties.includes(key) && Tree.isTreelike(object[key])) {
148
+ return trailingSlash.add(key);
149
+ }
150
+
151
+ // If entry will definitely create a subtree, add a trailing slash
152
+ const entryCreatesSubtree =
153
+ value instanceof Array &&
154
+ (value[0] === ops.object ||
155
+ (value[0] === ops.getter &&
156
+ value[1] instanceof Array &&
157
+ (value[1][0] === ops.object || value[1][0] === ops.merge)));
158
+ return trailingSlash.toggle(key, entryCreatesSubtree);
159
+ }
160
+
161
+ function keys(object, eagerProperties, entries) {
162
+ return entries.map((entry) => entryKey(object, eagerProperties, entry));
163
+ }
@@ -12,6 +12,7 @@ import {
12
12
  concat as treeConcat,
13
13
  } from "@weborigami/async-tree";
14
14
  import os from "node:os";
15
+ import taggedTemplateIndent from "../../src/runtime/taggedTemplateIndent.js";
15
16
  import { builtinReferenceError, scopeReferenceError } from "./errors.js";
16
17
  import expressionObject from "./expressionObject.js";
17
18
  import { evaluate } from "./internal.js";
@@ -447,10 +448,18 @@ addOpLabel(subtraction, "«ops.subtraction»");
447
448
  * Apply the default tagged template function.
448
449
  */
449
450
  export function template(strings, ...values) {
450
- return taggedTemplate(strings, values);
451
+ return taggedTemplate(strings, ...values);
451
452
  }
452
453
  addOpLabel(template, "«ops.template»");
453
454
 
455
+ /**
456
+ * Apply the tagged template indent function.
457
+ */
458
+ export function templateIndent(strings, ...values) {
459
+ return taggedTemplateIndent(strings, ...values);
460
+ }
461
+ addOpLabel(templateIndent, "«ops.templateIndent");
462
+
454
463
  /**
455
464
  * Traverse a path of keys through a tree.
456
465
  */
@@ -1,6 +1,6 @@
1
1
  // Default JavaScript tagged template function splices strings and values
2
2
  // together.
3
- export default function defaultTemplateJoin(strings, values) {
3
+ export default function taggedTemplate(strings, ...values) {
4
4
  let result = strings[0];
5
5
  for (let i = 0; i < values.length; i++) {
6
6
  result += values[i] + strings[i + 1];
@@ -0,0 +1,115 @@
1
+ const lastLineWhitespaceRegex = /\n(?<indent>[ \t]*)$/;
2
+
3
+ const mapStringsToModifications = new Map();
4
+
5
+ /**
6
+ * Normalize indentation in a tagged template string.
7
+ *
8
+ * @param {TemplateStringsArray} strings
9
+ * @param {...any} values
10
+ * @returns {string}
11
+ */
12
+ export default function indent(strings, ...values) {
13
+ let modified = mapStringsToModifications.get(strings);
14
+ if (!modified) {
15
+ modified = modifyStrings(strings);
16
+ mapStringsToModifications.set(strings, modified);
17
+ }
18
+ const { blockIndentations, strings: modifiedStrings } = modified;
19
+ return joinBlocks(modifiedStrings, values, blockIndentations);
20
+ }
21
+
22
+ // Join strings and values, applying the given block indentation to the lines of
23
+ // values for block placholders.
24
+ function joinBlocks(strings, values, blockIndentations) {
25
+ let result = strings[0];
26
+ for (let i = 0; i < values.length; i++) {
27
+ let text = values[i];
28
+ if (text) {
29
+ const blockIndentation = blockIndentations[i];
30
+ if (blockIndentation) {
31
+ const lines = text.split("\n");
32
+ text = "";
33
+ if (lines.at(-1) === "") {
34
+ // Drop empty last line
35
+ lines.pop();
36
+ }
37
+ for (let line of lines) {
38
+ text += blockIndentation + line + "\n";
39
+ }
40
+ }
41
+ result += text;
42
+ }
43
+ result += strings[i + 1];
44
+ }
45
+ return result;
46
+ }
47
+
48
+ // Given an array of template boilerplate strings, return an object { modified,
49
+ // blockIndentations } where `strings` is the array of strings with indentation
50
+ // removed, and `blockIndentations` is an array of indentation strings for each
51
+ // block placeholder.
52
+ function modifyStrings(strings) {
53
+ // Phase one: Identify the indentation based on the first real line of the
54
+ // first string (skipping the initial newline), and remove this indentation
55
+ // from all lines of all strings.
56
+ let indent;
57
+ if (strings.length > 0 && strings[0].startsWith("\n")) {
58
+ // Look for indenttation
59
+ const firstLineWhitespaceRegex = /^\n(?<indent>[ \t]*)/;
60
+ const match = strings[0].match(firstLineWhitespaceRegex);
61
+ indent = match?.groups.indent;
62
+ }
63
+
64
+ // Determine the modified strings. If this invoked as a JS tagged template
65
+ // literal, the `strings` argument will be an odd array-ish object that we'll
66
+ // want to convert to a real array.
67
+ let modified;
68
+ if (indent) {
69
+ // De-indent the strings.
70
+ const indentationRegex = new RegExp(`\n${indent}`, "g");
71
+ // The `replaceAll` also converts strings to a real array.
72
+ modified = strings.map((string) =>
73
+ string.replaceAll(indentationRegex, "\n")
74
+ );
75
+ // Remove indentation from last line of last string
76
+ modified[modified.length - 1] = modified
77
+ .at(-1)
78
+ .replace(lastLineWhitespaceRegex, "\n");
79
+ } else {
80
+ // No indentation; just copy the strings so we have a real array
81
+ modified = strings.slice();
82
+ }
83
+
84
+ // Phase two: Identify any block placholders, identify and remove their
85
+ // preceding indentation, and remove the following newline. Work backward from
86
+ // the end towards the start because we're modifying the strings in place and
87
+ // our pattern matching won't work going forward from start to end.
88
+ let blockIndentations = [];
89
+ for (let i = modified.length - 2; i >= 0; i--) {
90
+ // Get the modified before and after substitution with index `i`
91
+ const beforeString = modified[i];
92
+ const afterString = modified[i + 1];
93
+ const match = beforeString.match(lastLineWhitespaceRegex);
94
+ if (match && afterString.startsWith("\n")) {
95
+ // The substitution between these strings is a block substitution
96
+ let blockIndentation = match.groups.indent;
97
+ blockIndentations[i] = blockIndentation;
98
+ // Trim the before and after strings
99
+ if (blockIndentation) {
100
+ modified[i] = beforeString.slice(0, -blockIndentation.length);
101
+ }
102
+ modified[i + 1] = afterString.slice(1);
103
+ }
104
+ }
105
+
106
+ // Remove newline from start of first string *after* removing indentation.
107
+ if (modified[0].startsWith("\n")) {
108
+ modified[0] = modified[0].slice(1);
109
+ }
110
+
111
+ return {
112
+ blockIndentations,
113
+ strings: modified,
114
+ };
115
+ }
@@ -10,7 +10,7 @@ const shared = new ObjectTree({
10
10
  name: "Alice",
11
11
  });
12
12
 
13
- describe.only("compile", () => {
13
+ describe("compile", () => {
14
14
  test("array", async () => {
15
15
  await assertCompile("[]", []);
16
16
  await assertCompile("[ 1, 2, 3, ]", [1, 2, 3]);
@@ -46,12 +46,14 @@ describe.only("compile", () => {
46
46
  test("async object", async () => {
47
47
  const fn = compile.expression("{ a: { b = name }}");
48
48
  const object = await fn.call(shared);
49
- assert.deepEqual(await object["a/"].b, "Alice");
49
+ assert.deepEqual(await object.a.b, "Alice");
50
50
  });
51
51
 
52
52
  test("templateDocument", async () => {
53
- const fn = compile.templateDocument("Documents can contain ` backticks");
54
- const templateFn = await fn.call(shared);
53
+ const defineTemplateFn = compile.templateDocument(
54
+ "Documents can contain ` backticks"
55
+ );
56
+ const templateFn = await defineTemplateFn.call(null);
55
57
  const value = await templateFn.call(null);
56
58
  assert.deepEqual(value, "Documents can contain ` backticks");
57
59
  });
@@ -85,7 +87,7 @@ describe.only("compile", () => {
85
87
  assert.equal(bob, "Hello, Bob!");
86
88
  });
87
89
 
88
- test.only("converts non-local ops.scope calls to ops.external", async () => {
90
+ test("converts non-local ops.scope calls to ops.external", async () => {
89
91
  const expression = `
90
92
  (name) => {
91
93
  a: 1
@@ -108,6 +110,18 @@ describe.only("compile", () => {
108
110
  ],
109
111
  ]);
110
112
  });
113
+
114
+ test("can apply a macro", async () => {
115
+ const literal = [ops.literal, 1];
116
+ const expression = `{ a: literal }`;
117
+ const fn = compile.expression(expression, {
118
+ macros: {
119
+ literal,
120
+ },
121
+ });
122
+ const code = fn.code;
123
+ assert.deepEqual(stripCodeLocations(code), [ops.object, ["a", literal]]);
124
+ });
111
125
  });
112
126
 
113
127
  async function assertCompile(text, expected) {
@@ -667,7 +667,7 @@ describe("Origami parser", () => {
667
667
  assertParse("objectLiteral", "{ a: { b = fn() } }", [
668
668
  ops.object,
669
669
  [
670
- "a/",
670
+ "a",
671
671
  [ops.object, ["b", [ops.getter, [[ops.builtin, "fn"], undefined]]]],
672
672
  ],
673
673
  ]);
@@ -970,7 +970,7 @@ describe("Origami parser", () => {
970
970
  ops.lambda,
971
971
  ["_"],
972
972
  [
973
- ops.template,
973
+ ops.templateIndent,
974
974
  [ops.literal, ["hello", "world"]],
975
975
  [ops.concat, [ops.scope, "foo"]],
976
976
  ],
@@ -978,7 +978,10 @@ describe("Origami parser", () => {
978
978
  assertParse("templateDocument", "Documents can contain ` backticks", [
979
979
  ops.lambda,
980
980
  ["_"],
981
- [ops.template, [ops.literal, ["Documents can contain ` backticks"]]],
981
+ [
982
+ ops.templateIndent,
983
+ [ops.literal, ["Documents can contain ` backticks"]],
984
+ ],
982
985
  ]);
983
986
  });
984
987
 
@@ -5,7 +5,7 @@ import { describe, test } from "node:test";
5
5
  import expressionObject from "../../src/runtime/expressionObject.js";
6
6
  import { ops } from "../../src/runtime/internal.js";
7
7
 
8
- describe("expressionObject", () => {
8
+ describe.only("expressionObject", () => {
9
9
  test("can instantiate an object", async () => {
10
10
  const scope = new ObjectTree({
11
11
  upper: (s) => s.toUpperCase(),
@@ -73,4 +73,22 @@ describe("expressionObject", () => {
73
73
  assert.deepEqual(Object.keys(object), ["visible"]);
74
74
  assert.equal(object["hidden"], "shh");
75
75
  });
76
+
77
+ test.only("provides a symbols.keys method", async () => {
78
+ const entries = [
79
+ // Will return a tree, should have a slash
80
+ ["getter", [ops.getter, [ops.object, ["b", [ops.literal, 2]]]]],
81
+ ["hasSlash/", "This isn't really a tree but says it is"],
82
+ ["message", "Hello"],
83
+ // Immediate treelike value, should have a slash
84
+ ["object", [ops.object, ["b", [ops.literal, 2]]]],
85
+ ];
86
+ const object = await expressionObject(entries, null);
87
+ assert.deepEqual(object[symbols.keys](), [
88
+ "getter/",
89
+ "hasSlash/",
90
+ "message",
91
+ "object/",
92
+ ]);
93
+ });
76
94
  });
@@ -0,0 +1,44 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import indent from "../../src/runtime/taggedTemplateIndent.js";
4
+
5
+ describe("taggedTemplateIndent", () => {
6
+ test("joins strings and values together if template isn't a block template", () => {
7
+ const result = indent`a ${"b"} c`;
8
+ assert.equal(result, "a b c");
9
+ });
10
+
11
+ test("removes first and last lines if template is a block template", () => {
12
+ const actual = indent`
13
+ <p>
14
+ Hello, ${"Alice"}!
15
+ </p>
16
+ `;
17
+ const expected = `
18
+ <p>
19
+ Hello, Alice!
20
+ </p>
21
+ `.trimStart();
22
+ assert.equal(actual, expected);
23
+ });
24
+
25
+ test("indents all lines in a block substitution", () => {
26
+ const lines = `
27
+ Line 1
28
+ Line 2
29
+ Line 3`.trimStart();
30
+ const actual = indent`
31
+ <main>
32
+ ${lines}
33
+ </main>
34
+ `;
35
+ const expected = `
36
+ <main>
37
+ Line 1
38
+ Line 2
39
+ Line 3
40
+ </main>
41
+ `.trimStart();
42
+ assert.equal(actual, expected);
43
+ });
44
+ });