@weborigami/language 0.0.35
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/index.ts +30 -0
- package/main.js +21 -0
- package/package.json +24 -0
- package/src/compiler/code.d.ts +3 -0
- package/src/compiler/compile.js +55 -0
- package/src/compiler/origami.pegjs +277 -0
- package/src/compiler/parse.js +2292 -0
- package/src/compiler/parserHelpers.js +26 -0
- package/src/runtime/EventTargetMixin.d.ts +9 -0
- package/src/runtime/EventTargetMixin.js +117 -0
- package/src/runtime/ExpressionTree.js +20 -0
- package/src/runtime/FileLoadersTransform.d.ts +5 -0
- package/src/runtime/FileLoadersTransform.js +43 -0
- package/src/runtime/ImportModulesMixin.d.ts +5 -0
- package/src/runtime/ImportModulesMixin.js +48 -0
- package/src/runtime/InheritScopeMixin.js +34 -0
- package/src/runtime/InheritScopeMixin.ts +9 -0
- package/src/runtime/InvokeFunctionsTransform.d.ts +5 -0
- package/src/runtime/InvokeFunctionsTransform.js +27 -0
- package/src/runtime/OrigamiFiles.d.ts +11 -0
- package/src/runtime/OrigamiFiles.js +9 -0
- package/src/runtime/OrigamiTransform.d.ts +11 -0
- package/src/runtime/OrigamiTransform.js +11 -0
- package/src/runtime/OrigamiTree.js +4 -0
- package/src/runtime/ReadMe.md +1 -0
- package/src/runtime/Scope.js +89 -0
- package/src/runtime/TreeEvent.js +6 -0
- package/src/runtime/WatchFilesMixin.d.ts +5 -0
- package/src/runtime/WatchFilesMixin.js +58 -0
- package/src/runtime/concatTreeValues.js +46 -0
- package/src/runtime/evaluate.js +90 -0
- package/src/runtime/expressionFunction.js +33 -0
- package/src/runtime/extname.js +20 -0
- package/src/runtime/format.js +126 -0
- package/src/runtime/functionResultsMap.js +28 -0
- package/src/runtime/internal.js +20 -0
- package/src/runtime/ops.js +222 -0
- package/test/compiler/compile.test.js +64 -0
- package/test/compiler/parse.test.js +389 -0
- package/test/runtime/EventTargetMixin.test.js +68 -0
- package/test/runtime/ExpressionTree.test.js +27 -0
- package/test/runtime/FileLoadersTransform.test.js +41 -0
- package/test/runtime/InheritScopeMixin.test.js +29 -0
- package/test/runtime/OrigamiFiles.test.js +37 -0
- package/test/runtime/Scope.test.js +37 -0
- package/test/runtime/concatTreeValues.js +20 -0
- package/test/runtime/evaluate.test.js +55 -0
- package/test/runtime/fixtures/foo.js +1 -0
- package/test/runtime/fixtures/makeTest/a +1 -0
- package/test/runtime/fixtures/makeTest/b = a +0 -0
- package/test/runtime/fixtures/metagraphs/foo.txt +1 -0
- package/test/runtime/fixtures/metagraphs/greeting = this('world').js +3 -0
- package/test/runtime/fixtures/metagraphs/obj = this.json +5 -0
- package/test/runtime/fixtures/metagraphs/sample.txt = this().js +3 -0
- package/test/runtime/fixtures/metagraphs/string = this.json +1 -0
- package/test/runtime/fixtures/metagraphs/value = fn() +0 -0
- package/test/runtime/fixtures/programs/context.yaml +4 -0
- package/test/runtime/fixtures/programs/files.yaml +2 -0
- package/test/runtime/fixtures/programs/obj.yaml +3 -0
- package/test/runtime/fixtures/programs/simple.yaml +2 -0
- package/test/runtime/fixtures/subgraph = this.js +5 -0
- package/test/runtime/fixtures/templates/greet.orit +4 -0
- package/test/runtime/fixtures/templates/index.orit +15 -0
- package/test/runtime/fixtures/templates/names.yaml +3 -0
- package/test/runtime/fixtures/templates/plain.txt +1 -0
- package/test/runtime/fixtures/virtualKeys/.keys.json +1 -0
- package/test/runtime/format.test.js +66 -0
- package/test/runtime/functionResultsMap.test.js +27 -0
- package/test/runtime/ops.test.js +111 -0
package/index.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { AsyncTree } from "@weborigami/types";
|
|
2
|
+
import { StringLike } from "../async-tree/index.js";
|
|
3
|
+
|
|
4
|
+
export * from "./main.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A class constructor is an object with a `new` method that returns an
|
|
8
|
+
* instance of the indicated type.
|
|
9
|
+
*/
|
|
10
|
+
export type Constructor<T> = new (...args: any[]) => T;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A function that can convert a string-like input value into some live object.
|
|
14
|
+
*/
|
|
15
|
+
export type FileUnpackFunction = (
|
|
16
|
+
input: StringLike,
|
|
17
|
+
options?: { key?: any, parent?: AsyncTree | null }
|
|
18
|
+
) => any;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A mixin is a function that takes an existing class and returns a new class.
|
|
22
|
+
*
|
|
23
|
+
* The use of a generic type `T` here is a way of indicating that the members of
|
|
24
|
+
* the supplied base class automatically pass through to the result. That
|
|
25
|
+
* ensures the use of the mixin doesn't accidentally hide members of the class
|
|
26
|
+
* passed to the mixin.
|
|
27
|
+
*/
|
|
28
|
+
export type Mixin<MixinMembers> = <T>(
|
|
29
|
+
Base: Constructor<T>
|
|
30
|
+
) => Constructor<T & MixinMembers>;
|
package/main.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export * from "./src/runtime/internal.js";
|
|
2
|
+
|
|
3
|
+
export * as compile from "./src/compiler/compile.js";
|
|
4
|
+
export { default as EventTargetMixin } from "./src/runtime/EventTargetMixin.js";
|
|
5
|
+
export { default as ExpressionTree } from "./src/runtime/ExpressionTree.js";
|
|
6
|
+
export { default as FileLoadersTransform } from "./src/runtime/FileLoadersTransform.js";
|
|
7
|
+
export { default as ImportModulesMixin } from "./src/runtime/ImportModulesMixin.js";
|
|
8
|
+
export { default as InheritScopeMixin } from "./src/runtime/InheritScopeMixin.js";
|
|
9
|
+
export { default as InvokeFunctionsTransform } from "./src/runtime/InvokeFunctionsTransform.js";
|
|
10
|
+
export { default as OrigamiFiles } from "./src/runtime/OrigamiFiles.js";
|
|
11
|
+
export { default as OrigamiTransform } from "./src/runtime/OrigamiTransform.js";
|
|
12
|
+
export { default as OrigamiTree } from "./src/runtime/OrigamiTree.js";
|
|
13
|
+
export { default as Scope } from "./src/runtime/Scope.js";
|
|
14
|
+
export { default as TreeEvent } from "./src/runtime/TreeEvent.js";
|
|
15
|
+
export { default as WatchFilesMixin } from "./src/runtime/WatchFilesMixin.js";
|
|
16
|
+
export { default as concatTreeValues } from "./src/runtime/concatTreeValues.js";
|
|
17
|
+
export { default as evaluate } from "./src/runtime/evaluate.js";
|
|
18
|
+
export * as expressionFunction from "./src/runtime/expressionFunction.js";
|
|
19
|
+
export { default as extname } from "./src/runtime/extname.js";
|
|
20
|
+
export { default as format } from "./src/runtime/format.js";
|
|
21
|
+
export { default as functionResultsMap } from "./src/runtime/functionResultsMap.js";
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@weborigami/language",
|
|
3
|
+
"version": "0.0.35",
|
|
4
|
+
"description": "Graph Origami expression language compiler and runtime",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./main.js",
|
|
7
|
+
"types": "./index.ts",
|
|
8
|
+
"devDependencies": {
|
|
9
|
+
"@types/node": "20.8.10",
|
|
10
|
+
"typescript": "5.2.2"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@weborigami/types": "0.0.35",
|
|
14
|
+
"@weborigami/async-tree": "https://gitpkg.now.sh/GraphOrigami/origami/async-tree?main",
|
|
15
|
+
"peggy": "3.0.2",
|
|
16
|
+
"watcher": "2.3.0"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "peggy --allowed-start-rules=\"*\" --format es src/compiler/origami.pegjs --output src/compiler/parse.js",
|
|
20
|
+
"prepublishOnly": "npm run build",
|
|
21
|
+
"test": "node --test",
|
|
22
|
+
"typecheck": "tsc"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createExpressionFunction } from "../runtime/expressionFunction.js";
|
|
2
|
+
import { parse } from "./parse.js";
|
|
3
|
+
|
|
4
|
+
function compile(text, startRule) {
|
|
5
|
+
// Trim whitespace from template blocks before we begin lexing, as our
|
|
6
|
+
// heuristics are non-local and hard to implement in our parser.
|
|
7
|
+
const preprocessed = trimTemplateWhitespace(text);
|
|
8
|
+
const code = parse(preprocessed, { startRule });
|
|
9
|
+
const fn = createExpressionFunction(code);
|
|
10
|
+
return fn;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function expression(text) {
|
|
14
|
+
return compile(text, "expression");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function templateDocument(text) {
|
|
18
|
+
return compile(text, "templateDocument");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Trim the whitespace around and in substitution blocks in a template. There's
|
|
22
|
+
// no explicit syntax for blocks, but we infer them as any place where a
|
|
23
|
+
// substitution itself contains a multi-line template literal.
|
|
24
|
+
//
|
|
25
|
+
// Example:
|
|
26
|
+
//
|
|
27
|
+
// {{ if `
|
|
28
|
+
// true text
|
|
29
|
+
// `, `
|
|
30
|
+
// false text
|
|
31
|
+
// ` }}
|
|
32
|
+
//
|
|
33
|
+
// Case 1: a substitution that starts the text or starts a line (there's only
|
|
34
|
+
// whitespace before the `{{`), and has the line end with the start of a
|
|
35
|
+
// template literal (there's only whitespace after the backtick) marks the start
|
|
36
|
+
// of a block.
|
|
37
|
+
//
|
|
38
|
+
// Case 2: a line in the middle that ends one template literal and starts
|
|
39
|
+
// another is an internal break in the block. Edge case: three backticks in a
|
|
40
|
+
// row, like ```, are common in markdown and are not treated as a break.
|
|
41
|
+
//
|
|
42
|
+
// Case 3: a line that ends a template literal and ends with `}}` or ends the
|
|
43
|
+
// text marks the end of the block.
|
|
44
|
+
//
|
|
45
|
+
// In all three cases, we trim spaces and tabs from the start and end of the
|
|
46
|
+
// line. In case 1, we also remove the preceding newline.
|
|
47
|
+
function trimTemplateWhitespace(text) {
|
|
48
|
+
const regex1 = /(^|\n)[ \t]*({{.*?`)[ \t]*\n/g;
|
|
49
|
+
const regex2 = /\n[ \t]*(`(?!`).*?`)[ \t]*\n/g;
|
|
50
|
+
const regex3 = /\n[ \t]*(`(?!`).*?}})[ \t]*(?:\n|$)/g;
|
|
51
|
+
const trimBlockStarts = text.replace(regex1, "$1$2");
|
|
52
|
+
const trimBlockBreaks = trimBlockStarts.replace(regex2, "\n$1");
|
|
53
|
+
const trimBlockEnds = trimBlockBreaks.replace(regex3, "\n$1");
|
|
54
|
+
return trimBlockEnds;
|
|
55
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
{{
|
|
2
|
+
//
|
|
3
|
+
// Origami language parser
|
|
4
|
+
//
|
|
5
|
+
// Generate the parser via `npm build`.
|
|
6
|
+
// @ts-nocheck
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
import * as ops from "../runtime/ops.js";
|
|
10
|
+
import { makeFunctionCall, makeTemplate } from "./parserHelpers.js";
|
|
11
|
+
}}
|
|
12
|
+
|
|
13
|
+
// A block of optional whitespace
|
|
14
|
+
__
|
|
15
|
+
= (inlineSpace / newLine / comment)* { return ""; }
|
|
16
|
+
|
|
17
|
+
// A filesystem path that begins with a slash: `/foo/bar`
|
|
18
|
+
absoluteFilePath
|
|
19
|
+
= path:leadingSlashPath { return [[ops.filesRoot], ...path]; }
|
|
20
|
+
|
|
21
|
+
// A chain of arguments: `(arg1)(arg2)(arg3)`
|
|
22
|
+
argsChain
|
|
23
|
+
= parts:(parensArgs / leadingSlashPath)+ { return parts; }
|
|
24
|
+
|
|
25
|
+
// An assignment statement: `foo = 1`
|
|
26
|
+
assignment
|
|
27
|
+
= @identifier __ "=" __ @expr
|
|
28
|
+
|
|
29
|
+
assignmentOrShorthand
|
|
30
|
+
= assignment
|
|
31
|
+
/ key:identifier { return [key, [ops.inherited, key]]; }
|
|
32
|
+
|
|
33
|
+
array
|
|
34
|
+
= "[" __ list:list? "]" { return [ops.array, ...(list ?? [])]; }
|
|
35
|
+
|
|
36
|
+
// Something that can be called. This is more restrictive than the `expr`
|
|
37
|
+
// parser; it doesn't accept regular function calls.
|
|
38
|
+
callTarget
|
|
39
|
+
= absoluteFilePath
|
|
40
|
+
/ array
|
|
41
|
+
/ object
|
|
42
|
+
/ tree
|
|
43
|
+
/ lambda
|
|
44
|
+
/ protocolCall
|
|
45
|
+
/ group
|
|
46
|
+
/ scopeReference
|
|
47
|
+
|
|
48
|
+
// A single line comment
|
|
49
|
+
comment
|
|
50
|
+
= "#" [^\n\r]*
|
|
51
|
+
|
|
52
|
+
digits
|
|
53
|
+
= @[0-9]+
|
|
54
|
+
|
|
55
|
+
doubleQuoteString
|
|
56
|
+
= '"' chars:doubleQuoteStringChar* '"' { return chars.join(""); }
|
|
57
|
+
|
|
58
|
+
doubleQuoteStringChar
|
|
59
|
+
= !('"' / newLine) @textChar
|
|
60
|
+
|
|
61
|
+
escapedChar
|
|
62
|
+
= "\\" @.
|
|
63
|
+
|
|
64
|
+
// An Origami expression, no leading/trailing whitespace
|
|
65
|
+
expr
|
|
66
|
+
// Try function calls first, as they can start with expression types that
|
|
67
|
+
// follow (array, object, etc.); we want to parse the largest thing first.
|
|
68
|
+
= implicitParensCall
|
|
69
|
+
/ functionComposition
|
|
70
|
+
// Then try parsers that look for a distinctive token at the start: an opening
|
|
71
|
+
// slash, bracket, curly brace, etc.
|
|
72
|
+
/ absoluteFilePath
|
|
73
|
+
/ array
|
|
74
|
+
/ object
|
|
75
|
+
/ tree
|
|
76
|
+
/ lambda
|
|
77
|
+
/ templateLiteral
|
|
78
|
+
/ group
|
|
79
|
+
/ string
|
|
80
|
+
/ number
|
|
81
|
+
// Protocol calls are distinguished by a colon, but it's not at the start.
|
|
82
|
+
/ protocolCall
|
|
83
|
+
// Least distinctive option is a simple scope reference, so it comes last.
|
|
84
|
+
/ scopeReference
|
|
85
|
+
|
|
86
|
+
// Top-level Origami expression, possible leading/trailing whitepsace.
|
|
87
|
+
expression "Origami expression"
|
|
88
|
+
= __ @expr __
|
|
89
|
+
|
|
90
|
+
float
|
|
91
|
+
= sign? digits? "." digits {
|
|
92
|
+
return parseFloat(text());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Parse a function and its arguments, e.g. `fn(arg)`, possibly part of a chain
|
|
96
|
+
// of function calls, like `fn(arg1)(arg2)(arg3)`.
|
|
97
|
+
functionComposition
|
|
98
|
+
= target:callTarget chain:argsChain { return makeFunctionCall(target, chain); }
|
|
99
|
+
|
|
100
|
+
// An expression in parentheses: `(foo)`
|
|
101
|
+
group
|
|
102
|
+
= "(" __ @expr __ ")"
|
|
103
|
+
|
|
104
|
+
identifier
|
|
105
|
+
= chars:identifierChar+ { return chars.join(""); }
|
|
106
|
+
|
|
107
|
+
identifierChar
|
|
108
|
+
= [^(){}\[\],/:=\`"'\\# \t\n\r] // No unescaped whitespace or special chars
|
|
109
|
+
/ escapedChar
|
|
110
|
+
|
|
111
|
+
// A function call with implicit parentheses: `fn 1, 2, 3`
|
|
112
|
+
implicitParensCall
|
|
113
|
+
= target:(functionComposition / callTarget) inlineSpace+ args:list {
|
|
114
|
+
return [target, ...args];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// A host identifier that may include a colon and port number: `example.com:80`.
|
|
118
|
+
// This is used as a special case at the head of a path, where we want to
|
|
119
|
+
// interpret a colon as part of a text identifier.
|
|
120
|
+
host
|
|
121
|
+
= identifier (":" number)? { return text(); }
|
|
122
|
+
|
|
123
|
+
inlineSpace
|
|
124
|
+
= [ \t]
|
|
125
|
+
|
|
126
|
+
integer
|
|
127
|
+
= sign? digits {
|
|
128
|
+
return parseInt(text());
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// A lambda expression: `=foo()`
|
|
132
|
+
lambda
|
|
133
|
+
= "=" __ expr:expr { return [ops.lambda, expr]; }
|
|
134
|
+
|
|
135
|
+
// A path that begins with a slash: `/foo/bar`
|
|
136
|
+
leadingSlashPath
|
|
137
|
+
= "/" @path
|
|
138
|
+
/ "/" { return [""]; }
|
|
139
|
+
|
|
140
|
+
// A separated list of expressions
|
|
141
|
+
list = head:expr tail:(separator @expr)* separator? { return [head].concat(tail); }
|
|
142
|
+
|
|
143
|
+
newLine
|
|
144
|
+
= "\n"
|
|
145
|
+
/ "\r\n"
|
|
146
|
+
/ "\r"
|
|
147
|
+
|
|
148
|
+
// A number
|
|
149
|
+
number
|
|
150
|
+
= float
|
|
151
|
+
/ integer
|
|
152
|
+
|
|
153
|
+
// An object literal: `{foo: 1, bar: 2}`
|
|
154
|
+
//
|
|
155
|
+
// TODO: Use Object.fromEntries with array of key/value pairs
|
|
156
|
+
//
|
|
157
|
+
object
|
|
158
|
+
= "{" __ properties:objectProperties? "}" { return [ops.object, ...(properties ?? [])]; }
|
|
159
|
+
|
|
160
|
+
// A separated list of object properties or shorthands
|
|
161
|
+
objectProperties
|
|
162
|
+
= head:objectPropertyOrShorthand tail:(separator @objectPropertyOrShorthand)* separator? __ {
|
|
163
|
+
return [head].concat(tail);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// A single object property with key and value: `x: 1`
|
|
167
|
+
objectProperty
|
|
168
|
+
= @identifier __ ":" __ @expr
|
|
169
|
+
|
|
170
|
+
objectPropertyOrShorthand
|
|
171
|
+
= objectProperty
|
|
172
|
+
/ key:identifier { return [key, [ops.inherited, key]]; }
|
|
173
|
+
|
|
174
|
+
// Function arguments in parentheses
|
|
175
|
+
parensArgs
|
|
176
|
+
= "(" __ list:list? ")" { return list ?? [undefined]; }
|
|
177
|
+
|
|
178
|
+
separator
|
|
179
|
+
= __ "," __
|
|
180
|
+
/ whitespaceWithNewLine
|
|
181
|
+
|
|
182
|
+
sign
|
|
183
|
+
= [+\-]
|
|
184
|
+
|
|
185
|
+
// A slash-separated path of keys
|
|
186
|
+
path
|
|
187
|
+
= head:pathKey "/" tail:path { return [head].concat(tail); }
|
|
188
|
+
/ key:pathKey { return [key]; }
|
|
189
|
+
|
|
190
|
+
// A single key in a slash-separated path
|
|
191
|
+
pathKey
|
|
192
|
+
= key:identifierChar* { return key.join(""); }
|
|
193
|
+
|
|
194
|
+
// Parse a protocol call like `fn://foo/bar`.
|
|
195
|
+
// There can be zero, one, or two slashes after the colon.
|
|
196
|
+
protocolCall
|
|
197
|
+
= protocol:protocol ":" "/"|0..2| host:host path:leadingSlashPath? {
|
|
198
|
+
return [protocol, host, ...(path ?? [])];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
protocol
|
|
202
|
+
= reservedProtocol
|
|
203
|
+
/ scopeReference
|
|
204
|
+
|
|
205
|
+
reservedProtocol
|
|
206
|
+
= "https" { return ops.https; }
|
|
207
|
+
/ "http" { return ops.http; }
|
|
208
|
+
/ "treehttps" { return ops.treeHttps; }
|
|
209
|
+
/ "treehttp" { return ops.treeHttp; }
|
|
210
|
+
/ "tree" { return ops.treeHttps; } // Shorthand alias
|
|
211
|
+
|
|
212
|
+
scopeReference
|
|
213
|
+
= key:identifier { return [ops.scope, key]; }
|
|
214
|
+
|
|
215
|
+
singleQuoteString
|
|
216
|
+
= "'" chars:singleQuoteStringChar* "'" { return chars.join(""); }
|
|
217
|
+
|
|
218
|
+
singleQuoteStringChar
|
|
219
|
+
= !("'" / newLine) @textChar
|
|
220
|
+
|
|
221
|
+
start
|
|
222
|
+
= number
|
|
223
|
+
|
|
224
|
+
string
|
|
225
|
+
= doubleQuoteString
|
|
226
|
+
/ singleQuoteString
|
|
227
|
+
|
|
228
|
+
// A top-level document defining a template. This is the same as a template
|
|
229
|
+
// literal, but can contain backticks at the top level.
|
|
230
|
+
templateDocument "Origami template"
|
|
231
|
+
= contents:templateDocumentContents { return [ops.lambda, contents]; }
|
|
232
|
+
|
|
233
|
+
// Template documents can contain backticks at the top level.
|
|
234
|
+
templateDocumentChar
|
|
235
|
+
= !"{{" @textChar
|
|
236
|
+
|
|
237
|
+
// The contents of a template document containing plain text and substitutions
|
|
238
|
+
templateDocumentContents
|
|
239
|
+
= parts:(templateDocumentText / templateSubstitution)* { return makeTemplate(parts); }
|
|
240
|
+
|
|
241
|
+
templateDocumentText
|
|
242
|
+
= chars:templateDocumentChar+ { return chars.join(""); }
|
|
243
|
+
|
|
244
|
+
// A backtick-quoted template literal
|
|
245
|
+
templateLiteral
|
|
246
|
+
= "`" @templateLiteralContents "`"
|
|
247
|
+
|
|
248
|
+
templateLiteralChar
|
|
249
|
+
= !("`" / "{{") @textChar
|
|
250
|
+
|
|
251
|
+
// The contents of a template literal containing plain text and substitutions
|
|
252
|
+
templateLiteralContents
|
|
253
|
+
= parts:(templateLiteralText / templateSubstitution)* { return makeTemplate(parts); }
|
|
254
|
+
|
|
255
|
+
// Plain text in a template literal
|
|
256
|
+
templateLiteralText
|
|
257
|
+
= chars:templateLiteralChar+ { return chars.join(""); }
|
|
258
|
+
|
|
259
|
+
// A substitution in a template literal: `{{ fn() }}`
|
|
260
|
+
templateSubstitution
|
|
261
|
+
= "{{" @expression "}}"
|
|
262
|
+
|
|
263
|
+
textChar
|
|
264
|
+
= escapedChar / .
|
|
265
|
+
|
|
266
|
+
// A tree literal: `{ index.html = "Hello" }`
|
|
267
|
+
tree
|
|
268
|
+
= "{" __ assignments:treeAssignments? "}" { return [ops.tree, ...(assignments ?? [])]; }
|
|
269
|
+
|
|
270
|
+
// A separated list of assignments or shorthands
|
|
271
|
+
treeAssignments
|
|
272
|
+
= head:assignmentOrShorthand tail:(separator @assignmentOrShorthand)* separator? __ {
|
|
273
|
+
return [head].concat(tail);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
whitespaceWithNewLine
|
|
277
|
+
= inlineSpace* comment? newLine __
|