@weborigami/language 0.0.41 → 0.0.43

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.43",
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": "0.0.43",
14
+ "@weborigami/types": "0.0.43",
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
@@ -24,14 +30,14 @@ export function templateDocument(text) {
24
30
  //
25
31
  // Example:
26
32
  //
27
- // {{ if `
33
+ // ${ if `
28
34
  // true text
29
35
  // `, `
30
36
  // false text
31
- // ` }}
37
+ // ` }
32
38
  //
33
39
  // 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
40
+ // whitespace before the `${`), and has the line end with the start of a
35
41
  // template literal (there's only whitespace after the backtick) marks the start
36
42
  // of a block.
37
43
  //
@@ -39,17 +45,17 @@ export function templateDocument(text) {
39
45
  // another is an internal break in the block. Edge case: three backticks in a
40
46
  // row, like ```, are common in markdown and are not treated as a break.
41
47
  //
42
- // Case 3: a line that ends a template literal and ends with `}}` or ends the
48
+ // Case 3: a line that ends a template literal and ends with `}` or ends the
43
49
  // text marks the end of the block.
44
50
  //
45
51
  // In all three cases, we trim spaces and tabs from the start and end of the
46
52
  // line. In case 1, we also remove the preceding newline.
47
53
  function trimTemplateWhitespace(text) {
48
- const regex1 = /(^|\n)[ \t]*({{.*?`)[ \t]*\n/g;
54
+ const regex1 = /(^|\n)[ \t]*((?:{{|\${).*?`)[ \t]*\n/g;
49
55
  const regex2 = /\n[ \t]*(`(?!`).*?`)[ \t]*\n/g;
50
- const regex3 = /\n[ \t]*(`(?!`).*?}})[ \t]*(?:\n|$)/g;
56
+ const regex3js = /\n[ \t]*(`(?!`).*?(?:}}|[^\\]}))[ \t]*(?:\n|$)/g;
51
57
  const trimBlockStarts = text.replace(regex1, "$1$2");
52
58
  const trimBlockBreaks = trimBlockStarts.replace(regex2, "\n$1");
53
- const trimBlockEnds = trimBlockBreaks.replace(regex3, "\n$1");
59
+ const trimBlockEnds = trimBlockBreaks.replace(regex3js, "\n$1");
54
60
  return trimBlockEnds;
55
61
  }
@@ -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
@@ -15,12 +25,23 @@ __
15
25
  = (inlineSpace / newLine / comment)* { return ""; }
16
26
 
17
27
  // A filesystem path that begins with a slash: `/foo/bar`
28
+ // We take care to avoid treating two consecutive leading slashes as a path;
29
+ // that starts a comment.
18
30
  absoluteFilePath "absolute file path"
19
- = path:leadingSlashPath { return [[ops.filesRoot], ...path]; }
31
+ = !"//" path:leadingSlashPath {
32
+ return annotate([[ops.filesRoot], ...path], location());
33
+ }
20
34
 
21
35
  args "function arguments"
22
36
  = parensArgs
23
- / path:leadingSlashPath { return [ops.traverse, ...path]; }
37
+ / path:leadingSlashPath {
38
+ return annotate([ops.traverse, ...path], location());
39
+ }
40
+
41
+ array "array"
42
+ = "[" __ list:list? __ closingBracket {
43
+ return annotate([ops.array, ...(list ?? [])], location());
44
+ }
24
45
 
25
46
  // An assignment statement: `foo = 1`
26
47
  assignment "tree assignment"
@@ -28,10 +49,9 @@ assignment "tree assignment"
28
49
 
29
50
  assignmentOrShorthand
30
51
  = assignment
31
- / key:identifier { return [key, [ops.inherited, key]]; }
32
-
33
- array "array"
34
- = "[" __ list:list? "]" { return [ops.array, ...(list ?? [])]; }
52
+ / key:identifier {
53
+ return annotate([key, [ops.inherited, key]], location());
54
+ }
35
55
 
36
56
  // Something that can be called. This is more restrictive than the `expr`
37
57
  // parser; it doesn't accept regular function calls.
@@ -46,13 +66,41 @@ callTarget "function call"
46
66
  / group
47
67
  / scopeReference
48
68
 
69
+ // Required closing curly brace. We use this for the `tree` term: it's the last
70
+ // term in the `step` parser that starts with a curly brace, so if that parser
71
+ // sees a left curly brace, here we must see a right curly brace.
72
+ closingBrace
73
+ = "}"
74
+ / .? {
75
+ error("Expected right curly brace");
76
+ }
77
+
78
+ // Required closing bracket
79
+ closingBracket
80
+ = "]"
81
+ / .? {
82
+ error("Expected right bracket");
83
+ }
84
+
85
+ // Required closing parenthesis. We use this for the `group` term: it's the last
86
+ // term in the `step` parser that starts with a parenthesis, so if that parser
87
+ // sees a left parenthesis, here we must see a right parenthesis.
88
+ closingParen
89
+ = ")"
90
+ / .? {
91
+ error("Expected right parenthesis");
92
+ }
93
+
49
94
  // A single line comment
50
95
  comment "comment"
51
- = "#" [^\n\r]*
96
+ = multiLineComment
97
+ / singleLineComment
52
98
 
53
99
  digits
54
100
  = @[0-9]+
55
101
 
102
+ doubleArrow = "⇒" / "=>"
103
+
56
104
  doubleQuoteString "double quote string"
57
105
  = '"' chars:doubleQuoteStringChar* '"' { return chars.join(""); }
58
106
 
@@ -63,27 +111,8 @@ escapedChar "backslash-escaped character"
63
111
  = "\\" @.
64
112
 
65
113
  // 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
114
+ expr
115
+ = pipeline
87
116
 
88
117
  // Top-level Origami expression, possible leading/trailing whitepsace.
89
118
  expression "Origami expression"
@@ -97,36 +126,36 @@ float "floating-point number"
97
126
  // Parse a function and its arguments, e.g. `fn(arg)`, possibly part of a chain
98
127
  // of function calls, like `fn(arg1)(arg2)(arg3)`.
99
128
  functionComposition "function composition"
100
- // = target:callTarget chain:argsChain { return makeFunctionCall(target, chain); }
101
- = target:callTarget chain:args+ { return makeFunctionCall(target, chain); }
129
+ = target:callTarget chain:args* end:implicitParensArgs? {
130
+ if (end) {
131
+ chain.push(end);
132
+ }
133
+ return annotate(makeFunctionCall(target, chain), location());
134
+ }
102
135
 
103
136
  // An expression in parentheses: `(foo)`
104
137
  group "parenthetical group"
105
- = "(" __ @expr __ ")"
138
+ = "(" __ @expr __ closingParen
139
+
140
+ // A host identifier that may include a colon and port number: `example.com:80`.
141
+ // This is used as a special case at the head of a path, where we want to
142
+ // interpret a colon as part of a text identifier.
143
+ host "HTTP/HTTPS host"
144
+ = identifier (":" number)? { return text(); }
106
145
 
107
146
  identifier "identifier"
108
147
  = chars:identifierChar+ { return chars.join(""); }
109
148
 
110
149
  identifierChar
111
- = [^(){}\[\]<>,/:=\`"'\\# \t\n\r] // No unescaped whitespace or special chars
150
+ = [^(){}\[\]<>\-=,/:\`"'\\# →⇒\t\n\r] // No unescaped whitespace or special chars
151
+ / @'-' !'>' // Accept a hyphen but not in a single arrow combination
112
152
  / escapedChar
113
153
 
114
154
  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
- }
155
+ = @identifier|1.., separator| separator?
124
156
 
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(); }
157
+ implicitParensArgs "arguments with implicit parentheses"
158
+ = inlineSpace+ @list
130
159
 
131
160
  inlineSpace
132
161
  = [ \t]
@@ -138,16 +167,21 @@ integer "integer"
138
167
 
139
168
  // A lambda expression: `=foo()`
140
169
  lambda "lambda function"
141
- = "=" __ expr:expr { return [ops.lambda, null, expr]; }
170
+ = "=" __ expr:expr {
171
+ return annotate([ops.lambda, null, expr], location());
172
+ }
142
173
 
143
174
  // A path that begins with a slash: `/foo/bar`
144
175
  leadingSlashPath "path with a leading slash"
145
176
  = "/" @path
146
- / "/" { return [""]; }
177
+ / "/" { return annotate([""], location()); }
147
178
 
148
179
  // A separated list of expressions
149
180
  list "list"
150
- = head:expr tail:(separator @expr)* separator? { return [head].concat(tail); }
181
+ = @expr|1.., separator| separator?
182
+
183
+ multiLineComment
184
+ = "/*" (!"*/" .)* "*/" { return null; }
151
185
 
152
186
  newLine
153
187
  = "\n"
@@ -164,13 +198,13 @@ number "number"
164
198
  // TODO: Use Object.fromEntries with array of key/value pairs
165
199
  //
166
200
  object "object literal"
167
- = "{" __ properties:objectProperties? "}" { return [ops.object, ...(properties ?? [])]; }
201
+ = "{" __ properties:objectProperties? __ "}" {
202
+ return annotate([ops.object, ...(properties ?? [])], location());
203
+ }
168
204
 
169
205
  // A separated list of object properties or shorthands
170
206
  objectProperties
171
- = head:objectPropertyOrShorthand tail:(separator @objectPropertyOrShorthand)* separator? __ {
172
- return [head].concat(tail);
173
- }
207
+ = @objectPropertyOrShorthand|1.., separator| separator?
174
208
 
175
209
  // A single object property with key and value: `x: 1`
176
210
  objectProperty "object property"
@@ -178,28 +212,29 @@ objectProperty "object property"
178
212
 
179
213
  objectPropertyOrShorthand
180
214
  = objectProperty
181
- / key:identifier { return [key, [ops.scope, key]]; }
215
+ / key:identifier {
216
+ return annotate([key, [ops.scope, key]], location());
217
+ }
182
218
 
183
219
  parameterizedLambda
184
- = "(" __ parameters:identifierList? ")" __ ("=>"/"⇒") __ expr:expr {
185
- return [ops.lambda, parameters ?? [], expr];
220
+ = "(" __ parameters:identifierList? __ ")" __ doubleArrow __ expr:expr {
221
+ return annotate([ops.lambda, parameters ?? [], expr], location());
186
222
  }
187
223
 
188
224
  // Function arguments in parentheses
189
225
  parensArgs "function arguments in parentheses"
190
- = "(" __ list:list? ")" { return list ?? [undefined]; }
191
-
192
- separator
193
- = __ "," __
194
- / whitespaceWithNewLine
226
+ = "(" __ list:list? __ ")" {
227
+ return list ?? annotate([undefined], location());
228
+ }
195
229
 
196
- sign
197
- = [+\-]
230
+ pipeline
231
+ = steps:(@step|1.., __ singleArrow __ |) {
232
+ return annotate(makePipeline(steps), location());
233
+ }
198
234
 
199
235
  // A slash-separated path of keys
200
236
  path "slash-separated path"
201
- = head:pathKey "/" tail:path { return [head].concat(tail); }
202
- / key:pathKey { return [key]; }
237
+ = pathKey|1.., "/"|
203
238
 
204
239
  // A single key in a slash-separated path
205
240
  pathKey "path element"
@@ -209,7 +244,7 @@ pathKey "path element"
209
244
  // There can be zero, one, or two slashes after the colon.
210
245
  protocolCall "function call using protocol: syntax"
211
246
  = protocol:protocol ":" "/"|0..2| host:host path:leadingSlashPath? {
212
- return [protocol, host, ...(path ?? [])];
247
+ return annotate([protocol, host, ...(path ?? [])], location());
213
248
  }
214
249
 
215
250
  protocol "protocol"
@@ -225,7 +260,22 @@ reservedProtocol "reserved protocol"
225
260
  / "tree" { return ops.treeHttps; } // Alias
226
261
 
227
262
  scopeReference "scope reference"
228
- = key:identifier { return [ops.scope, key]; }
263
+ = key:identifier {
264
+ return annotate([ops.scope, key], location());
265
+ }
266
+
267
+ separator
268
+ = __ "," __
269
+ / whitespaceWithNewLine
270
+
271
+ sign
272
+ = [+\-]
273
+
274
+ singleArrow = "→" / "->"
275
+
276
+ singleLineComment
277
+ = "#" [^\n\r]* { return null; }
278
+ / "//" [^\n\r]* { return null; }
229
279
 
230
280
  singleQuoteString "single quote string"
231
281
  = "'" chars:singleQuoteStringChar* "'" { return chars.join(""); }
@@ -233,6 +283,29 @@ singleQuoteString "single quote string"
233
283
  singleQuoteStringChar
234
284
  = !("'" / newLine) @textChar
235
285
 
286
+ // A single step in a pipeline, or a top-level expression
287
+ step
288
+ // Literals that can't start a function call
289
+ = number
290
+ // Try functions next; they can start with expression types that follow
291
+ // (array, object, etc.), and we want to parse the larger thing first.
292
+ / functionComposition
293
+ // Then try parsers that look for a distinctive token at the start: an opening
294
+ // slash, bracket, curly brace, etc.
295
+ / absoluteFilePath
296
+ / array
297
+ / object
298
+ / tree
299
+ / lambda
300
+ / parameterizedLambda
301
+ / templateLiteral
302
+ / string
303
+ / group
304
+ // Protocol calls are distinguished by a colon, but it's not at the start.
305
+ / protocolCall
306
+ // Least distinctive option is a simple scope reference, so it comes last.
307
+ / scopeReference
308
+
236
309
  start
237
310
  = number
238
311
 
@@ -243,15 +316,19 @@ string "string"
243
316
  // A top-level document defining a template. This is the same as a template
244
317
  // literal, but can contain backticks at the top level.
245
318
  templateDocument "template"
246
- = contents:templateDocumentContents { return [ops.lambda, null, contents]; }
319
+ = contents:templateDocumentContents {
320
+ return annotate([ops.lambda, null, contents], location());
321
+ }
247
322
 
248
323
  // Template documents can contain backticks at the top level.
249
324
  templateDocumentChar
250
- = !"{{" @textChar
325
+ = !("{{" / "${") @textChar
251
326
 
252
327
  // The contents of a template document containing plain text and substitutions
253
328
  templateDocumentContents
254
- = parts:(templateDocumentText / templateSubstitution)* { return makeTemplate(parts); }
329
+ = parts:(templateDocumentText / templateSubstitution)* {
330
+ return annotate(makeTemplate(parts), location());
331
+ }
255
332
 
256
333
  templateDocumentText "template text"
257
334
  = chars:templateDocumentChar+ { return chars.join(""); }
@@ -261,11 +338,13 @@ templateLiteral "template literal"
261
338
  = "`" @templateLiteralContents "`"
262
339
 
263
340
  templateLiteralChar
264
- = !("`" / "{{") @textChar
341
+ = !("`" / "{{" / "${") @textChar
265
342
 
266
343
  // The contents of a template literal containing plain text and substitutions
267
344
  templateLiteralContents
268
- = parts:(templateLiteralText / templateSubstitution)* { return makeTemplate(parts); }
345
+ = parts:(templateLiteralText / templateSubstitution)* {
346
+ return annotate(makeTemplate(parts), location());
347
+ }
269
348
 
270
349
  // Plain text in a template literal
271
350
  templateLiteralText
@@ -274,19 +353,20 @@ templateLiteralText
274
353
  // A substitution in a template literal: `{{ fn() }}`
275
354
  templateSubstitution "template substitution"
276
355
  = "{{" @expression "}}"
356
+ / "${" @expression "}"
277
357
 
278
358
  textChar
279
359
  = escapedChar / .
280
360
 
281
361
  // A tree literal: `{ index.html = "Hello" }`
282
362
  tree "tree literal"
283
- = "{" __ assignments:treeAssignments? "}" { return [ops.tree, ...(assignments ?? [])]; }
363
+ = "{" __ assignments:treeAssignments? __ closingBrace {
364
+ return annotate([ops.tree, ...(assignments ?? [])], location());
365
+ }
284
366
 
285
367
  // A separated list of assignments or shorthands
286
368
  treeAssignments
287
- = head:assignmentOrShorthand tail:(separator @assignmentOrShorthand)* separator? __ {
288
- return [head].concat(tail);
289
- }
369
+ = @assignmentOrShorthand|1.., separator| separator?
290
370
 
291
371
  whitespaceWithNewLine
292
372
  = inlineSpace* comment? newLine __
@@ -0,0 +1,3 @@
1
+ import { Code } from "../../index.ts";
2
+
3
+ export function parse(input: string, options: any): Code;