@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 +21 -0
- package/main.js +1 -0
- package/package.json +7 -7
- package/src/compiler/compile.js +43 -23
- package/src/compiler/origami.pegjs +7 -14
- package/src/compiler/parse.js +73 -98
- package/src/compiler/parserHelpers.js +18 -27
- package/src/runtime/expressionObject.js +56 -13
- package/src/runtime/ops.js +10 -1
- package/src/runtime/taggedTemplate.js +1 -1
- package/src/runtime/taggedTemplateIndent.js +115 -0
- package/test/compiler/compile.test.js +19 -5
- package/test/compiler/parse.test.js +6 -3
- package/test/runtime/expressionObject.test.js +19 -1
- package/test/runtime/taggedTemplateIndent.test.js +44 -0
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.
|
|
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.
|
|
10
|
-
"peggy": "4.0.
|
|
11
|
-
"typescript": "5.
|
|
12
|
-
"yaml": "2.
|
|
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.
|
|
16
|
-
"@weborigami/types": "0.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": {
|
package/src/compiler/compile.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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 (
|
|
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
|
|
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
|
-
=
|
|
575
|
-
return annotate(
|
|
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
|
-
= "`"
|
|
596
|
-
return annotate(makeTemplate(ops.template,
|
|
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* {
|
package/src/compiler/parse.js
CHANGED
|
@@ -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,
|
|
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(
|
|
607
|
-
return annotate(
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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$
|
|
612
|
+
var peg$f70 = function(chars) {
|
|
613
613
|
return chars.join("");
|
|
614
614
|
};
|
|
615
|
-
var peg$
|
|
616
|
-
return annotate(makeTemplate(ops.template,
|
|
615
|
+
var peg$f71 = function(head, tail) {
|
|
616
|
+
return annotate(makeTemplate(ops.template, head, tail), location());
|
|
617
617
|
};
|
|
618
|
-
var peg$
|
|
618
|
+
var peg$f72 = function(chars) {
|
|
619
619
|
return chars.join("");
|
|
620
620
|
};
|
|
621
|
-
var peg$
|
|
621
|
+
var peg$f73 = function(expression) {
|
|
622
622
|
return annotate(expression, location());
|
|
623
623
|
};
|
|
624
|
-
var peg$
|
|
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$
|
|
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
|
-
|
|
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$
|
|
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$
|
|
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
|
-
|
|
4495
|
+
s4 = peg$c59;
|
|
4484
4496
|
peg$currPos++;
|
|
4485
4497
|
} else {
|
|
4486
|
-
|
|
4498
|
+
s4 = peg$FAILED;
|
|
4487
4499
|
if (peg$silentFails === 0) { peg$fail(peg$e98); }
|
|
4488
4500
|
}
|
|
4489
|
-
if (
|
|
4501
|
+
if (s4 !== peg$FAILED) {
|
|
4490
4502
|
peg$savedPos = s0;
|
|
4491
|
-
s0 = peg$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
//
|
|
273
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|
package/src/runtime/ops.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
49
|
+
assert.deepEqual(await object.a.b, "Alice");
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
test("templateDocument", async () => {
|
|
53
|
-
const
|
|
54
|
-
|
|
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
|
|
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.
|
|
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
|
-
[
|
|
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
|
+
});
|