@weborigami/language 0.2.9 → 0.2.11

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/main.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./src/runtime/internal.js";
2
2
 
3
3
  export * as compile from "./src/compiler/compile.js";
4
+ export { default as isOrigamiFrontMatter } from "./src/compiler/isOrigamiFrontMatter.js";
4
5
  export * from "./src/runtime/errors.js";
5
6
  export { default as evaluate } from "./src/runtime/evaluate.js";
6
7
  export { default as EventTargetMixin } from "./src/runtime/EventTargetMixin.js";
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "@weborigami/language",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
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.13.5",
9
+ "@types/node": "22.13.13",
10
10
  "peggy": "4.2.0.",
11
- "typescript": "5.8.2",
12
- "yaml": "2.7.0"
11
+ "typescript": "5.8.2"
13
12
  },
14
13
  "dependencies": {
15
- "@weborigami/async-tree": "0.2.9",
16
- "@weborigami/types": "0.2.9",
17
- "watcher": "2.3.1"
14
+ "@weborigami/async-tree": "0.2.11",
15
+ "@weborigami/types": "0.2.11",
16
+ "watcher": "2.3.1",
17
+ "yaml": "2.7.0"
18
18
  },
19
19
  "scripts": {
20
20
  "build": "peggy --allowed-start-rules=\"*\" --format es src/compiler/origami.pegjs --output src/compiler/parse.js",
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Return true if the given text is Origami front matter
3
+ *
4
+ * Our heurstic is to see skip any initial alphanumeric or underscore
5
+ * characters, then see if the next character is a parenthesis, dot, slash,
6
+ * curly brace, or equals sign. If so, we assume this is an Origami expression.
7
+ *
8
+ * The goal is to identify Origami front matter like:
9
+ *
10
+ * ```
11
+ * fn(x) function call
12
+ * index.ori() file extension
13
+ * src/data.json file path
14
+ * // Hello comment
15
+ * { a: 1 } object literal
16
+ * ```
17
+ *
18
+ * These are generally invalid ways to start YAML. The last is valid YAML, but
19
+ * Origami supports the same syntax for basic object literals so interpreting as
20
+ * Origami should generally be acceptable.
21
+ *
22
+ * @param {string} text
23
+ */
24
+ export default function isOrigamiFrontMatter(text) {
25
+ return /^[ \t\r\n]*[A-Za-z0-9_]*[\(\.\/\{=]/.test(text);
26
+ }
@@ -34,7 +34,7 @@ export default function optimize(
34
34
  switch (fn) {
35
35
  case ops.lambda:
36
36
  const parameters = args[0];
37
- additionalLocalNames = parameters;
37
+ additionalLocalNames = parameters.map((param) => param[1]);
38
38
  break;
39
39
 
40
40
  case ops.literal:
@@ -61,7 +61,7 @@ export default function optimize(
61
61
  } else if (enableCaching && !locals[normalizedKey]) {
62
62
  // Upgrade to cached external scope reference
63
63
  return annotate(
64
- [ops.external, key, [ops.scope, key], cache],
64
+ [ops.external, key, annotate([ops.scope, key], code.location), cache],
65
65
  code.location
66
66
  );
67
67
  } else if (fn === undetermined) {
@@ -110,13 +110,15 @@ export default function optimize(
110
110
  }
111
111
 
112
112
  // Optimize children
113
- const optimized = code.map((child) => {
114
- if (Array.isArray(child) && "location" in child) {
113
+ const optimized = code.map((child, index) => {
114
+ // Don't optimize lambda parameter names
115
+ if (fn === ops.lambda && index === 1) {
116
+ return child;
117
+ } else if (Array.isArray(child) && "location" in child) {
115
118
  // Review: This currently descends into arrays that are not instructions,
116
- // such as the parameters of a lambda. This should be harmless, but it'd
119
+ // such as the entries of an ops.object. This should be harmless, but it'd
117
120
  // be preferable to only descend into instructions. This would require
118
- // surrounding ops.lambda parameters with ops.literal, and ops.object
119
- // entries with ops.array.
121
+ // surrounding ops.object entries with ops.array.
120
122
  return optimize(
121
123
  /** @type {AnnotatedCode} */ (child),
122
124
  enableCaching,
@@ -14,6 +14,7 @@
14
14
  import * as ops from "../runtime/ops.js";
15
15
  import {
16
16
  annotate,
17
+ applyMacro,
17
18
  downgradeReference,
18
19
  makeArray,
19
20
  makeBinaryOperation,
@@ -24,8 +25,10 @@ import {
24
25
  makeProperty,
25
26
  makeReference,
26
27
  makeTemplate,
27
- makeUnaryOperation
28
+ makeUnaryOperation,
29
+ makeYamlObject
28
30
  } from "./parserHelpers.js";
31
+ import isOrigamiFrontMatter from "./isOrigamiFrontMatter.js";
29
32
 
30
33
  }}
31
34
 
@@ -50,7 +53,7 @@ arguments "function arguments"
50
53
  / templateLiteral
51
54
 
52
55
  arrayLiteral "array"
53
- = "[" __ entries:arrayEntries? __ closingBracket {
56
+ = "[" __ entries:arrayEntries? __ expectClosingBracket {
54
57
  return makeArray(entries ?? [], location());
55
58
  }
56
59
 
@@ -69,13 +72,12 @@ arrayEntry
69
72
  }
70
73
 
71
74
  arrowFunction
72
- = "(" __ parameters:identifierList? __ ")" __ doubleArrow __ pipeline:pipelineExpression {
73
- const lambdaParameters = annotate(parameters ?? [], location());
75
+ = "(" __ parameters:parameterList? __ ")" __ doubleArrow __ pipeline:expectPipelineExpression {
76
+ const lambdaParameters = parameters ?? annotate([], location());
74
77
  return annotate([ops.lambda, lambdaParameters, pipeline], location());
75
78
  }
76
- / identifier:identifier __ doubleArrow __ pipeline:pipelineExpression {
77
- const lambdaParameters = annotate([identifier], location())
78
- return annotate([ops.lambda, lambdaParameters, pipeline], location());
79
+ / parameter:parameterSingleton __ doubleArrow __ pipeline:expectPipelineExpression {
80
+ return annotate([ops.lambda, parameter, pipeline], location());
79
81
  }
80
82
  / conditionalExpression
81
83
 
@@ -110,30 +112,6 @@ callExpression "function call"
110
112
  return tail.reduce(makeCall, head);
111
113
  }
112
114
 
113
- // Required closing curly brace. We use this for the `object` term: if the
114
- // parser sees a left curly brace, here we must see a right curly brace.
115
- closingBrace
116
- = "}"
117
- / .? {
118
- error(`An object ended without a closing brace, or contained something that wasn't expected.\nThe top level of an object can only contain definitions ("a: b" or "a = b") or spreads ("...a").`);
119
- }
120
-
121
- // Required closing bracket
122
- closingBracket
123
- = "]"
124
- / .? {
125
- error("Expected right bracket");
126
- }
127
-
128
- // Required closing parenthesis. We use this for the `group` term: it's the last
129
- // term in the `step` parser that starts with a parenthesis, so if that parser
130
- // sees a left parenthesis, here we must see a right parenthesis.
131
- closingParenthesis
132
- = ")"
133
- / .? {
134
- error("Expected right parenthesis");
135
- }
136
-
137
115
  // A comma-separated list of expressions: `x, y, z`
138
116
  commaExpression
139
117
  // The commas are required, but the list can have a single item.
@@ -171,7 +149,7 @@ digits
171
149
  doubleArrow = "⇒" / "=>"
172
150
 
173
151
  doubleQuoteString "double quote string"
174
- = '"' chars:doubleQuoteStringChar* '"' {
152
+ = '"' chars:doubleQuoteStringChar* expectDoubleQuote {
175
153
  return annotate([ops.literal, chars.join("")], location());
176
154
  }
177
155
 
@@ -201,6 +179,67 @@ escapedChar "backslash-escaped character"
201
179
  / "\\v" { return "\v"; }
202
180
  / "\\" @.
203
181
 
182
+ expectBacktick
183
+ = "`"
184
+ / .? {
185
+ error("Expected closing backtick");
186
+ }
187
+
188
+ expectClosingBrace
189
+ = "}"
190
+ / .? {
191
+ error(`An object ended without a closing brace, or contained something that wasn't expected.\nThe top level of an object can only contain definitions ("a: b" or "a = b") or spreads ("...a").`);
192
+ }
193
+
194
+ expectClosingBracket
195
+ = "]"
196
+ / .? {
197
+ error("Expected right bracket");
198
+ }
199
+
200
+ expectClosingParenthesis
201
+ = ")"
202
+ / .? {
203
+ error("Expected right parenthesis");
204
+ }
205
+
206
+ expectDoubleQuote
207
+ = '"'
208
+ / .? {
209
+ error("Expected closing quote");
210
+ }
211
+
212
+ expectExpression
213
+ = expression
214
+ / .? {
215
+ error("Expected an Origami expression");
216
+ }
217
+
218
+ expectFrontDelimiter
219
+ = frontDelimiter
220
+ / .? {
221
+ error("Expected \"---\"");
222
+ }
223
+
224
+ expectGuillemet
225
+ = '»'
226
+ / .? {
227
+ error("Expected closing guillemet");
228
+ }
229
+
230
+ expectSingleQuote
231
+ = "'"
232
+ / .? {
233
+ error("Expected closing quote");
234
+ }
235
+
236
+ // Required expression
237
+ expectPipelineExpression
238
+ = pipelineExpression
239
+ / .? {
240
+ error("Expected an expression");
241
+ }
242
+
204
243
  exponentiationExpression
205
244
  = left:unaryExpression right:(__ "**" __ @exponentiationExpression)? {
206
245
  return right ? annotate([ops.exponentiation, left, right], location()) : left;
@@ -215,14 +254,35 @@ floatLiteral "floating-point number"
215
254
  return annotate([ops.literal, parseFloat(text())], location());
216
255
  }
217
256
 
257
+ // Marker for the beginning or end of front matter
258
+ frontDelimiter
259
+ = "---\n"
260
+
261
+ // Origami front matter
262
+ frontMatterExpression
263
+ // If we detect Origami front matter, we need to see an expression
264
+ = frontDelimiter &{
265
+ return isOrigamiFrontMatter(input.slice(location().end.offset))
266
+ } @expectExpression expectFrontDelimiter
267
+
268
+ frontMatterText
269
+ = chars:( !frontDelimiter @. )* {
270
+ return chars.join("");
271
+ }
272
+
273
+ frontMatterYaml "YAML front matter"
274
+ = frontDelimiter yaml:frontMatterText frontDelimiter {
275
+ return makeYamlObject(yaml, location());
276
+ }
277
+
218
278
  // An expression in parentheses: `(foo)`
219
279
  group "parenthetical group"
220
- = "(" expression:expression closingParenthesis {
280
+ = "(" expression:expression expectClosingParenthesis {
221
281
  return annotate(downgradeReference(expression), location());
222
282
  }
223
283
 
224
284
  guillemetString "guillemet string"
225
- = '«' chars:guillemetStringChar* '»' {
285
+ = '«' chars:guillemetStringChar* expectGuillemet {
226
286
  return annotate([ops.literal, chars.join("")], location());
227
287
  }
228
288
 
@@ -254,11 +314,6 @@ identifierChar
254
314
  / @'-' !'>' // Accept a hyphen but not in a single arrow combination
255
315
  / escapedChar
256
316
 
257
- identifierList
258
- = list:identifier|1.., separator| separator? {
259
- return annotate(list, location());
260
- }
261
-
262
317
  implicitParenthesesCallExpression "function call with implicit parentheses"
263
318
  = head:arrowFunction args:(inlineSpace+ @implicitParensthesesArguments)? {
264
319
  return args ? makeCall(head, args) : head;
@@ -350,7 +405,7 @@ nullishCoalescingExpression
350
405
 
351
406
  // An object literal: `{foo: 1, bar: 2}`
352
407
  objectLiteral "object literal"
353
- = "{" __ entries:objectEntries? __ closingBrace {
408
+ = "{" __ entries:objectEntries? __ expectClosingBrace {
354
409
  return makeObject(entries ?? [], location());
355
410
  }
356
411
 
@@ -368,7 +423,7 @@ objectEntry
368
423
 
369
424
  // A getter definition inside an object literal: `foo = 1`
370
425
  objectGetter "object getter"
371
- = key:objectKey __ "=" __ pipeline:pipelineExpression {
426
+ = key:objectKey __ "=" __ pipeline:expectPipelineExpression {
372
427
  return annotate(
373
428
  makeProperty(key, annotate([ops.getter, pipeline], location())),
374
429
  location()
@@ -384,7 +439,7 @@ objectKey "object key"
384
439
 
385
440
  // A property definition in an object literal: `x: 1`
386
441
  objectProperty "object property"
387
- = key:objectKey __ ":" __ pipeline:pipelineExpression {
442
+ = key:objectKey __ ":" __ pipeline:expectPipelineExpression {
388
443
  return annotate(makeProperty(key, pipeline), location());
389
444
  }
390
445
 
@@ -404,9 +459,28 @@ objectPublicKey
404
459
  return string[1];
405
460
  }
406
461
 
462
+ parameter
463
+ = identifier:identifier {
464
+ return annotate([ops.literal, identifier], location());
465
+ }
466
+
467
+ parameterList
468
+ = list:parameter|1.., separator| separator? {
469
+ return annotate(list, location());
470
+ }
471
+
472
+ // A list with a single identifier
473
+ parameterSingleton
474
+ = identifier:identifier {
475
+ return annotate(
476
+ [annotate([ops.literal, identifier], location())],
477
+ location()
478
+ );
479
+ }
480
+
407
481
  // Function arguments in parentheses
408
482
  parenthesesArguments "function arguments in parentheses"
409
- = "(" __ list:list? __ ")" {
483
+ = "(" __ list:list? __ expectClosingParenthesis {
410
484
  return annotate(list ?? [undefined], location());
411
485
  }
412
486
 
@@ -535,7 +609,10 @@ shiftOperator
535
609
  shorthandFunction "lambda function"
536
610
  // Avoid a following equal sign (for an equality)
537
611
  = "=" !"=" __ definition:implicitParenthesesCallExpression {
538
- const lambdaParameters = annotate(["_"], location());
612
+ const lambdaParameters = annotate(
613
+ [annotate([ops.literal, "_"], location())],
614
+ location()
615
+ );
539
616
  return annotate([ops.lambda, lambdaParameters, definition], location());
540
617
  }
541
618
  / implicitParenthesesCallExpression
@@ -548,7 +625,7 @@ singleLineComment
548
625
  = "//" [^\n\r]* { return null; }
549
626
 
550
627
  singleQuoteString "single quote string"
551
- = "'" chars:singleQuoteStringChar* "'" {
628
+ = "'" chars:singleQuoteStringChar* expectSingleQuote {
552
629
  return annotate([ops.literal, chars.join("")], location());
553
630
  }
554
631
 
@@ -578,29 +655,42 @@ stringLiteral "string"
578
655
  / singleQuoteString
579
656
  / guillemetString
580
657
 
581
- // A top-level document defining a template. This is the same as a template
582
- // literal, but can contain backticks at the top level.
583
- templateDocument "template"
584
- = head:templateDocumentText tail:(templateSubstitution templateDocumentText)* {
585
- const lambdaParameters = annotate(["_"], location());
658
+ // The body of a template document is a kind of template literal that can
659
+ // contain backticks at the top level.
660
+ templateBody "template"
661
+ = head:templateBodyText tail:(templateSubstitution templateBodyText)* {
662
+ const lambdaParameters = annotate(
663
+ [annotate([ops.literal, "_"], location())],
664
+ location()
665
+ );
586
666
  return annotate(
587
667
  [ops.lambda, lambdaParameters, makeTemplate(ops.templateIndent, head, tail, location())],
588
668
  location()
589
669
  );
590
670
  }
591
671
 
592
- // Template documents can contain backticks at the top level.
593
- templateDocumentChar
672
+ // Template document bodies can contain backticks at the top level
673
+ templateBodyChar
594
674
  = !("${") @textChar
595
675
 
596
- templateDocumentText "template text"
597
- = chars:templateDocumentChar* {
676
+ templateBodyText "template text"
677
+ = chars:templateBodyChar* {
598
678
  return annotate([ops.literal, chars.join("")], location());
599
679
  }
600
680
 
681
+ templateDocument "template document"
682
+ = front:frontMatterExpression __ body:templateBody {
683
+ return annotate(applyMacro(front, "@template", body), location());
684
+ }
685
+ / front:frontMatterYaml? body:templateBody {
686
+ return front
687
+ ? annotate([ops.document, front, body], location())
688
+ : annotate(body, location());
689
+ }
690
+
601
691
  // A backtick-quoted template literal
602
692
  templateLiteral "template literal"
603
- = "`" head:templateLiteralText tail:(templateSubstitution templateLiteralText)* "`" {
693
+ = "`" head:templateLiteralText tail:(templateSubstitution templateLiteralText)* expectBacktick {
604
694
  return makeTemplate(ops.template, head, tail, location());
605
695
  }
606
696
 
@@ -615,7 +705,7 @@ templateLiteralText
615
705
 
616
706
  // A substitution in a template literal: `${x}`
617
707
  templateSubstitution "template substitution"
618
- = "${" expression:expression "}" {
708
+ = "${" expression:expectExpression "}" {
619
709
  return annotate(expression, location());
620
710
  }
621
711