@weborigami/language 0.0.40 → 0.0.42

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 CHANGED
@@ -3,6 +3,15 @@ import { StringLike } from "../async-tree/index.js";
3
3
 
4
4
  export * from "./main.js";
5
5
 
6
+ /**
7
+ * A chunk of compiled Origami code. This is just an Array with an additional
8
+ * `source` property.
9
+ */
10
+ interface ArrayWithSource extends Array<any> {
11
+ source?: Source;
12
+ }
13
+ export type Code = ArrayWithSource;
14
+
6
15
  /**
7
16
  * A class constructor is an object with a `new` method that returns an
8
17
  * instance of the indicated type.
@@ -32,3 +41,12 @@ export type FileUnpackFunction = (
32
41
  export type Mixin<MixinMembers> = <T>(
33
42
  Base: Constructor<T>
34
43
  ) => Constructor<T & MixinMembers>;
44
+
45
+ /**
46
+ * Source code representation used by the parser.
47
+ */
48
+ export type Source = {
49
+ name: string;
50
+ text: string;
51
+ url: URL;
52
+ }
package/main.js CHANGED
@@ -17,5 +17,5 @@ export { default as concatTreeValues } from "./src/runtime/concatTreeValues.js";
17
17
  export { default as evaluate } from "./src/runtime/evaluate.js";
18
18
  export * as expressionFunction from "./src/runtime/expressionFunction.js";
19
19
  export { default as extname } from "./src/runtime/extname.js";
20
- export { default as format } from "./src/runtime/format.js";
20
+ export { default as formatError } from "./src/runtime/formatError.js";
21
21
  export { default as functionResultsMap } from "./src/runtime/functionResultsMap.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weborigami/language",
3
- "version": "0.0.40",
3
+ "version": "0.0.42",
4
4
  "description": "Web Origami expression language compiler and runtime",
5
5
  "type": "module",
6
6
  "main": "./main.js",
@@ -10,8 +10,8 @@
10
10
  "typescript": "5.3.3"
11
11
  },
12
12
  "dependencies": {
13
- "@weborigami/async-tree": "0.0.40",
14
- "@weborigami/types": "0.0.40",
13
+ "@weborigami/async-tree": "https://gitpkg.now.sh/WebOrigami/origami/async-tree?e08c2451",
14
+ "@weborigami/types": "0.0.42",
15
15
  "peggy": "3.0.2",
16
16
  "watcher": "2.3.0"
17
17
  },
@@ -1,21 +1,27 @@
1
1
  import { createExpressionFunction } from "../runtime/expressionFunction.js";
2
2
  import { parse } from "./parse.js";
3
3
 
4
- function compile(text, startRule) {
4
+ function compile(source, startRule) {
5
+ if (typeof source === "string") {
6
+ source = { text: source };
7
+ }
5
8
  // Trim whitespace from template blocks before we begin lexing, as our
6
9
  // heuristics are non-local and hard to implement in our parser.
7
- const preprocessed = trimTemplateWhitespace(text);
8
- const code = parse(preprocessed, { startRule });
10
+ const preprocessed = trimTemplateWhitespace(source.text);
11
+ const code = parse(preprocessed, {
12
+ grammarSource: source,
13
+ startRule,
14
+ });
9
15
  const fn = createExpressionFunction(code);
10
16
  return fn;
11
17
  }
12
18
 
13
- export function expression(text) {
14
- return compile(text, "expression");
19
+ export function expression(source) {
20
+ return compile(source, "expression");
15
21
  }
16
22
 
17
- export function templateDocument(text) {
18
- return compile(text, "templateDocument");
23
+ export function templateDocument(source) {
24
+ return compile(source, "templateDocument");
19
25
  }
20
26
 
21
27
  // Trim the whitespace around and in substitution blocks in a template. There's
@@ -7,7 +7,17 @@
7
7
  //
8
8
 
9
9
  import * as ops from "../runtime/ops.js";
10
- import { makeFunctionCall, makeTemplate } from "./parserHelpers.js";
10
+ import { makeFunctionCall, makePipeline, makeTemplate } from "./parserHelpers.js";
11
+
12
+ // If a parse result is an object that will be evaluated at runtime, attach the
13
+ // location of the source code that produced it for debugging and error messages.
14
+ function annotate(parseResult, location) {
15
+ if (typeof parseResult === "object" && parseResult !== null) {
16
+ parseResult.location = location;
17
+ }
18
+ return parseResult;
19
+ }
20
+
11
21
  }}
12
22
 
13
23
  // A block of optional whitespace
@@ -16,11 +26,20 @@ __
16
26
 
17
27
  // A filesystem path that begins with a slash: `/foo/bar`
18
28
  absoluteFilePath "absolute file path"
19
- = path:leadingSlashPath { return [[ops.filesRoot], ...path]; }
29
+ = path:leadingSlashPath {
30
+ return annotate([[ops.filesRoot], ...path], location());
31
+ }
20
32
 
21
- // A chain of arguments: `(arg1)(arg2)(arg3)`
22
- argsChain "function arguments"
23
- = parts:(parensArgs / leadingSlashPath)+ { return parts; }
33
+ args "function arguments"
34
+ = parensArgs
35
+ / path:leadingSlashPath {
36
+ return annotate([ops.traverse, ...path], location());
37
+ }
38
+
39
+ array "array"
40
+ = "[" __ list:list? __ closingBracket {
41
+ return annotate([ops.array, ...(list ?? [])], location());
42
+ }
24
43
 
25
44
  // An assignment statement: `foo = 1`
26
45
  assignment "tree assignment"
@@ -28,10 +47,9 @@ assignment "tree assignment"
28
47
 
29
48
  assignmentOrShorthand
30
49
  = assignment
31
- / key:identifier { return [key, [ops.inherited, key]]; }
32
-
33
- array "array"
34
- = "[" __ list:list? "]" { return [ops.array, ...(list ?? [])]; }
50
+ / key:identifier {
51
+ return annotate([key, [ops.inherited, key]], location());
52
+ }
35
53
 
36
54
  // Something that can be called. This is more restrictive than the `expr`
37
55
  // parser; it doesn't accept regular function calls.
@@ -41,10 +59,36 @@ callTarget "function call"
41
59
  / object
42
60
  / tree
43
61
  / lambda
62
+ / parameterizedLambda
44
63
  / protocolCall
45
64
  / group
46
65
  / scopeReference
47
66
 
67
+ // Required closing curly brace. We use this for the `tree` term: it's the last
68
+ // term in the `step` parser that starts with a curly brace, so if that parser
69
+ // sees a left curly brace, here we must see a right curly brace.
70
+ closingBrace
71
+ = "}"
72
+ / .? {
73
+ error("Expected right curly brace");
74
+ }
75
+
76
+ // Required closing bracket
77
+ closingBracket
78
+ = "]"
79
+ / .? {
80
+ error("Expected right bracket");
81
+ }
82
+
83
+ // Required closing parenthesis. We use this for the `group` term: it's the last
84
+ // term in the `step` parser that starts with a parenthesis, so if that parser
85
+ // sees a left parenthesis, here we must see a right parenthesis.
86
+ closingParen
87
+ = ")"
88
+ / .? {
89
+ error("Expected right parenthesis");
90
+ }
91
+
48
92
  // A single line comment
49
93
  comment "comment"
50
94
  = "#" [^\n\r]*
@@ -52,6 +96,8 @@ comment "comment"
52
96
  digits
53
97
  = @[0-9]+
54
98
 
99
+ doubleArrow = "⇒" / "=>"
100
+
55
101
  doubleQuoteString "double quote string"
56
102
  = '"' chars:doubleQuoteStringChar* '"' { return chars.join(""); }
57
103
 
@@ -62,26 +108,8 @@ escapedChar "backslash-escaped character"
62
108
  = "\\" @.
63
109
 
64
110
  // An Origami expression, no leading/trailing whitespace
65
- expr "expression"
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
111
+ expr
112
+ = pipeline
85
113
 
86
114
  // Top-level Origami expression, possible leading/trailing whitepsace.
87
115
  expression "Origami expression"
@@ -95,30 +123,36 @@ float "floating-point number"
95
123
  // Parse a function and its arguments, e.g. `fn(arg)`, possibly part of a chain
96
124
  // of function calls, like `fn(arg1)(arg2)(arg3)`.
97
125
  functionComposition "function composition"
98
- = target:callTarget chain:argsChain { return makeFunctionCall(target, chain); }
126
+ = target:callTarget chain:args* end:implicitParensArgs? {
127
+ if (end) {
128
+ chain.push(end);
129
+ }
130
+ return annotate(makeFunctionCall(target, chain), location());
131
+ }
99
132
 
100
133
  // An expression in parentheses: `(foo)`
101
134
  group "parenthetical group"
102
- = "(" __ @expr __ ")"
135
+ = "(" __ @expr __ closingParen
136
+
137
+ // A host identifier that may include a colon and port number: `example.com:80`.
138
+ // This is used as a special case at the head of a path, where we want to
139
+ // interpret a colon as part of a text identifier.
140
+ host "HTTP/HTTPS host"
141
+ = identifier (":" number)? { return text(); }
103
142
 
104
143
  identifier "identifier"
105
144
  = chars:identifierChar+ { return chars.join(""); }
106
145
 
107
146
  identifierChar
108
- = [^(){}\[\],/:=\`"'\\# \t\n\r] // No unescaped whitespace or special chars
147
+ = [^(){}\[\]<>\-=,/:\`"'\\# →⇒\t\n\r] // No unescaped whitespace or special chars
148
+ / @'-' !'>' // Accept a hyphen but not in a single arrow combination
109
149
  / escapedChar
110
150
 
111
- // A function call with implicit parentheses: `fn 1, 2, 3`
112
- implicitParensCall "function call with implicit parentheses"
113
- = target:(functionComposition / callTarget) inlineSpace+ args:list {
114
- return [target, ...args];
115
- }
151
+ identifierList
152
+ = @identifier|1.., separator| separator?
116
153
 
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 "HTTP/HTTPS host"
121
- = identifier (":" number)? { return text(); }
154
+ implicitParensArgs "arguments with implicit parentheses"
155
+ = inlineSpace+ @list
122
156
 
123
157
  inlineSpace
124
158
  = [ \t]
@@ -130,16 +164,18 @@ integer "integer"
130
164
 
131
165
  // A lambda expression: `=foo()`
132
166
  lambda "lambda function"
133
- = "=" __ expr:expr { return [ops.lambda, expr]; }
167
+ = "=" __ expr:expr {
168
+ return annotate([ops.lambda, null, expr], location());
169
+ }
134
170
 
135
171
  // A path that begins with a slash: `/foo/bar`
136
172
  leadingSlashPath "path with a leading slash"
137
173
  = "/" @path
138
- / "/" { return [""]; }
174
+ / "/" { return annotate([""], location()); }
139
175
 
140
176
  // A separated list of expressions
141
177
  list "list"
142
- = head:expr tail:(separator @expr)* separator? { return [head].concat(tail); }
178
+ = @expr|1.., separator| separator?
143
179
 
144
180
  newLine
145
181
  = "\n"
@@ -156,13 +192,13 @@ number "number"
156
192
  // TODO: Use Object.fromEntries with array of key/value pairs
157
193
  //
158
194
  object "object literal"
159
- = "{" __ properties:objectProperties? "}" { return [ops.object, ...(properties ?? [])]; }
195
+ = "{" __ properties:objectProperties? __ "}" {
196
+ return annotate([ops.object, ...(properties ?? [])], location());
197
+ }
160
198
 
161
199
  // A separated list of object properties or shorthands
162
200
  objectProperties
163
- = head:objectPropertyOrShorthand tail:(separator @objectPropertyOrShorthand)* separator? __ {
164
- return [head].concat(tail);
165
- }
201
+ = @objectPropertyOrShorthand|1.., separator| separator?
166
202
 
167
203
  // A single object property with key and value: `x: 1`
168
204
  objectProperty "object property"
@@ -170,23 +206,29 @@ objectProperty "object property"
170
206
 
171
207
  objectPropertyOrShorthand
172
208
  = objectProperty
173
- / key:identifier { return [key, [ops.scope, key]]; }
209
+ / key:identifier {
210
+ return annotate([key, [ops.scope, key]], location());
211
+ }
212
+
213
+ parameterizedLambda
214
+ = "(" __ parameters:identifierList? __ ")" __ doubleArrow __ expr:expr {
215
+ return annotate([ops.lambda, parameters ?? [], expr], location());
216
+ }
174
217
 
175
218
  // Function arguments in parentheses
176
219
  parensArgs "function arguments in parentheses"
177
- = "(" __ list:list? ")" { return list ?? [undefined]; }
178
-
179
- separator
180
- = __ "," __
181
- / whitespaceWithNewLine
220
+ = "(" __ list:list? __ ")" {
221
+ return list ?? annotate([undefined], location());
222
+ }
182
223
 
183
- sign
184
- = [+\-]
224
+ pipeline
225
+ = steps:(@step|1.., __ singleArrow __ |) {
226
+ return annotate(makePipeline(steps), location());
227
+ }
185
228
 
186
229
  // A slash-separated path of keys
187
230
  path "slash-separated path"
188
- = head:pathKey "/" tail:path { return [head].concat(tail); }
189
- / key:pathKey { return [key]; }
231
+ = pathKey|1.., "/"|
190
232
 
191
233
  // A single key in a slash-separated path
192
234
  pathKey "path element"
@@ -196,7 +238,7 @@ pathKey "path element"
196
238
  // There can be zero, one, or two slashes after the colon.
197
239
  protocolCall "function call using protocol: syntax"
198
240
  = protocol:protocol ":" "/"|0..2| host:host path:leadingSlashPath? {
199
- return [protocol, host, ...(path ?? [])];
241
+ return annotate([protocol, host, ...(path ?? [])], location());
200
242
  }
201
243
 
202
244
  protocol "protocol"
@@ -212,7 +254,18 @@ reservedProtocol "reserved protocol"
212
254
  / "tree" { return ops.treeHttps; } // Alias
213
255
 
214
256
  scopeReference "scope reference"
215
- = key:identifier { return [ops.scope, key]; }
257
+ = key:identifier {
258
+ return annotate([ops.scope, key], location());
259
+ }
260
+
261
+ separator
262
+ = __ "," __
263
+ / whitespaceWithNewLine
264
+
265
+ sign
266
+ = [+\-]
267
+
268
+ singleArrow = "→" / "->"
216
269
 
217
270
  singleQuoteString "single quote string"
218
271
  = "'" chars:singleQuoteStringChar* "'" { return chars.join(""); }
@@ -220,6 +273,29 @@ singleQuoteString "single quote string"
220
273
  singleQuoteStringChar
221
274
  = !("'" / newLine) @textChar
222
275
 
276
+ // A single step in a pipeline, or a top-level expression
277
+ step
278
+ // Literals that can't start a function call
279
+ = number
280
+ // Try functions next; they can start with expression types that follow
281
+ // (array, object, etc.), and we want to parse the larger thing first.
282
+ / functionComposition
283
+ // Then try parsers that look for a distinctive token at the start: an opening
284
+ // slash, bracket, curly brace, etc.
285
+ / absoluteFilePath
286
+ / array
287
+ / object
288
+ / tree
289
+ / lambda
290
+ / parameterizedLambda
291
+ / templateLiteral
292
+ / string
293
+ / group
294
+ // Protocol calls are distinguished by a colon, but it's not at the start.
295
+ / protocolCall
296
+ // Least distinctive option is a simple scope reference, so it comes last.
297
+ / scopeReference
298
+
223
299
  start
224
300
  = number
225
301
 
@@ -230,7 +306,9 @@ string "string"
230
306
  // A top-level document defining a template. This is the same as a template
231
307
  // literal, but can contain backticks at the top level.
232
308
  templateDocument "template"
233
- = contents:templateDocumentContents { return [ops.lambda, contents]; }
309
+ = contents:templateDocumentContents {
310
+ return annotate([ops.lambda, null, contents], location());
311
+ }
234
312
 
235
313
  // Template documents can contain backticks at the top level.
236
314
  templateDocumentChar
@@ -238,7 +316,9 @@ templateDocumentChar
238
316
 
239
317
  // The contents of a template document containing plain text and substitutions
240
318
  templateDocumentContents
241
- = parts:(templateDocumentText / templateSubstitution)* { return makeTemplate(parts); }
319
+ = parts:(templateDocumentText / templateSubstitution)* {
320
+ return annotate(makeTemplate(parts), location());
321
+ }
242
322
 
243
323
  templateDocumentText "template text"
244
324
  = chars:templateDocumentChar+ { return chars.join(""); }
@@ -252,7 +332,9 @@ templateLiteralChar
252
332
 
253
333
  // The contents of a template literal containing plain text and substitutions
254
334
  templateLiteralContents
255
- = parts:(templateLiteralText / templateSubstitution)* { return makeTemplate(parts); }
335
+ = parts:(templateLiteralText / templateSubstitution)* {
336
+ return annotate(makeTemplate(parts), location());
337
+ }
256
338
 
257
339
  // Plain text in a template literal
258
340
  templateLiteralText
@@ -267,13 +349,13 @@ textChar
267
349
 
268
350
  // A tree literal: `{ index.html = "Hello" }`
269
351
  tree "tree literal"
270
- = "{" __ assignments:treeAssignments? "}" { return [ops.tree, ...(assignments ?? [])]; }
352
+ = "{" __ assignments:treeAssignments? __ closingBrace {
353
+ return annotate([ops.tree, ...(assignments ?? [])], location());
354
+ }
271
355
 
272
356
  // A separated list of assignments or shorthands
273
357
  treeAssignments
274
- = head:assignmentOrShorthand tail:(separator @assignmentOrShorthand)* separator? __ {
275
- return [head].concat(tail);
276
- }
358
+ = @assignmentOrShorthand|1.., separator| separator?
277
359
 
278
360
  whitespaceWithNewLine
279
361
  = inlineSpace* comment? newLine __
@@ -0,0 +1,3 @@
1
+ import { Code } from "../../index.ts";
2
+
3
+ export function parse(input: string, options: any): Code;