@weborigami/language 0.3.3-jse.3 → 0.3.4-jse.4

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.
@@ -15,16 +15,14 @@ import * as ops from "../runtime/ops.js";
15
15
  import {
16
16
  annotate,
17
17
  applyMacro,
18
- downgradeReference,
19
18
  makeArray,
20
19
  makeBinaryOperation,
21
20
  makeCall,
22
21
  makeDeferredArguments,
23
22
  makeDocument,
24
- makeJsPropertyAccess,
25
23
  makeObject,
24
+ makePath,
26
25
  makePipeline,
27
- makeProperty,
28
26
  makeTemplate,
29
27
  makeUnaryOperation,
30
28
  makeYamlObject,
@@ -41,58 +39,51 @@ __
41
39
  }
42
40
 
43
41
  additiveExpression
44
- = head:multiplicativeExpression tail:(whitespaceShell @additiveOperator whitespaceShell @multiplicativeExpression)* {
42
+ = head:multiplicativeExpression tail:(whitespace @additiveOperator whitespace @multiplicativeExpression)* {
45
43
  return tail.reduce(makeBinaryOperation, head);
46
44
  }
47
45
 
48
46
  additiveOperator
49
47
  = "+"
50
- / "-"
48
+ / minus
51
49
 
52
50
  angleBracketLiteral
53
- = "<" __ protocol:angleBracketProtocol "//"? path:angleBracketPath __ ">" {
54
- return annotate([protocol, ...path], location());
51
+ = "<" scheme:uriScheme "//"? path:angleBracketPath ">" {
52
+ return annotate([scheme, ...path], location());
55
53
  }
56
- / "<" __ "/" path:angleBracketPath __ ">" {
57
- const root = annotate([ops.rootDirectory], location());
58
- return path.length > 0 ? annotate([root, ...path], location()) : root;
54
+ / "</" path:angleBracketPath ">" {
55
+ const external = annotate([markers.external, "/"], location());
56
+ return annotate([markers.traverse, external, ...path], location());
59
57
  }
60
- / "<" __ "~" "/"? path:angleBracketPath __ ">" {
61
- const home = annotate([ops.homeDirectory], location());
62
- return path.length > 0 ? annotate([home, ...path], location()) : home;
63
- }
64
- / "<" __ path:angleBracketPath __ ">" {
65
- // Angle bracket paths always reference scope
66
- const scope = annotate([ops.scope], location());
67
- return annotate([scope, ...path], location());
58
+ / "<" path:angleBracketPath ">" {
59
+ const [head, ...tail] = path;
60
+ const external = annotate([markers.external, head[1]], location());
61
+ return annotate([markers.traverse, external, ...tail], location());
68
62
  }
69
63
 
70
64
  angleBracketPath
71
- = @angleBracketPathKey|0.., "/"| "/"?
65
+ = @angleBracketKey|0.., "/"| "/"?
72
66
 
73
- angleBracketPathKey
67
+ // Single key in an angle bracket path, possibly with a trailing slash
68
+ angleBracketKey
74
69
  = chars:angleBracketPathChar+ slashFollows:slashFollows? {
75
70
  // Append a trailing slash if one follows (but don't consume it)
76
71
  const key = chars.join("") + (slashFollows ? "/" : "");
77
72
  return annotate([ops.literal, key], location());
78
73
  }
79
74
 
80
- // A single character in a slash-separated path segment
75
+ // A single character in an angle bracket key
81
76
  angleBracketPathChar
82
- = [^/:<>] // Much more permissive than an identifier
77
+ // Accept anything that doesn't end the angle bracket key or path
78
+ = [^/>\t\n\r]
83
79
  / escapedChar
84
80
 
85
- angleBracketProtocol
86
- = protocol:jsIdentifier ":" {
87
- return annotate([markers.global, `${protocol[1]}:`], location());
88
- }
89
-
90
81
  arguments "function arguments"
91
82
  = parenthesesArguments
92
- / shellMode @pathArguments
93
- / jsPropertyAccess
83
+ / pathArguments
84
+ / propertyAccess
94
85
  / computedPropertyAccess
95
- / optionalChaining
86
+ // / optionalChaining
96
87
  / templateLiteral
97
88
 
98
89
  arrayLiteral "array"
@@ -125,7 +116,7 @@ arrowFunction
125
116
  / conditionalExpression
126
117
 
127
118
  bitwiseAndExpression
128
- = head:equalityExpression tail:(__ @bitwiseAndOperator __ @equalityExpression)* {
119
+ = head:equalityExpression tail:(whitespace @bitwiseAndOperator whitespace @equalityExpression)* {
129
120
  return tail.reduce(makeBinaryOperation, head);
130
121
  }
131
122
 
@@ -133,7 +124,7 @@ bitwiseAndOperator
133
124
  = @"&" !"&"
134
125
 
135
126
  bitwiseOrExpression
136
- = head:bitwiseXorExpression tail:(__ @bitwiseOrOperator __ @bitwiseXorExpression)* {
127
+ = head:bitwiseXorExpression tail:(whitespace @bitwiseOrOperator whitespace @bitwiseXorExpression)* {
137
128
  return tail.reduce(makeBinaryOperation, head);
138
129
  }
139
130
 
@@ -141,7 +132,7 @@ bitwiseOrOperator
141
132
  = @"|" !"|"
142
133
 
143
134
  bitwiseXorExpression
144
- = head:bitwiseAndExpression tail:(__ @bitwiseXorOperator __ @bitwiseAndExpression)* {
135
+ = head:bitwiseAndExpression tail:(whitespace @bitwiseXorOperator whitespace @bitwiseAndExpression)* {
145
136
  return tail.reduce(makeBinaryOperation, head);
146
137
  }
147
138
 
@@ -151,8 +142,11 @@ bitwiseXorOperator
151
142
  // A function call: `fn(arg)`, possibly part of a chain of function calls, like
152
143
  // `fn(arg1)(arg2)(arg3)`.
153
144
  callExpression "function call"
154
- = head:protocolExpression tail:arguments* {
155
- return tail.reduce((target, args) => makeCall(target, args, options.mode), head);
145
+ = head:uriExpression tail:arguments* {
146
+ return tail.reduce(
147
+ (target, args) => makeCall(target, args, location()),
148
+ head
149
+ );
156
150
  }
157
151
 
158
152
  // A comma-separated list of expressions: `x, y, z`
@@ -171,7 +165,7 @@ comment "comment"
171
165
 
172
166
  computedPropertyAccess
173
167
  = __ "[" expression:expression expectClosingBracket {
174
- return annotate([markers.traverse, expression], location());
168
+ return annotate([markers.property, expression], location());
175
169
  }
176
170
 
177
171
  conditionalExpression
@@ -185,9 +179,9 @@ conditionalExpression
185
179
  const deferred = makeDeferredArguments(tail);
186
180
  return annotate([
187
181
  ops.conditional,
188
- downgradeReference(condition),
189
- downgradeReference(deferred[0]),
190
- downgradeReference(deferred[1])
182
+ condition,
183
+ deferred[0],
184
+ deferred[1]
191
185
  ], location());
192
186
  }
193
187
 
@@ -289,7 +283,7 @@ expectPipelineExpression
289
283
  }
290
284
 
291
285
  exponentiationExpression
292
- = left:unaryExpression right:(__ "**" __ @exponentiationExpression)? {
286
+ = left:unaryExpression right:(whitespace "**" whitespace @exponentiationExpression)? {
293
287
  return right ? annotate([ops.exponentiation, left, right], location()) : left;
294
288
  }
295
289
 
@@ -326,7 +320,7 @@ frontMatterYaml "YAML front matter"
326
320
  // An expression in parentheses: `(foo)`
327
321
  group "parenthetical group"
328
322
  = "(" expression:expression expectClosingParenthesis {
329
- return annotate(downgradeReference(expression), location());
323
+ return annotate(expression, location());
330
324
  }
331
325
 
332
326
  guillemetString "guillemet string"
@@ -337,34 +331,47 @@ guillemetString "guillemet string"
337
331
  guillemetStringChar
338
332
  = !('»' / newLine) @textChar
339
333
 
340
- // The user's home directory: `~`
341
- homeDirectory
342
- = "~" {
343
- return annotate([ops.homeDirectory], location());
344
- }
345
-
346
334
  // A host identifier that may include a colon and port number: `example.com:80`.
347
335
  // This is used as a special case at the head of a path, where we want to
348
336
  // interpret a colon as part of a text identifier.
349
337
  host "HTTP/HTTPS host"
350
- = identifier:identifier port:(":" @integerLiteral)? slashFollows:slashFollows? {
351
- const portText = port ? `:${port[1]}` : "";
352
- const slashText = slashFollows ? "/" : "";
353
- const hostText = identifier + portText + slashText;
354
- return annotate([ops.literal, hostText], location());
338
+ = name:hostname port:(":" @integerLiteral)? slashFollows:slashFollows? {
339
+ const portText = port ? `:${port[1]}` : "";
340
+ const slashText = slashFollows ? "/" : "";
341
+ const host = name + portText + slashText;
342
+ return annotate([ops.literal, host], location());
343
+ }
344
+
345
+ hostname
346
+ = key {
347
+ return text();
355
348
  }
356
349
 
357
- identifier "identifier"
358
- = chars:identifierChar+ { return chars.join(""); }
350
+ // JavaScript-compatible identifier
351
+ identifier
352
+ = id:$( identifierStart identifierPart* ) {
353
+ return id;
354
+ }
359
355
 
360
- identifierChar
361
- = [^(){}\[\]<>\?!\|\-=,/:\`"'«»\\→⇒… \t\n\r] // No unescaped whitespace or special chars
362
- / @'-' !'>' // Accept a hyphen but not in a single arrow combination
363
- / escapedChar
356
+ // Identifier as a literal
357
+ identifierLiteral
358
+ = id:identifier {
359
+ return annotate([ops.literal, id], location());
360
+ }
361
+
362
+ // Continuation of a JavaScript identifier
363
+ // https://tc39.es/ecma262/multipage/ecmascript-language-lexical-grammar.html#prod-IdentifierPart
364
+ identifierPart "JavaScript identifier continuation"
365
+ = char:. &{ return char.match(/[$_\p{ID_Continue}]/u) }
366
+
367
+ // Start of a JavaScript identifier
368
+ // https://tc39.es/ecma262/multipage/ecmascript-language-lexical-grammar.html#prod-IdentifierStart
369
+ identifierStart "JavaScript identifier start"
370
+ = char:. &{ return char.match(/[$_\p{ID_Start}]/u) }
364
371
 
365
372
  implicitParenthesesCallExpression "function call with implicit parentheses"
366
373
  = head:arrowFunction args:(inlineSpace+ @implicitParensthesesArguments)? {
367
- return args ? makeCall(head, args, options.mode) : head;
374
+ return args ? makeCall(head, args, location()) : head;
368
375
  }
369
376
 
370
377
  // A separated list of values for an implicit parens call. This differs from
@@ -382,33 +389,32 @@ integerLiteral "integer"
382
389
  return annotate([ops.literal, parseInt(text())], location());
383
390
  }
384
391
 
385
- jseMode
386
- = &{ return options.mode === "jse" }
387
-
388
- jsIdentifier
389
- = id:$( jsIdentifierStart jsIdentifierPart* ) {
390
- return annotate([ops.literal, id], location());
392
+ // A key in a path or an expression that looks like one
393
+ key
394
+ = keyCharStart keyChar* {
395
+ return text();
391
396
  }
392
397
 
393
- // Continuation of a JavaScript identifier
394
- // https://tc39.es/ecma262/multipage/ecmascript-language-lexical-grammar.html#prod-IdentifierPart
395
- jsIdentifierPart "JavaScript identifier continuation"
396
- = char:. &{ return char.match(/[$_\p{ID_Continue}]/u) }
397
-
398
- // Start of a JavaScript identifier
399
- // https://tc39.es/ecma262/multipage/ecmascript-language-lexical-grammar.html#prod-IdentifierStart
400
- jsIdentifierStart "JavaScript identifier start"
401
- = char:. &{ return char.match(/[$_\p{ID_Start}]/u) }
402
-
403
- jsPropertyAccess
404
- = __ "." __ property:jsIdentifier {
405
- return annotate([markers.traverse, property], location());
406
- }
398
+ // Character after the first in a key
399
+ keyChar
400
+ = keyCharStart
401
+ // Also allow some math operators (not slash)
402
+ / "!"
403
+ / "+"
404
+ / minus
405
+ / "*"
406
+ / "%"
407
+ / "&"
408
+ / "|"
409
+ / "^"
407
410
 
408
- jsReference "identifier reference"
409
- = id:jsIdentifier {
410
- return annotate([markers.reference, id], location());
411
- }
411
+ // First character in a key
412
+ keyCharStart
413
+ // All JS identifier characters
414
+ = char:. &{ return char.match(/[$_\p{ID_Continue}]/u) }
415
+ / "."
416
+ / "~"
417
+ / "@"
412
418
 
413
419
  // A separated list of values
414
420
  list "list"
@@ -421,7 +427,7 @@ logicalAndExpression
421
427
  return tail.length === 0
422
428
  ? head
423
429
  : annotate(
424
- [ops.logicalAnd, downgradeReference(head), ...makeDeferredArguments(tail)],
430
+ [ops.logicalAnd, head, ...makeDeferredArguments(tail)],
425
431
  location()
426
432
  );
427
433
  }
@@ -431,16 +437,22 @@ logicalOrExpression
431
437
  return tail.length === 0
432
438
  ? head
433
439
  : annotate(
434
- [ops.logicalOr, downgradeReference(head), ...makeDeferredArguments(tail)],
440
+ [ops.logicalOr, head, ...makeDeferredArguments(tail)],
435
441
  location()
436
442
  );
437
443
  }
438
444
 
445
+ // Unary or binary minus operator
446
+ minus
447
+ // Don't match a front matter delimiter or pipeline operator. For some reason,
448
+ // the negative lookahead !"--\n" doesn't work.
449
+ = @"-" !"-\n" !">"
450
+
439
451
  multiLineComment
440
452
  = "/*" (!"*/" .)* "*/" { return null; }
441
453
 
442
454
  multiplicativeExpression
443
- = head:exponentiationExpression tail:(whitespaceShell @multiplicativeOperator whitespaceShell @exponentiationExpression)* {
455
+ = head:exponentiationExpression tail:(whitespace @multiplicativeOperator whitespace @exponentiationExpression)* {
444
456
  return tail.reduce(makeBinaryOperation, head);
445
457
  }
446
458
 
@@ -449,19 +461,13 @@ multiplicativeOperator
449
461
  / "/"
450
462
  / "%"
451
463
 
452
- // A namespace reference is a string of letters only, followed by a colon.
453
- namespace
454
- = chars:[A-Za-z]+ ":" {
455
- return annotate([markers.global, chars.join("") + ":"], location());
456
- }
457
-
458
464
  // A new expression: `new Foo()`
459
465
  newExpression
460
- = "new" __ head:jsReference tail:parenthesesArguments {
466
+ = "new" __ head:pathLiteral tail:parenthesesArguments? {
461
467
  const args = tail?.[0] !== undefined ? tail : [];
462
468
  return annotate([ops.construct, head, ...args], location());
463
469
  }
464
- / "new:" head:jsReference tail:parenthesesArguments {
470
+ / "new:" head:pathLiteral tail:parenthesesArguments {
465
471
  const args = tail?.[0] !== undefined ? tail : [];
466
472
  return annotate([ops.construct, head, ...args], location());
467
473
  }
@@ -481,7 +487,7 @@ nullishCoalescingExpression
481
487
  return tail.length === 0
482
488
  ? head
483
489
  : annotate(
484
- [ops.nullishCoalescing, downgradeReference(head), ...makeDeferredArguments(tail)],
490
+ [ops.nullishCoalescing, head, ...makeDeferredArguments(tail)],
485
491
  location()
486
492
  );
487
493
  }
@@ -507,10 +513,8 @@ objectEntry
507
513
  // A getter definition inside an object literal: `foo = 1`
508
514
  objectGetter "object getter"
509
515
  = key:objectKey __ "=" __ pipeline:expectPipelineExpression {
510
- return annotate(
511
- makeProperty(key, annotate([ops.getter, pipeline], location())),
512
- location()
513
- );
516
+ const getter = annotate([ops.getter, pipeline], location());
517
+ return annotate([key, getter], location());
514
518
  }
515
519
 
516
520
  objectHiddenKey
@@ -523,16 +527,17 @@ objectKey "object key"
523
527
  // A property definition in an object literal: `x: 1`
524
528
  objectProperty "object property"
525
529
  = key:objectKey __ ":" __ pipeline:expectPipelineExpression {
526
- return annotate(makeProperty(key, pipeline), location());
530
+ return annotate([key, pipeline], location());
527
531
  }
528
532
 
529
533
  // A shorthand reference inside an object literal: `foo`
530
534
  objectShorthandProperty "object identifier"
531
535
  = key:objectPublicKey {
532
536
  const reference = annotate([markers.reference, key], location());
533
- return annotate([key, reference], location());
537
+ const traverse = annotate([markers.traverse, reference], location());
538
+ return annotate([key, traverse], location());
534
539
  }
535
- / jseMode path:angleBracketLiteral {
540
+ / path:angleBracketLiteral {
536
541
  let lastKey = path.at(-1);
537
542
  if (lastKey instanceof Array) {
538
543
  lastKey = lastKey[1]; // get scope identifier or literal
@@ -541,23 +546,23 @@ objectShorthandProperty "object identifier"
541
546
  }
542
547
 
543
548
  objectPublicKey
544
- = identifier:identifier slash:"/"? {
545
- return identifier + (slash ?? "");
546
- }
549
+ = key:key slash:"/"? {
550
+ return text();
551
+ }
547
552
  / string:stringLiteral {
548
553
  // Remove `ops.literal` from the string code
549
554
  return string[1];
550
555
  }
551
556
 
552
557
  optionalChaining
553
- = __ "?." __ property:jsIdentifier {
558
+ = __ "?." __ property:identifier {
554
559
  return annotate([ops.optionalTraverse, property], location());
555
560
  }
556
-
561
+
562
+ // Name of a unction parameter
557
563
  parameter
558
- = jseMode @jsIdentifier
559
- / shellMode identifier:identifier {
560
- return annotate([ops.literal, identifier], location());
564
+ = key:key {
565
+ return annotate([ops.literal, key], location());
561
566
  }
562
567
 
563
568
  parameterList
@@ -567,11 +572,8 @@ parameterList
567
572
 
568
573
  // A list with a single identifier
569
574
  parameterSingleton
570
- = identifier:identifier {
571
- return annotate(
572
- [annotate([ops.literal, identifier], location())],
573
- location()
574
- );
575
+ = param:parameter {
576
+ return annotate([param], location());
575
577
  }
576
578
 
577
579
  // Function arguments in parentheses
@@ -580,90 +582,66 @@ parenthesesArguments "function arguments in parentheses"
580
582
  return annotate(list ?? [undefined], location());
581
583
  }
582
584
 
583
- // A slash-separated path of keys: `a/b/c`
584
- path "slash-separated path"
585
- // Path with at least a tail
586
- = segments:pathSegment|1..| {
587
- // Drop empty segments that represent consecutive or final slashes
588
- segments = segments.filter(segment => segment);
589
- return annotate(segments, location());
590
- }
591
-
592
- // A slash-separated path of keys that follows a call target
585
+ // A slash-separated path of keys that follows a call target, such as the path
586
+ // after the slash in `(x)/y/z`
593
587
  pathArguments
594
- = path:path {
595
- return annotate([markers.traverse, ...path], location());
588
+ = "/" keys:pathKeys? {
589
+ const args = keys ?? [];
590
+ return annotate([markers.traverse, ...args], location());
596
591
  }
597
592
 
598
- // A single key in a slash-separated path: `/a`
599
- pathKey
600
- = chars:pathSegmentChar+ slashFollows:slashFollows? {
601
- // Append a trailing slash if one follows (but don't consume it)
602
- const key = chars.join("") + (slashFollows ? "/" : "");
603
- return annotate([ops.literal, key], location());
604
- }
593
+ // Sequence of keys that may each have trailing slashes
594
+ pathKeys
595
+ = pathSegment|1..|
596
+
597
+ // A path without angle brackets
598
+ pathLiteral
599
+ = keys:pathKeys {
600
+ return makePath(keys);
601
+ }
605
602
 
603
+ // A path key with an optional trailing slash
606
604
  pathSegment
607
- = "/" @pathKey?
608
-
609
- // A single character in a slash-separated path segment
610
- pathSegmentChar
611
- // This is more permissive than an identifier. It allows some characters like
612
- // brackets or quotes that are not allowed in identifiers.
613
- = [^(){}\[\],:/\\ \t\n\r]
614
- / escapedChar
605
+ = key:key "/"? {
606
+ return annotate([ops.literal, text()], location());
607
+ }
608
+ // A single slash is a path key
609
+ / "/" {
610
+ return annotate([ops.literal, text()], location());
611
+ }
615
612
 
616
613
  // A pipeline that starts with a value and optionally applies a series of
617
614
  // functions to it.
618
615
  pipelineExpression
619
616
  = head:shorthandFunction tail:(__ singleArrow __ @shorthandFunction)* {
620
617
  return annotate(
621
- tail.reduce((arg, fn) => makePipeline(arg, fn, options.mode), downgradeReference(head)),
618
+ tail.reduce((arg, fn) => makePipeline(arg, fn, location()), head),
622
619
  location()
623
620
  );
624
621
  }
625
622
 
626
623
  primary
627
- = numericLiteral
628
- / stringLiteral
624
+ // The following start with distinct characters
625
+ = stringLiteral
629
626
  / arrayLiteral
630
627
  / objectLiteral
631
628
  / group
632
- / templateLiteral
633
- / shellMode @primaryShell
634
- / jseMode @primaryJse
635
-
636
- // Primary allowed in JSE mode
637
- primaryJse
638
- = angleBracketLiteral
639
- / jsReference
629
+ / angleBracketLiteral
640
630
  / regexLiteral
631
+ / templateLiteral
641
632
 
642
- primaryShell
643
- = rootDirectory
644
- / homeDirectory
645
- / qualifiedReference
646
- / namespace
647
- / scopeReference
633
+ // These are more ambiguous
634
+ / @numericLiteral !keyChar // numbers + chars would be a key
635
+ / pathLiteral
648
636
 
649
637
  // Top-level Origami progam with possible shebang directive (which is ignored)
650
638
  program "Origami program"
651
639
  = shebang? @expression
652
640
 
653
- // Protocol with double-slash path: `https://example.com/index.html`
654
- protocolExpression
655
- = fn:namespace "//" host:(host / slash) path:path? {
656
- const keys = annotate([host, ...(path ?? [])], location());
657
- return makeCall(fn, keys, options.mode);
658
- }
659
- / newExpression
660
- / primary
661
-
662
- // A namespace followed by a key: `foo:x`
663
- qualifiedReference
664
- = fn:namespace reference:scopeReference {
665
- return makeCall(fn, [reference[1]], options.mode);
666
- }
641
+ propertyAccess
642
+ = __ "." __ property:identifierLiteral {
643
+ return annotate([markers.property, property], location());
644
+ }
667
645
 
668
646
  regexFlags
669
647
  = flags:[gimuy]* {
@@ -695,19 +673,6 @@ relationalOperator
695
673
  / ">="
696
674
  / ">"
697
675
 
698
- // The root folder: `/`
699
- rootDirectory
700
- = &("/" !"/") {
701
- return annotate([ops.rootDirectory], location());
702
- }
703
-
704
- scopeReference "scope reference"
705
- = identifier:identifier slashFollows:slashFollows? {
706
- const id = identifier + (slashFollows ? "/" : "");
707
- const idCode = annotate([ops.literal, identifier], location());
708
- return annotate([markers.reference, idCode], location());
709
- }
710
-
711
676
  separator
712
677
  = __ "," __
713
678
  / @whitespaceWithNewLine
@@ -761,6 +726,12 @@ slash
761
726
  return annotate([ops.literal, "/"], location());
762
727
  }
763
728
 
729
+ // One or more consecutive slashes
730
+ slashes
731
+ = "/"+ {
732
+ return annotate([ops.literal, "/"], location());
733
+ }
734
+
764
735
  // Check whether next character is a slash without consuming input
765
736
  slashFollows
766
737
  // This expression returned `undefined` if successful; we convert to `true`
@@ -796,13 +767,16 @@ templateBodyText "template text"
796
767
 
797
768
  templateDocument "template document"
798
769
  = front:frontMatterExpression __ body:templateBody {
799
- const macroName = options.mode === "jse" ? "_template" : "@template";
770
+ const macroName = text().includes("@template") ? "@template" : "_template";
800
771
  return annotate(applyMacro(front, macroName, body), location());
801
772
  }
802
773
  / front:frontMatterYaml body:templateBody {
803
- return makeDocument(options.mode, front, body, location());
774
+ return makeDocument(front, body, location());
804
775
  }
805
776
  / body:templateBody {
777
+ if (options.front) {
778
+ return makeDocument(options.front, body, location());
779
+ }
806
780
  const lambdaParameters = annotate(
807
781
  [annotate([ops.literal, "_"], location())],
808
782
  location()
@@ -813,8 +787,7 @@ templateDocument "template document"
813
787
  // A backtick-quoted template literal
814
788
  templateLiteral "template literal"
815
789
  = "`" head:templateLiteralText tail:(templateSubstitution templateLiteralText)* expectBacktick {
816
- const op = options.mode === "jse" ? ops.templateStandard : ops.templateTree;
817
- return makeTemplate(op, head, tail, location());
790
+ return makeTemplate(ops.templateTree, head, tail, location());
818
791
  }
819
792
 
820
793
  templateLiteralChar
@@ -838,29 +811,69 @@ textChar
838
811
 
839
812
  // A unary prefix operator: `!x`
840
813
  unaryExpression
841
- = operator:unaryOperator __ expression:unaryExpression {
814
+ = operator:unaryOperator __ expression:expectExpression {
842
815
  return makeUnaryOperation(operator, expression, location());
843
816
  }
844
817
  / callExpression
845
818
 
819
+ // URI
820
+ uri
821
+ // Double slashes after colon: `https://example.com/index.html`
822
+ = scheme:uriScheme "//" host:host path:("/" uriPath)? {
823
+ const rest = path ? path[1] : [];
824
+ const keys = annotate([host, ...rest], location());
825
+ return makeCall(scheme, keys, location());
826
+ }
827
+ // No slashes after colon: `files:assets`
828
+ / scheme:uriScheme keys:pathKeys {
829
+ return makeCall(scheme, keys, location());
830
+ }
831
+
832
+ // URI expression
833
+ uriExpression
834
+ = uri
835
+ / newExpression
836
+ / primary
837
+
838
+ // A single key in a path, possibly with trailing slash: `a/`, `b`
839
+ uriKey
840
+ = chars:uriKeyChar+ "/"? {
841
+ return annotate([ops.literal, text()], location());
842
+ }
843
+ / "/" {
844
+ // A single slash is a path key
845
+ return annotate([ops.literal, ""], location());
846
+ }
847
+
848
+ // A single character in a URI key
849
+ uriKeyChar
850
+ // Accept anything that doesn't end the URI key or path
851
+ = [^/,\)\]\}\s]
852
+ / escapedChar
853
+
854
+ // A slash-separated path of keys: `a/b/c`
855
+ uriPath "slash-separated path"
856
+ = keys:uriKey|1..| {
857
+ return annotate(keys, location());
858
+ }
859
+
860
+ // URI scheme, commonly called a protocol
861
+ // See https://datatracker.ietf.org/doc/html/rfc3986#section-3.1
862
+ uriScheme
863
+ = [a-z][a-z0-9+-.]*[:] {
864
+ return annotate([markers.global, text()], location());
865
+ }
866
+
846
867
  unaryOperator
847
868
  = "!"
848
869
  / "+"
849
- // Don't match a front matter delimiter. For some reason, the negative
850
- // lookahead !"--\n" doesn't work.
851
- / @"-" !"-\n"
852
- // Don't match a path that starts with a tilde: ~/foo
853
- / @"~" !"/"
870
+ / @"~" ![\/\)\]\}] // don't match `~/` or end of term
871
+ / minus
854
872
 
855
873
  whitespace
856
874
  = inlineSpace
857
875
  / newLine
858
876
  / comment
859
877
 
860
- // Whitespace required in shell mode, optional in JSE mode
861
- whitespaceShell
862
- = shellMode whitespace
863
- / jseMode __
864
-
865
878
  whitespaceWithNewLine
866
879
  = inlineSpace* comment? newLine __