@weborigami/language 0.3.3 → 0.3.4-jse.5

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,19 +15,18 @@ 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
- makeJsPropertyAccess,
22
+ makeDocument,
24
23
  makeObject,
24
+ makePath,
25
25
  makePipeline,
26
- makeProperty,
27
- makeReference,
28
26
  makeTemplate,
29
27
  makeUnaryOperation,
30
- makeYamlObject
28
+ makeYamlObject,
29
+ markers,
31
30
  } from "./parserHelpers.js";
32
31
  import isOrigamiFrontMatter from "./isOrigamiFrontMatter.js";
33
32
 
@@ -46,13 +45,45 @@ additiveExpression
46
45
 
47
46
  additiveOperator
48
47
  = "+"
49
- / "-"
48
+ / minus
49
+
50
+ angleBracketLiteral
51
+ = "<" scheme:uriScheme "//"? path:angleBracketPath ">" {
52
+ return annotate([scheme, ...path], location());
53
+ }
54
+ / "</" path:angleBracketPath ">" {
55
+ const external = annotate([markers.external, "/"], location());
56
+ return annotate([markers.traverse, external, ...path], location());
57
+ }
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());
62
+ }
63
+
64
+ angleBracketPath
65
+ = @angleBracketKey|0.., "/"| "/"?
66
+
67
+ // Single key in an angle bracket path, possibly with a trailing slash
68
+ angleBracketKey
69
+ = chars:angleBracketPathChar+ slashFollows:slashFollows? {
70
+ // Append a trailing slash if one follows (but don't consume it)
71
+ const key = chars.join("") + (slashFollows ? "/" : "");
72
+ return annotate([ops.literal, key], location());
73
+ }
74
+
75
+ // A single character in an angle bracket key
76
+ angleBracketPathChar
77
+ // Accept anything that doesn't end the angle bracket key or path
78
+ = [^/>\t\n\r]
79
+ / escapedChar
50
80
 
51
81
  arguments "function arguments"
52
82
  = parenthesesArguments
53
83
  / pathArguments
54
- / jsPropertyAccess
84
+ / propertyAccess
55
85
  / computedPropertyAccess
86
+ // / optionalChaining
56
87
  / templateLiteral
57
88
 
58
89
  arrayLiteral "array"
@@ -85,7 +116,7 @@ arrowFunction
85
116
  / conditionalExpression
86
117
 
87
118
  bitwiseAndExpression
88
- = head:equalityExpression tail:(__ @bitwiseAndOperator __ @equalityExpression)* {
119
+ = head:equalityExpression tail:(whitespace @bitwiseAndOperator whitespace @equalityExpression)* {
89
120
  return tail.reduce(makeBinaryOperation, head);
90
121
  }
91
122
 
@@ -93,7 +124,7 @@ bitwiseAndOperator
93
124
  = @"&" !"&"
94
125
 
95
126
  bitwiseOrExpression
96
- = head:bitwiseXorExpression tail:(__ @bitwiseOrOperator __ @bitwiseXorExpression)* {
127
+ = head:bitwiseXorExpression tail:(whitespace @bitwiseOrOperator whitespace @bitwiseXorExpression)* {
97
128
  return tail.reduce(makeBinaryOperation, head);
98
129
  }
99
130
 
@@ -101,7 +132,7 @@ bitwiseOrOperator
101
132
  = @"|" !"|"
102
133
 
103
134
  bitwiseXorExpression
104
- = head:bitwiseAndExpression tail:(__ @bitwiseXorOperator __ @bitwiseAndExpression)* {
135
+ = head:bitwiseAndExpression tail:(whitespace @bitwiseXorOperator whitespace @bitwiseAndExpression)* {
105
136
  return tail.reduce(makeBinaryOperation, head);
106
137
  }
107
138
 
@@ -111,8 +142,11 @@ bitwiseXorOperator
111
142
  // A function call: `fn(arg)`, possibly part of a chain of function calls, like
112
143
  // `fn(arg1)(arg2)(arg3)`.
113
144
  callExpression "function call"
114
- = head:protocolExpression tail:arguments* {
115
- return tail.reduce(makeCall, head);
145
+ = head:uriExpression tail:arguments* {
146
+ return tail.reduce(
147
+ (target, args) => makeCall(target, args, location()),
148
+ head
149
+ );
116
150
  }
117
151
 
118
152
  // A comma-separated list of expressions: `x, y, z`
@@ -131,7 +165,7 @@ comment "comment"
131
165
 
132
166
  computedPropertyAccess
133
167
  = __ "[" expression:expression expectClosingBracket {
134
- return annotate([ops.traverse, expression], location());
168
+ return annotate([markers.property, expression], location());
135
169
  }
136
170
 
137
171
  conditionalExpression
@@ -145,9 +179,9 @@ conditionalExpression
145
179
  const deferred = makeDeferredArguments(tail);
146
180
  return annotate([
147
181
  ops.conditional,
148
- downgradeReference(condition),
149
- downgradeReference(deferred[0]),
150
- downgradeReference(deferred[1])
182
+ condition,
183
+ deferred[0],
184
+ deferred[1]
151
185
  ], location());
152
186
  }
153
187
 
@@ -249,7 +283,7 @@ expectPipelineExpression
249
283
  }
250
284
 
251
285
  exponentiationExpression
252
- = left:unaryExpression right:(__ "**" __ @exponentiationExpression)? {
286
+ = left:unaryExpression right:(whitespace "**" whitespace @exponentiationExpression)? {
253
287
  return right ? annotate([ops.exponentiation, left, right], location()) : left;
254
288
  }
255
289
 
@@ -286,7 +320,7 @@ frontMatterYaml "YAML front matter"
286
320
  // An expression in parentheses: `(foo)`
287
321
  group "parenthetical group"
288
322
  = "(" expression:expression expectClosingParenthesis {
289
- return annotate(downgradeReference(expression), location());
323
+ return annotate(expression, location());
290
324
  }
291
325
 
292
326
  guillemetString "guillemet string"
@@ -297,50 +331,56 @@ guillemetString "guillemet string"
297
331
  guillemetStringChar
298
332
  = !('»' / newLine) @textChar
299
333
 
300
- // The user's home directory: `~`
301
- homeDirectory
302
- = "~" {
303
- return annotate([ops.homeDirectory], location());
304
- }
305
-
306
334
  // A host identifier that may include a colon and port number: `example.com:80`.
307
335
  // This is used as a special case at the head of a path, where we want to
308
336
  // interpret a colon as part of a text identifier.
309
337
  host "HTTP/HTTPS host"
310
- = identifier:identifier port:(":" @integerLiteral)? slashFollows:slashFollows? {
311
- const portText = port ? `:${port[1]}` : "";
312
- const slashText = slashFollows ? "/" : "";
313
- const hostText = identifier + portText + slashText;
314
- 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();
315
348
  }
316
349
 
317
- identifier "identifier"
318
- = chars:identifierChar+ { return chars.join(""); }
350
+ // JavaScript-compatible identifier
351
+ identifier
352
+ = id:$( identifierStart identifierPart* ) {
353
+ return id;
354
+ }
319
355
 
320
- identifierChar
321
- = [^(){}\[\]<>\?!\|\-=,/:\`"'«»\\→⇒… \t\n\r] // No unescaped whitespace or special chars
322
- / @'-' !'>' // Accept a hyphen but not in a single arrow combination
323
- / 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) }
324
371
 
325
372
  implicitParenthesesCallExpression "function call with implicit parentheses"
326
373
  = head:arrowFunction args:(inlineSpace+ @implicitParensthesesArguments)? {
327
- return args ? makeCall(head, args) : head;
374
+ return args ? makeCall(head, args, location()) : head;
328
375
  }
329
376
 
330
377
  // A separated list of values for an implicit parens call. This differs from
331
378
  // `list` in that the value term can't be a pipeline.
332
379
  implicitParensthesesArguments
333
- = values:shorthandFunction|1.., separator| separator? {
380
+ = shellMode values:shorthandFunction|1.., separator| separator? {
334
381
  return annotate(values, location());
335
382
  }
336
383
 
337
- inherited
338
- = rootDirectory
339
- / homeDirectory
340
- / qualifiedReference
341
- / namespace
342
- / scopeReference
343
-
344
384
  inlineSpace
345
385
  = [ \t]
346
386
 
@@ -349,24 +389,32 @@ integerLiteral "integer"
349
389
  return annotate([ops.literal, parseInt(text())], location());
350
390
  }
351
391
 
352
- jsIdentifier
353
- = $( jsIdentifierStart jsIdentifierPart* )
354
-
355
- // Continuation of a JavaScript identifier
356
- // https://tc39.es/ecma262/multipage/ecmascript-language-lexical-grammar.html#prod-IdentifierPart
357
- jsIdentifierPart "JavaScript identifier continuation"
358
- = char:. &{ return char.match(/[$_\p{ID_Continue}]/u) }
392
+ // A key in a path or an expression that looks like one
393
+ key
394
+ = keyCharStart keyChar* {
395
+ return text();
396
+ }
359
397
 
360
- // Start of a JavaScript identifier
361
- // https://tc39.es/ecma262/multipage/ecmascript-language-lexical-grammar.html#prod-IdentifierStart
362
- jsIdentifierStart "JavaScript identifier start"
363
- = char:. &{ return char.match(/[$_\p{ID_Start}]/u) }
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
+ / "^"
364
410
 
365
- jsPropertyAccess
366
- = __ "." __ property:jsIdentifier {
367
- const literal = annotate([ops.literal, property], location());
368
- return annotate([ops.traverse, literal], location());
369
- }
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
+ / "@"
370
418
 
371
419
  // A separated list of values
372
420
  list "list"
@@ -374,16 +422,12 @@ list "list"
374
422
  return annotate(values, location());
375
423
  }
376
424
 
377
- literal
378
- = numericLiteral
379
- / stringLiteral
380
-
381
425
  logicalAndExpression
382
426
  = head:bitwiseOrExpression tail:(__ "&&" __ @bitwiseOrExpression)* {
383
427
  return tail.length === 0
384
428
  ? head
385
429
  : annotate(
386
- [ops.logicalAnd, downgradeReference(head), ...makeDeferredArguments(tail)],
430
+ [ops.logicalAnd, head, ...makeDeferredArguments(tail)],
387
431
  location()
388
432
  );
389
433
  }
@@ -393,11 +437,17 @@ logicalOrExpression
393
437
  return tail.length === 0
394
438
  ? head
395
439
  : annotate(
396
- [ops.logicalOr, downgradeReference(head), ...makeDeferredArguments(tail)],
440
+ [ops.logicalOr, head, ...makeDeferredArguments(tail)],
397
441
  location()
398
442
  );
399
443
  }
400
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
+
401
451
  multiLineComment
402
452
  = "/*" (!"*/" .)* "*/" { return null; }
403
453
 
@@ -411,11 +461,16 @@ multiplicativeOperator
411
461
  / "/"
412
462
  / "%"
413
463
 
414
- // A namespace reference is a string of letters only, followed by a colon.
415
- namespace
416
- = chars:[A-Za-z]+ ":" {
417
- return annotate([ops.builtin, chars.join("") + ":"], location());
418
- }
464
+ // A new expression: `new Foo()`
465
+ newExpression
466
+ = "new" __ head:pathLiteral tail:parenthesesArguments? {
467
+ const args = tail?.[0] !== undefined ? tail : [];
468
+ return annotate([ops.construct, head, ...args], location());
469
+ }
470
+ / "new:" head:pathLiteral tail:parenthesesArguments {
471
+ const args = tail?.[0] !== undefined ? tail : [];
472
+ return annotate([ops.construct, head, ...args], location());
473
+ }
419
474
 
420
475
  newLine
421
476
  = "\n"
@@ -432,7 +487,7 @@ nullishCoalescingExpression
432
487
  return tail.length === 0
433
488
  ? head
434
489
  : annotate(
435
- [ops.nullishCoalescing, downgradeReference(head), ...makeDeferredArguments(tail)],
490
+ [ops.nullishCoalescing, head, ...makeDeferredArguments(tail)],
436
491
  location()
437
492
  );
438
493
  }
@@ -458,10 +513,8 @@ objectEntry
458
513
  // A getter definition inside an object literal: `foo = 1`
459
514
  objectGetter "object getter"
460
515
  = key:objectKey __ "=" __ pipeline:expectPipelineExpression {
461
- return annotate(
462
- makeProperty(key, annotate([ops.getter, pipeline], location())),
463
- location()
464
- );
516
+ const getter = annotate([ops.getter, pipeline], location());
517
+ return annotate([key, getter], location());
465
518
  }
466
519
 
467
520
  objectHiddenKey
@@ -474,28 +527,42 @@ objectKey "object key"
474
527
  // A property definition in an object literal: `x: 1`
475
528
  objectProperty "object property"
476
529
  = key:objectKey __ ":" __ pipeline:expectPipelineExpression {
477
- return annotate(makeProperty(key, pipeline), location());
530
+ return annotate([key, pipeline], location());
478
531
  }
479
532
 
480
533
  // A shorthand reference inside an object literal: `foo`
481
534
  objectShorthandProperty "object identifier"
482
535
  = key:objectPublicKey {
483
- const inherited = annotate([ops.inherited, key], location());
484
- return annotate([key, inherited], location());
536
+ const reference = annotate([markers.reference, key], location());
537
+ const traverse = annotate([markers.traverse, reference], location());
538
+ return annotate([key, traverse], location());
539
+ }
540
+ / path:angleBracketLiteral {
541
+ let lastKey = path.at(-1);
542
+ if (lastKey instanceof Array) {
543
+ lastKey = lastKey[1]; // get scope identifier or literal
485
544
  }
545
+ return annotate([lastKey, path], location());
546
+ }
486
547
 
487
548
  objectPublicKey
488
- = identifier:identifier slash:"/"? {
489
- return identifier + (slash ?? "");
549
+ = key:key slash:"/"? {
550
+ return text();
490
551
  }
491
552
  / string:stringLiteral {
492
- // Remove `ops.literal` from the string code
493
- return string[1];
553
+ // Remove `ops.literal` from the string code
554
+ return string[1];
555
+ }
556
+
557
+ optionalChaining
558
+ = __ "?." __ property:identifier {
559
+ return annotate([ops.optionalTraverse, property], location());
494
560
  }
495
561
 
562
+ // Name of a unction parameter
496
563
  parameter
497
- = identifier:identifier {
498
- return annotate([ops.literal, identifier], location());
564
+ = key:key {
565
+ return annotate([ops.literal, key], location());
499
566
  }
500
567
 
501
568
  parameterList
@@ -505,11 +572,8 @@ parameterList
505
572
 
506
573
  // A list with a single identifier
507
574
  parameterSingleton
508
- = identifier:identifier {
509
- return annotate(
510
- [annotate([ops.literal, identifier], location())],
511
- location()
512
- );
575
+ = param:parameter {
576
+ return annotate([param], location());
513
577
  }
514
578
 
515
579
  // Function arguments in parentheses
@@ -518,78 +582,88 @@ parenthesesArguments "function arguments in parentheses"
518
582
  return annotate(list ?? [undefined], location());
519
583
  }
520
584
 
521
- // A slash-separated path of keys: `a/b/c`
522
- path "slash-separated path"
523
- // Path with at least a tail
524
- = segments:pathSegment|1..| {
525
- // Drop empty segments that represent consecutive or final slashes
526
- segments = segments.filter(segment => segment);
527
- return annotate(segments, location());
528
- }
529
-
530
- // 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`
531
587
  pathArguments
532
- = path:path {
533
- return annotate([ops.traverse, ...path], location());
588
+ = "/" keys:pathKeys? {
589
+ const args = keys ?? [];
590
+ return annotate([markers.traverse, ...args], location());
534
591
  }
535
592
 
536
- // A single key in a slash-separated path: `/a`
537
- pathKey
538
- = chars:pathSegmentChar+ slashFollows:slashFollows? {
539
- // Append a trailing slash if one follows (but don't consume it)
540
- const key = chars.join("") + (slashFollows ? "/" : "");
541
- return annotate([ops.literal, key], location());
542
- }
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
+ }
543
602
 
603
+ // A path key with an optional trailing slash
544
604
  pathSegment
545
- = "/" @pathKey?
546
-
547
- // A single character in a slash-separated path segment
548
- pathSegmentChar
549
- // This is more permissive than an identifier. It allows some characters like
550
- // brackets or quotes that are not allowed in identifiers.
551
- = [^(){}\[\],:/\\ \t\n\r]
552
- / 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
+ }
553
612
 
554
613
  // A pipeline that starts with a value and optionally applies a series of
555
614
  // functions to it.
556
615
  pipelineExpression
557
616
  = head:shorthandFunction tail:(__ singleArrow __ @shorthandFunction)* {
558
617
  return annotate(
559
- tail.reduce(makePipeline, downgradeReference(head)),
618
+ tail.reduce((arg, fn) => makePipeline(arg, fn, location()), head),
560
619
  location()
561
620
  );
562
621
  }
563
622
 
564
623
  primary
565
- = literal
624
+ // The following start with distinct characters
625
+ = stringLiteral
566
626
  / arrayLiteral
567
627
  / objectLiteral
568
628
  / group
629
+ / angleBracketLiteral
630
+ / regexLiteral
569
631
  / templateLiteral
570
- / inherited
632
+
633
+ // These are more ambiguous
634
+ / @numericLiteral !keyChar // numbers + chars would be a key
635
+ / pathLiteral
571
636
 
572
637
  // Top-level Origami progam with possible shebang directive (which is ignored)
573
638
  program "Origami program"
574
639
  = shebang? @expression
575
640
 
576
- // Protocol with double-slash path: `https://example.com/index.html`
577
- protocolExpression
578
- = fn:namespace "//" host:(host / slash) path:path? {
579
- const keys = annotate([host, ...(path ?? [])], location());
580
- return makeCall(fn, keys);
641
+ propertyAccess
642
+ = __ "." __ property:identifierLiteral {
643
+ return annotate([markers.property, property], location());
644
+ }
645
+
646
+ regexFlags
647
+ = flags:[gimuy]* {
648
+ return flags.join("");
581
649
  }
582
- / primary
583
650
 
584
- // A namespace followed by a key: `foo:x`
585
- qualifiedReference
586
- = fn:namespace reference:scopeReference {
587
- const literal = annotate([ops.literal, reference[1]], reference.location);
588
- return makeCall(fn, [literal]);
651
+ regexLiteral
652
+ = "/" chars:regexLiteralChar* "/" flags:regexFlags? {
653
+ const regex = new RegExp(chars.join(""), flags);
654
+ return annotate([ops.literal, regex], location());
589
655
  }
590
656
 
657
+ regexLiteralChar
658
+ = [^/\n\r] // No unescaped slashes or newlines
659
+ / escapedChar
660
+
591
661
  relationalExpression
592
- = head:shiftExpression tail:(__ @relationalOperator __ @shiftExpression)* {
662
+ // We disallow a newline before the relational operator to support a newline
663
+ // as a separator in an object literal that has an object shorthand property
664
+ // with an angle bracket path. Otherwise the opening angle bracket would be
665
+ // interpreted as a relational operator.
666
+ = head:shiftExpression tail:(inlineSpace @relationalOperator __ @shiftExpression)* {
593
667
  return tail.reduce(makeBinaryOperation, head);
594
668
  }
595
669
 
@@ -599,29 +673,16 @@ relationalOperator
599
673
  / ">="
600
674
  / ">"
601
675
 
602
- // A top-level folder below the root: `/foo`
603
- // or the root folder itself: `/`
604
- rootDirectory
605
- = "/" key:pathKey {
606
- return annotate([ops.rootDirectory, key], location());
607
- }
608
- / "/" !"/" {
609
- return annotate([ops.rootDirectory], location());
610
- }
611
-
612
- scopeReference "scope reference"
613
- = identifier:identifier slashFollows:slashFollows? {
614
- const id = identifier + (slashFollows ? "/" : "");
615
- return annotate(makeReference(id), location());
616
- }
617
-
618
676
  separator
619
677
  = __ "," __
620
- / whitespaceWithNewLine
678
+ / @whitespaceWithNewLine
621
679
 
622
680
  shebang
623
681
  = "#!" [^\n\r]* { return null; }
624
682
 
683
+ shellMode
684
+ = &{ return options.mode === "shell" }
685
+
625
686
  shiftExpression
626
687
  = head:additiveExpression tail:(__ @shiftOperator __ @additiveExpression)* {
627
688
  return tail.reduce(makeBinaryOperation, head);
@@ -635,7 +696,7 @@ shiftOperator
635
696
  // A shorthand lambda expression: `=foo(_)`
636
697
  shorthandFunction "lambda function"
637
698
  // Avoid a following equal sign (for an equality)
638
- = "=" !"=" __ definition:implicitParenthesesCallExpression {
699
+ = shellMode "=" !"=" __ definition:implicitParenthesesCallExpression {
639
700
  const lambdaParameters = annotate(
640
701
  [annotate([ops.literal, "_"], location())],
641
702
  location()
@@ -665,6 +726,12 @@ slash
665
726
  return annotate([ops.literal, "/"], location());
666
727
  }
667
728
 
729
+ // One or more consecutive slashes
730
+ slashes
731
+ = "/"+ {
732
+ return annotate([ops.literal, "/"], location());
733
+ }
734
+
668
735
  // Check whether next character is a slash without consuming input
669
736
  slashFollows
670
737
  // This expression returned `undefined` if successful; we convert to `true`
@@ -673,27 +740,20 @@ slashFollows
673
740
  }
674
741
 
675
742
  spreadElement
676
- = ellipsis __ value:pipelineExpression {
743
+ = ellipsis __ value:expectPipelineExpression {
677
744
  return annotate([ops.spread, value], location());
678
745
  }
679
746
 
680
747
  stringLiteral "string"
681
748
  = doubleQuoteString
682
749
  / singleQuoteString
683
- / guillemetString
750
+ / shellMode @guillemetString
684
751
 
685
752
  // The body of a template document is a kind of template literal that can
686
753
  // contain backticks at the top level.
687
754
  templateBody "template"
688
755
  = head:templateBodyText tail:(templateSubstitution templateBodyText)* {
689
- const lambdaParameters = annotate(
690
- [annotate([ops.literal, "_"], location())],
691
- location()
692
- );
693
- return annotate(
694
- [ops.lambda, lambdaParameters, makeTemplate(ops.templateIndent, head, tail, location())],
695
- location()
696
- );
756
+ return makeTemplate(ops.templateIndent, head, tail, location());
697
757
  }
698
758
 
699
759
  // Template document bodies can contain backticks at the top level
@@ -707,18 +767,27 @@ templateBodyText "template text"
707
767
 
708
768
  templateDocument "template document"
709
769
  = front:frontMatterExpression __ body:templateBody {
710
- return annotate(applyMacro(front, "@template", body), location());
770
+ const macroName = text().includes("@template") ? "@template" : "_template";
771
+ return annotate(applyMacro(front, macroName, body), location());
711
772
  }
712
- / front:frontMatterYaml? body:templateBody {
713
- return front
714
- ? annotate([ops.document, front, body], location())
715
- : annotate(body, location());
773
+ / front:frontMatterYaml body:templateBody {
774
+ return makeDocument(front, body, location());
775
+ }
776
+ / body:templateBody {
777
+ if (options.front) {
778
+ return makeDocument(options.front, body, location());
779
+ }
780
+ const lambdaParameters = annotate(
781
+ [annotate([ops.literal, "_"], location())],
782
+ location()
783
+ );
784
+ return annotate([ops.lambda, lambdaParameters, body], location());
716
785
  }
717
786
 
718
787
  // A backtick-quoted template literal
719
788
  templateLiteral "template literal"
720
789
  = "`" head:templateLiteralText tail:(templateSubstitution templateLiteralText)* expectBacktick {
721
- return makeTemplate(ops.template, head, tail, location());
790
+ return makeTemplate(ops.templateTree, head, tail, location());
722
791
  }
723
792
 
724
793
  templateLiteralChar
@@ -742,16 +811,64 @@ textChar
742
811
 
743
812
  // A unary prefix operator: `!x`
744
813
  unaryExpression
745
- = operator:unaryOperator __ expression:unaryExpression {
814
+ = operator:unaryOperator __ expression:expectExpression {
746
815
  return makeUnaryOperation(operator, expression, location());
747
816
  }
748
817
  / callExpression
749
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
+
750
867
  unaryOperator
751
868
  = "!"
752
869
  / "+"
753
- / "-"
754
- / "~"
870
+ / @"~" ![\/\)\]\}] // don't match `~/` or end of term
871
+ / minus
755
872
 
756
873
  whitespace
757
874
  = inlineSpace