@weborigami/language 0.0.41 → 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.41",
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.41",
14
- "@weborigami/types": "0.0.41",
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
33
  args "function arguments"
22
34
  = parensArgs
23
- / path:leadingSlashPath { return [ops.traverse, ...path]; }
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.
@@ -46,6 +64,31 @@ callTarget "function call"
46
64
  / group
47
65
  / scopeReference
48
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
+
49
92
  // A single line comment
50
93
  comment "comment"
51
94
  = "#" [^\n\r]*
@@ -53,6 +96,8 @@ comment "comment"
53
96
  digits
54
97
  = @[0-9]+
55
98
 
99
+ doubleArrow = "⇒" / "=>"
100
+
56
101
  doubleQuoteString "double quote string"
57
102
  = '"' chars:doubleQuoteStringChar* '"' { return chars.join(""); }
58
103
 
@@ -63,27 +108,8 @@ escapedChar "backslash-escaped character"
63
108
  = "\\" @.
64
109
 
65
110
  // An Origami expression, no leading/trailing whitespace
66
- expr "expression"
67
- // Try function calls first, as they can start with expression types that
68
- // follow (array, object, etc.); we want to parse the largest thing first.
69
- = implicitParensCall
70
- / functionComposition
71
- // Then try parsers that look for a distinctive token at the start: an opening
72
- // slash, bracket, curly brace, etc.
73
- / absoluteFilePath
74
- / array
75
- / object
76
- / tree
77
- / templateLiteral
78
- / lambda
79
- / parameterizedLambda
80
- / group
81
- / string
82
- / number
83
- // Protocol calls are distinguished by a colon, but it's not at the start.
84
- / protocolCall
85
- // Least distinctive option is a simple scope reference, so it comes last.
86
- / scopeReference
111
+ expr
112
+ = pipeline
87
113
 
88
114
  // Top-level Origami expression, possible leading/trailing whitepsace.
89
115
  expression "Origami expression"
@@ -97,36 +123,36 @@ float "floating-point number"
97
123
  // Parse a function and its arguments, e.g. `fn(arg)`, possibly part of a chain
98
124
  // of function calls, like `fn(arg1)(arg2)(arg3)`.
99
125
  functionComposition "function composition"
100
- // = target:callTarget chain:argsChain { return makeFunctionCall(target, chain); }
101
- = target:callTarget chain:args+ { 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
+ }
102
132
 
103
133
  // An expression in parentheses: `(foo)`
104
134
  group "parenthetical group"
105
- = "(" __ @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(); }
106
142
 
107
143
  identifier "identifier"
108
144
  = chars:identifierChar+ { return chars.join(""); }
109
145
 
110
146
  identifierChar
111
- = [^(){}\[\]<>,/:=\`"'\\# \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
112
149
  / escapedChar
113
150
 
114
151
  identifierList
115
- = head:identifier tail:(separator @identifier)* separator? {
116
- return [head].concat(tail);
117
- }
118
-
119
- // A function call with implicit parentheses: `fn 1, 2, 3`
120
- implicitParensCall "function call with implicit parentheses"
121
- = target:(functionComposition / callTarget) inlineSpace+ args:list {
122
- return [target, ...args];
123
- }
152
+ = @identifier|1.., separator| separator?
124
153
 
125
- // A host identifier that may include a colon and port number: `example.com:80`.
126
- // This is used as a special case at the head of a path, where we want to
127
- // interpret a colon as part of a text identifier.
128
- host "HTTP/HTTPS host"
129
- = identifier (":" number)? { return text(); }
154
+ implicitParensArgs "arguments with implicit parentheses"
155
+ = inlineSpace+ @list
130
156
 
131
157
  inlineSpace
132
158
  = [ \t]
@@ -138,16 +164,18 @@ integer "integer"
138
164
 
139
165
  // A lambda expression: `=foo()`
140
166
  lambda "lambda function"
141
- = "=" __ expr:expr { return [ops.lambda, null, expr]; }
167
+ = "=" __ expr:expr {
168
+ return annotate([ops.lambda, null, expr], location());
169
+ }
142
170
 
143
171
  // A path that begins with a slash: `/foo/bar`
144
172
  leadingSlashPath "path with a leading slash"
145
173
  = "/" @path
146
- / "/" { return [""]; }
174
+ / "/" { return annotate([""], location()); }
147
175
 
148
176
  // A separated list of expressions
149
177
  list "list"
150
- = head:expr tail:(separator @expr)* separator? { return [head].concat(tail); }
178
+ = @expr|1.., separator| separator?
151
179
 
152
180
  newLine
153
181
  = "\n"
@@ -164,13 +192,13 @@ number "number"
164
192
  // TODO: Use Object.fromEntries with array of key/value pairs
165
193
  //
166
194
  object "object literal"
167
- = "{" __ properties:objectProperties? "}" { return [ops.object, ...(properties ?? [])]; }
195
+ = "{" __ properties:objectProperties? __ "}" {
196
+ return annotate([ops.object, ...(properties ?? [])], location());
197
+ }
168
198
 
169
199
  // A separated list of object properties or shorthands
170
200
  objectProperties
171
- = head:objectPropertyOrShorthand tail:(separator @objectPropertyOrShorthand)* separator? __ {
172
- return [head].concat(tail);
173
- }
201
+ = @objectPropertyOrShorthand|1.., separator| separator?
174
202
 
175
203
  // A single object property with key and value: `x: 1`
176
204
  objectProperty "object property"
@@ -178,28 +206,29 @@ objectProperty "object property"
178
206
 
179
207
  objectPropertyOrShorthand
180
208
  = objectProperty
181
- / key:identifier { return [key, [ops.scope, key]]; }
209
+ / key:identifier {
210
+ return annotate([key, [ops.scope, key]], location());
211
+ }
182
212
 
183
213
  parameterizedLambda
184
- = "(" __ parameters:identifierList? ")" __ ("=>"/"⇒") __ expr:expr {
185
- return [ops.lambda, parameters ?? [], expr];
214
+ = "(" __ parameters:identifierList? __ ")" __ doubleArrow __ expr:expr {
215
+ return annotate([ops.lambda, parameters ?? [], expr], location());
186
216
  }
187
217
 
188
218
  // Function arguments in parentheses
189
219
  parensArgs "function arguments in parentheses"
190
- = "(" __ list:list? ")" { return list ?? [undefined]; }
191
-
192
- separator
193
- = __ "," __
194
- / whitespaceWithNewLine
220
+ = "(" __ list:list? __ ")" {
221
+ return list ?? annotate([undefined], location());
222
+ }
195
223
 
196
- sign
197
- = [+\-]
224
+ pipeline
225
+ = steps:(@step|1.., __ singleArrow __ |) {
226
+ return annotate(makePipeline(steps), location());
227
+ }
198
228
 
199
229
  // A slash-separated path of keys
200
230
  path "slash-separated path"
201
- = head:pathKey "/" tail:path { return [head].concat(tail); }
202
- / key:pathKey { return [key]; }
231
+ = pathKey|1.., "/"|
203
232
 
204
233
  // A single key in a slash-separated path
205
234
  pathKey "path element"
@@ -209,7 +238,7 @@ pathKey "path element"
209
238
  // There can be zero, one, or two slashes after the colon.
210
239
  protocolCall "function call using protocol: syntax"
211
240
  = protocol:protocol ":" "/"|0..2| host:host path:leadingSlashPath? {
212
- return [protocol, host, ...(path ?? [])];
241
+ return annotate([protocol, host, ...(path ?? [])], location());
213
242
  }
214
243
 
215
244
  protocol "protocol"
@@ -225,7 +254,18 @@ reservedProtocol "reserved protocol"
225
254
  / "tree" { return ops.treeHttps; } // Alias
226
255
 
227
256
  scopeReference "scope reference"
228
- = 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 = "→" / "->"
229
269
 
230
270
  singleQuoteString "single quote string"
231
271
  = "'" chars:singleQuoteStringChar* "'" { return chars.join(""); }
@@ -233,6 +273,29 @@ singleQuoteString "single quote string"
233
273
  singleQuoteStringChar
234
274
  = !("'" / newLine) @textChar
235
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
+
236
299
  start
237
300
  = number
238
301
 
@@ -243,7 +306,9 @@ string "string"
243
306
  // A top-level document defining a template. This is the same as a template
244
307
  // literal, but can contain backticks at the top level.
245
308
  templateDocument "template"
246
- = contents:templateDocumentContents { return [ops.lambda, null, contents]; }
309
+ = contents:templateDocumentContents {
310
+ return annotate([ops.lambda, null, contents], location());
311
+ }
247
312
 
248
313
  // Template documents can contain backticks at the top level.
249
314
  templateDocumentChar
@@ -251,7 +316,9 @@ templateDocumentChar
251
316
 
252
317
  // The contents of a template document containing plain text and substitutions
253
318
  templateDocumentContents
254
- = parts:(templateDocumentText / templateSubstitution)* { return makeTemplate(parts); }
319
+ = parts:(templateDocumentText / templateSubstitution)* {
320
+ return annotate(makeTemplate(parts), location());
321
+ }
255
322
 
256
323
  templateDocumentText "template text"
257
324
  = chars:templateDocumentChar+ { return chars.join(""); }
@@ -265,7 +332,9 @@ templateLiteralChar
265
332
 
266
333
  // The contents of a template literal containing plain text and substitutions
267
334
  templateLiteralContents
268
- = parts:(templateLiteralText / templateSubstitution)* { return makeTemplate(parts); }
335
+ = parts:(templateLiteralText / templateSubstitution)* {
336
+ return annotate(makeTemplate(parts), location());
337
+ }
269
338
 
270
339
  // Plain text in a template literal
271
340
  templateLiteralText
@@ -280,13 +349,13 @@ textChar
280
349
 
281
350
  // A tree literal: `{ index.html = "Hello" }`
282
351
  tree "tree literal"
283
- = "{" __ assignments:treeAssignments? "}" { return [ops.tree, ...(assignments ?? [])]; }
352
+ = "{" __ assignments:treeAssignments? __ closingBrace {
353
+ return annotate([ops.tree, ...(assignments ?? [])], location());
354
+ }
284
355
 
285
356
  // A separated list of assignments or shorthands
286
357
  treeAssignments
287
- = head:assignmentOrShorthand tail:(separator @assignmentOrShorthand)* separator? __ {
288
- return [head].concat(tail);
289
- }
358
+ = @assignmentOrShorthand|1.., separator| separator?
290
359
 
291
360
  whitespaceWithNewLine
292
361
  = inlineSpace* comment? newLine __
@@ -0,0 +1,3 @@
1
+ import { Code } from "../../index.ts";
2
+
3
+ export function parse(input: string, options: any): Code;