@skeptools/skep-organization-project 0.0.17 → 0.0.19

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.
Files changed (61) hide show
  1. package/.jsii +22 -2139
  2. package/API.md +533 -418
  3. package/lib/skep-organization-project.js +36 -5
  4. package/node_modules/liquidjs/README.md +110 -67
  5. package/node_modules/liquidjs/dist/build/base64-impl-browser.d.ts +2 -0
  6. package/node_modules/liquidjs/dist/build/base64-impl-browser.spec.d.ts +1 -0
  7. package/node_modules/liquidjs/dist/build/crypto-impl-browser.d.ts +2 -0
  8. package/node_modules/liquidjs/dist/build/crypto-impl-browser.spec.d.ts +1 -0
  9. package/node_modules/liquidjs/dist/context/context.d.ts +10 -4
  10. package/node_modules/liquidjs/dist/context/scope.d.ts +2 -2
  11. package/node_modules/liquidjs/dist/drop/blank-drop.d.ts +1 -1
  12. package/node_modules/liquidjs/dist/drop/block-drop.d.ts +3 -2
  13. package/node_modules/liquidjs/dist/drop/comparable.d.ts +11 -5
  14. package/node_modules/liquidjs/dist/drop/drop.d.ts +3 -1
  15. package/node_modules/liquidjs/dist/drop/empty-drop.d.ts +1 -1
  16. package/node_modules/liquidjs/dist/drop/null-drop.d.ts +1 -1
  17. package/node_modules/liquidjs/dist/filters/array.d.ts +9 -3
  18. package/node_modules/liquidjs/dist/filters/base64-impl.d.ts +2 -0
  19. package/node_modules/liquidjs/dist/filters/base64.d.ts +9 -0
  20. package/node_modules/liquidjs/dist/filters/crypto-impl.d.ts +2 -0
  21. package/node_modules/liquidjs/dist/filters/crypto.d.ts +8 -0
  22. package/node_modules/liquidjs/dist/filters/math.d.ts +1 -1
  23. package/node_modules/liquidjs/dist/filters/misc.d.ts +2 -2
  24. package/node_modules/liquidjs/dist/filters/string.d.ts +2 -1
  25. package/node_modules/liquidjs/dist/fs/fs-impl.d.ts +2 -1
  26. package/node_modules/liquidjs/dist/fs/fs.d.ts +4 -2
  27. package/node_modules/liquidjs/dist/fs/loader.d.ts +2 -1
  28. package/node_modules/liquidjs/dist/index.d.ts +8 -3
  29. package/node_modules/liquidjs/dist/liquid-options.d.ts +8 -2
  30. package/node_modules/liquidjs/dist/liquid.browser.min.js +1 -1
  31. package/node_modules/liquidjs/dist/liquid.browser.min.js.map +1 -1
  32. package/node_modules/liquidjs/dist/liquid.browser.mjs +836 -406
  33. package/node_modules/liquidjs/dist/liquid.browser.umd.js +1683 -788
  34. package/node_modules/liquidjs/dist/liquid.browser.umd.js.map +1 -1
  35. package/node_modules/liquidjs/dist/liquid.d.ts +1 -0
  36. package/node_modules/liquidjs/dist/liquid.node.js +848 -426
  37. package/node_modules/liquidjs/dist/liquid.node.mjs +849 -422
  38. package/node_modules/liquidjs/dist/parser/parser.d.ts +1 -0
  39. package/node_modules/liquidjs/dist/parser/token-kind.d.ts +1 -0
  40. package/node_modules/liquidjs/dist/parser/tokenizer.d.ts +4 -3
  41. package/node_modules/liquidjs/dist/render/expression.d.ts +11 -1
  42. package/node_modules/liquidjs/dist/render/operator.d.ts +11 -4
  43. package/node_modules/liquidjs/dist/tags/case.d.ts +2 -2
  44. package/node_modules/liquidjs/dist/tags/for.d.ts +2 -2
  45. package/node_modules/liquidjs/dist/tags/include.d.ts +2 -0
  46. package/node_modules/liquidjs/dist/tags/layout.d.ts +1 -0
  47. package/node_modules/liquidjs/dist/tags/render.d.ts +2 -0
  48. package/node_modules/liquidjs/dist/tags/tablerow.d.ts +2 -2
  49. package/node_modules/liquidjs/dist/tokens/literal-token.d.ts +2 -2
  50. package/node_modules/liquidjs/dist/tokens/operator-token.d.ts +3 -2
  51. package/node_modules/liquidjs/dist/tokens/property-access-token.d.ts +3 -2
  52. package/node_modules/liquidjs/dist/tokens/tag-token.d.ts +1 -1
  53. package/node_modules/liquidjs/dist/tokens/value-token.d.ts +2 -1
  54. package/node_modules/liquidjs/dist/util/async.d.ts +2 -0
  55. package/node_modules/liquidjs/dist/util/liquid-date.d.ts +7 -0
  56. package/node_modules/liquidjs/dist/util/operator-trie.d.ts +4 -8
  57. package/node_modules/liquidjs/dist/util/strftime.d.ts +2 -1
  58. package/node_modules/liquidjs/dist/util/type-guards.d.ts +2 -1
  59. package/node_modules/liquidjs/dist/util/underscore.d.ts +5 -1
  60. package/node_modules/liquidjs/package.json +20 -14
  61. package/package.json +47 -41
@@ -1,6 +1,6 @@
1
1
  /*
2
- * liquidjs@10.20.1, https://github.com/harttle/liquidjs
3
- * (c) 2016-2025 harttle
2
+ * liquidjs@10.30.1, https://github.com/harttle/liquidjs
3
+ * (c) 2016-2026 harttle
4
4
  * Released under the MIT License.
5
5
  */
6
6
  'use strict';
@@ -10,6 +10,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
10
10
  var stream = require('stream');
11
11
  var path = require('path');
12
12
  var fs$1 = require('fs');
13
+ var crypto = require('crypto');
13
14
 
14
15
  class Token {
15
16
  constructor(kind, input, begin, end, file) {
@@ -40,7 +41,7 @@ class Token {
40
41
  }
41
42
 
42
43
  class Drop {
43
- liquidMethodMissing(key) {
44
+ liquidMethodMissing(key, context) {
44
45
  return undefined;
45
46
  }
46
47
  }
@@ -61,9 +62,6 @@ function isPromise(val) {
61
62
  function isIterator(val) {
62
63
  return val && isFunction(val.next) && isFunction(val.throw) && isFunction(val.return);
63
64
  }
64
- function escapeRegex(str) {
65
- return str.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
66
- }
67
65
  function promisify(fn) {
68
66
  return function (...args) {
69
67
  return new Promise((resolve, reject) => {
@@ -83,6 +81,25 @@ function stringify(value) {
83
81
  return value.map(x => stringify(x)).join('');
84
82
  return String(value);
85
83
  }
84
+ function advanceCodePoints(str, from, count) {
85
+ let index = from;
86
+ for (let i = 0; i < count && index < str.length; i++)
87
+ index += str.codePointAt(index) > 0xFFFF ? 2 : 1;
88
+ return index;
89
+ }
90
+ function codePointLength(str) {
91
+ let length = 0;
92
+ for (let index = 0; index < str.length; index = advanceCodePoints(str, index, 1))
93
+ length++;
94
+ return length;
95
+ }
96
+ function readArrayElement(arr, index, ownPropertyOnly) {
97
+ if (index < 0)
98
+ index = arr.length + index;
99
+ if (ownPropertyOnly && !hasOwnProperty.call(arr, index))
100
+ return undefined;
101
+ return arr[index];
102
+ }
86
103
  function toEnumerable(val) {
87
104
  val = toValue(val);
88
105
  if (isArray(val))
@@ -107,8 +124,7 @@ function toValue(value) {
107
124
  return (value instanceof Drop && isFunction(value.valueOf)) ? value.valueOf() : value;
108
125
  }
109
126
  function toNumber(value) {
110
- value = Number(value);
111
- return isNaN(value) ? 0 : value;
127
+ return +toValue(value) || 0;
112
128
  }
113
129
  function isNumber(value) {
114
130
  return typeof value === 'number';
@@ -152,9 +168,6 @@ function forOwn(obj, iteratee) {
152
168
  }
153
169
  return obj;
154
170
  }
155
- function last(arr) {
156
- return arr[arr.length - 1];
157
- }
158
171
  /*
159
172
  * Checks if value is the language type of Object.
160
173
  * (e.g. arrays, functions, objects, regexes, new Number(0), and new String(''))
@@ -180,10 +193,10 @@ function padEnd(str, length, ch = ' ') {
180
193
  }
181
194
  function pad(str, length, ch, add) {
182
195
  str = String(str);
183
- let n = length - str.length;
184
- while (n-- > 0)
185
- str = add(str, ch);
186
- return str;
196
+ const n = length - str.length;
197
+ if (n <= 0)
198
+ return str;
199
+ return add(str, ch.repeat(n));
187
200
  }
188
201
  function identify(val) {
189
202
  return val;
@@ -195,13 +208,26 @@ function changeCase(str) {
195
208
  function ellipsis(str, N) {
196
209
  return str.length > N ? str.slice(0, N - 3) + '...' : str;
197
210
  }
211
+ function orderedCompare(a, b) {
212
+ if (isNil(a) && isNil(b))
213
+ return 0;
214
+ if (isNil(a))
215
+ return 1;
216
+ if (isNil(b))
217
+ return -1;
218
+ if (a < b)
219
+ return -1;
220
+ if (a > b)
221
+ return 1;
222
+ return 0;
223
+ }
198
224
  // compare string in case-insensitive way, undefined values to the tail
199
225
  function caseInsensitiveCompare(a, b) {
200
- if (a == null && b == null)
226
+ if (isNil(a) && isNil(b))
201
227
  return 0;
202
- if (a == null)
228
+ if (isNil(a))
203
229
  return 1;
204
- if (b == null)
230
+ if (isNil(b))
205
231
  return -1;
206
232
  a = toLowerCase.call(a);
207
233
  b = toLowerCase.call(b);
@@ -214,8 +240,8 @@ function caseInsensitiveCompare(a, b) {
214
240
  function argumentsToValue(fn) {
215
241
  return function (...args) { return fn.call(this, ...args.map(toValue)); };
216
242
  }
217
- function escapeRegExp(text) {
218
- return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
243
+ function argumentsToNumber(fn) {
244
+ return function (...args) { return fn.call(this, ...args.map(toNumber)); };
219
245
  }
220
246
  /** Return an array containing unique elements from _array_. Works with nested arrays and objects. */
221
247
  function* strictUniq(array) {
@@ -381,8 +407,8 @@ function assertEmpty(predicate, message = `unexpected ${JSON.stringify(predicate
381
407
  }
382
408
 
383
409
  class NullDrop extends Drop {
384
- equals(value) {
385
- return isNil(toValue(value));
410
+ *equals(value) {
411
+ return isNil(yield toValue(value));
386
412
  }
387
413
  gt() {
388
414
  return false;
@@ -402,10 +428,10 @@ class NullDrop extends Drop {
402
428
  }
403
429
 
404
430
  class EmptyDrop extends Drop {
405
- equals(value) {
431
+ *equals(value) {
406
432
  if (value instanceof EmptyDrop)
407
433
  return false;
408
- value = toValue(value);
434
+ value = yield toValue(value);
409
435
  if (isString(value) || isArray(value))
410
436
  return value.length === 0;
411
437
  if (isObject(value))
@@ -433,14 +459,14 @@ class EmptyDrop extends Drop {
433
459
  }
434
460
 
435
461
  class BlankDrop extends EmptyDrop {
436
- equals(value) {
462
+ *equals(value) {
437
463
  if (value === false)
438
464
  return true;
439
- if (isNil(toValue(value)))
465
+ if (isNil(yield toValue(value)))
440
466
  return true;
441
467
  if (isString(value))
442
468
  return /^\s*$/.test(value);
443
- return super.equals(value);
469
+ return yield super.equals(value);
444
470
  }
445
471
  static is(value) {
446
472
  return value instanceof BlankDrop;
@@ -480,6 +506,50 @@ class ForloopDrop extends Drop {
480
506
  }
481
507
  }
482
508
 
509
+ class SimpleEmitter {
510
+ constructor() {
511
+ this.buffer = '';
512
+ }
513
+ write(html) {
514
+ this.buffer += stringify(html);
515
+ }
516
+ }
517
+
518
+ class StreamedEmitter {
519
+ constructor() {
520
+ this.buffer = '';
521
+ this.stream = new stream.PassThrough();
522
+ }
523
+ write(html) {
524
+ this.stream.write(stringify(html));
525
+ }
526
+ error(err) {
527
+ this.stream.emit('error', err);
528
+ }
529
+ end() {
530
+ this.stream.end();
531
+ }
532
+ }
533
+
534
+ class KeepingTypeEmitter {
535
+ constructor() {
536
+ this.buffer = '';
537
+ }
538
+ write(html) {
539
+ html = toValue(html);
540
+ // This will only preserve the type if the value is isolated.
541
+ // I.E:
542
+ // {{ my-port }} -> 42
543
+ // {{ my-host }}:{{ my-port }} -> 'host:42'
544
+ if (typeof html !== 'string' && this.buffer === '') {
545
+ this.buffer = html;
546
+ }
547
+ else {
548
+ this.buffer = stringify(this.buffer) + stringify(html);
549
+ }
550
+ }
551
+ }
552
+
483
553
  class BlockDrop extends Drop {
484
554
  constructor(
485
555
  // the block render from layout template
@@ -491,8 +561,10 @@ class BlockDrop extends Drop {
491
561
  * Provide parent access in child block by
492
562
  * {{ block.super }}
493
563
  */
494
- super() {
495
- return this.superBlockRender();
564
+ *super() {
565
+ const emitter = new SimpleEmitter();
566
+ yield this.superBlockRender(emitter);
567
+ return emitter.buffer;
496
568
  }
497
569
  }
498
570
 
@@ -515,7 +587,16 @@ const literalValues = {
515
587
  'blank': new BlankDrop()
516
588
  };
517
589
 
590
+ // Tries are built once per input object and reused: the Tokenizer rebuilds them
591
+ // on every instantiation, but `input` (operators/literalValues) is a stable
592
+ // reference. WeakMap-keying by `input` lets short-lived operator objects (and
593
+ // their tries) be garbage collected. The returned trie is treated as read-only
594
+ // by callers (matchTrie only reads it); do not mutate it.
595
+ const trieCache = new WeakMap();
518
596
  function createTrie(input) {
597
+ const cached = trieCache.get(input);
598
+ if (cached)
599
+ return cached;
519
600
  const trie = {};
520
601
  for (const [name, data] of Object.entries(input)) {
521
602
  let node = trie;
@@ -530,9 +611,16 @@ function createTrie(input) {
530
611
  node.data = data;
531
612
  node.end = true;
532
613
  }
614
+ trieCache.set(input, trie);
533
615
  return trie;
534
616
  }
535
617
 
618
+ function toLiquidAsync(asyncFn, syncFn) {
619
+ const syncImpl = syncFn || asyncFn;
620
+ return (sync, ...args) => {
621
+ return sync ? syncImpl(...args) : asyncFn(...args);
622
+ };
623
+ }
536
624
  // convert an async iterator to a Promise
537
625
  async function toPromise(val) {
538
626
  if (!isIterator(val))
@@ -542,7 +630,7 @@ async function toPromise(val) {
542
630
  let next = 'next';
543
631
  do {
544
632
  const state = val[next](value);
545
- done = state.done;
633
+ done = !!state.done;
546
634
  value = state.value;
547
635
  next = 'next';
548
636
  try {
@@ -567,7 +655,7 @@ function toValueSync(val) {
567
655
  let next = 'next';
568
656
  do {
569
657
  const state = val[next](value);
570
- done = state.done;
658
+ done = !!state.done;
571
659
  value = state.value;
572
660
  next = 'next';
573
661
  if (isIterator(value)) {
@@ -597,12 +685,9 @@ function getDayOfYear(d) {
597
685
  return num + d.getDate();
598
686
  }
599
687
  function getWeekOfYear(d, startDay) {
600
- // Skip to startDay of this week
601
- const now = getDayOfYear(d) + (startDay - d.getDay());
602
- // Find the first startDay of the year
603
- const jan1 = new Date(d.getFullYear(), 0, 1);
604
- const then = (7 - jan1.getDay() + startDay);
605
- return String(Math.floor((now - then) / 7) + 1);
688
+ // Days before the first startDay of the year belong to week 0
689
+ const daysSinceStartDay = (d.getDay() - startDay + 7) % 7;
690
+ return String(Math.floor((getDayOfYear(d) - 1 - daysSinceStartDay + 7) / 7));
606
691
  }
607
692
  function isLeapYear(d) {
608
693
  const year = d.getFullYear();
@@ -620,10 +705,11 @@ function ordinal(d) {
620
705
  }
621
706
  }
622
707
  function century(d) {
623
- return parseInt(d.getFullYear().toString().substring(0, 2), 10);
708
+ return Math.floor(d.getFullYear() / 100);
624
709
  }
625
710
  // default to 0
626
711
  const padWidths = {
712
+ C: 2,
627
713
  d: 2,
628
714
  e: 2,
629
715
  H: 2,
@@ -636,7 +722,9 @@ const padWidths = {
636
722
  M: 2,
637
723
  S: 2,
638
724
  U: 2,
639
- W: 2
725
+ W: 2,
726
+ y: 2,
727
+ Y: 4
640
728
  };
641
729
  const padSpaceChars = new Set('aAbBceklpP');
642
730
  function getTimezoneOffset(d, opts) {
@@ -667,13 +755,14 @@ const formatCodes = {
667
755
  M: (d) => d.getMinutes(),
668
756
  N: (d, opts) => {
669
757
  const width = Number(opts.width) || 9;
670
- const str = String(d.getMilliseconds()).slice(0, width);
758
+ const str = padStart(String(d.getMilliseconds()), 3, '0').slice(0, width);
759
+ opts.memoryLimit?.use(width - str.length);
671
760
  return padEnd(str, width, '0');
672
761
  },
673
762
  p: (d) => (d.getHours() < 12 ? 'AM' : 'PM'),
674
763
  P: (d) => (d.getHours() < 12 ? 'am' : 'pm'),
675
764
  q: (d) => ordinal(d),
676
- s: (d) => Math.round(d.getTime() / 1000),
765
+ s: (d) => Math.floor(d.dateValue() / 1000),
677
766
  S: (d) => d.getSeconds(),
678
767
  u: (d) => d.getDay() || 7,
679
768
  U: (d) => getWeekOfYear(d, 0),
@@ -681,7 +770,7 @@ const formatCodes = {
681
770
  W: (d) => getWeekOfYear(d, 1),
682
771
  x: (d) => d.toLocaleDateString(),
683
772
  X: (d) => d.toLocaleTimeString(),
684
- y: (d) => d.getFullYear().toString().slice(2, 4),
773
+ y: (d) => (d.getFullYear() % 100 + 100) % 100,
685
774
  Y: (d) => d.getFullYear(),
686
775
  z: getTimezoneOffset,
687
776
  Z: (d, opts) => d.getTimeZoneName() || getTimezoneOffset(d, opts),
@@ -690,18 +779,18 @@ const formatCodes = {
690
779
  '%': () => '%'
691
780
  };
692
781
  formatCodes.h = formatCodes.b;
693
- function strftime(d, formatStr) {
782
+ function strftime(d, formatStr, memoryLimit) {
694
783
  let output = '';
695
784
  let remaining = formatStr;
696
785
  let match;
697
786
  while ((match = rFormat.exec(remaining))) {
698
787
  output += remaining.slice(0, match.index);
699
788
  remaining = remaining.slice(match.index + match[0].length);
700
- output += format(d, match);
789
+ output += format(d, match, memoryLimit);
701
790
  }
702
791
  return output + remaining;
703
792
  }
704
- function format(d, match) {
793
+ function format(d, match, memoryLimit) {
705
794
  const [input, flagStr = '', width, modifier, conversion] = match;
706
795
  const convert = formatCodes[conversion];
707
796
  if (!convert)
@@ -709,9 +798,9 @@ function format(d, match) {
709
798
  const flags = {};
710
799
  for (const flag of flagStr)
711
800
  flags[flag] = true;
712
- let ret = String(convert(d, { flags, width, modifier }));
801
+ let ret = String(convert(d, { flags, width, modifier, memoryLimit }));
713
802
  let padChar = padSpaceChars.has(conversion) ? ' ' : '0';
714
- let padWidth = width || padWidths[conversion] || 0;
803
+ let padWidth = Number(width) || padWidths[conversion] || 0;
715
804
  if (flags['^'])
716
805
  ret = ret.toUpperCase();
717
806
  else if (flags['#'])
@@ -722,6 +811,9 @@ function format(d, match) {
722
811
  padChar = '0';
723
812
  if (flags['-'])
724
813
  padWidth = 0;
814
+ memoryLimit?.use(Number(padWidth) - ret.length);
815
+ if (padChar === '0' && ret[0] === '-')
816
+ return '-' + padStart(ret.slice(1), padWidth - 1, padChar);
725
817
  return padStart(ret, padWidth, padChar);
726
818
  }
727
819
 
@@ -770,6 +862,15 @@ class LiquidDate {
770
862
  getTime() {
771
863
  return this.displayDate.getTime();
772
864
  }
865
+ /**
866
+ * The underlying UTC timestamp in milliseconds, unaffected by the display
867
+ * timezone. Use this (not `getTime()`) for timezone-invariant values like
868
+ * `%s`: `getTime()` reads `displayDate`, which is deliberately shifted by
869
+ * the display timezone offset so wall-clock getters can delegate to Date.
870
+ */
871
+ dateValue() {
872
+ return this.date.getTime();
873
+ }
773
874
  getMilliseconds() {
774
875
  return this.displayDate.getMilliseconds();
775
876
  }
@@ -877,13 +978,15 @@ class Limiter {
877
978
  this.limit = limit;
878
979
  }
879
980
  use(count) {
880
- count = toNumber(count);
881
- assert(this.base + count <= this.limit, this.message);
882
- this.base += count;
981
+ if (+count > 0) {
982
+ assert(this.base + +count <= this.limit, this.message);
983
+ this.base += +count;
984
+ }
883
985
  }
884
986
  check(count) {
885
- count = toNumber(count);
886
- assert(count <= this.limit, this.message);
987
+ if (+count > 0) {
988
+ assert(+count <= this.limit, this.message);
989
+ }
887
990
  }
888
991
  }
889
992
 
@@ -914,13 +1017,11 @@ class TagToken extends DelimitedToken {
914
1017
  const { trimTagLeft, trimTagRight, tagDelimiterLeft, tagDelimiterRight } = options;
915
1018
  const [valueBegin, valueEnd] = [begin + tagDelimiterLeft.length, end - tagDelimiterRight.length];
916
1019
  super(exports.TokenKind.Tag, [valueBegin, valueEnd], input, begin, end, trimTagLeft, trimTagRight, file);
917
- this.tokenizer = new Tokenizer(input, options.operators, file, this.contentRange);
1020
+ this.tokenizer = new Tokenizer(input, options.operators, file, this.contentRange, options.groupedExpressions);
918
1021
  this.name = this.tokenizer.readTagName();
919
1022
  this.tokenizer.assert(this.name, `illegal tag syntax, tag name expected`);
920
1023
  this.tokenizer.skipBlank();
921
- }
922
- get args() {
923
- return this.tokenizer.input.slice(this.tokenizer.p, this.contentRange[1]);
1024
+ this.args = this.tokenizer.input.slice(this.tokenizer.p, this.contentRange[1]);
924
1025
  }
925
1026
  }
926
1027
 
@@ -1015,8 +1116,7 @@ class OperatorToken extends Token {
1015
1116
  this.operator = this.getText();
1016
1117
  }
1017
1118
  getPrecedence() {
1018
- const key = this.getText();
1019
- return key in operatorPrecedences ? operatorPrecedences[key] : 1;
1119
+ return this.operator in operatorPrecedences ? operatorPrecedences[this.operator] : 1;
1020
1120
  }
1021
1121
  }
1022
1122
 
@@ -1130,7 +1230,7 @@ class RangeToken extends Token {
1130
1230
  class LiquidTagToken extends DelimitedToken {
1131
1231
  constructor(input, begin, end, options, file) {
1132
1232
  super(exports.TokenKind.Tag, [begin, end], input, begin, end, false, false, file);
1133
- this.tokenizer = new Tokenizer(input, options.operators, file, this.contentRange);
1233
+ this.tokenizer = new Tokenizer(input, options.operators, file, this.contentRange, options.groupedExpressions);
1134
1234
  this.name = this.tokenizer.readTagName();
1135
1235
  this.tokenizer.assert(this.name, 'illegal liquid tag syntax');
1136
1236
  this.tokenizer.skipBlank();
@@ -1166,50 +1266,6 @@ function getPerformance() {
1166
1266
  polyfill;
1167
1267
  }
1168
1268
 
1169
- class SimpleEmitter {
1170
- constructor() {
1171
- this.buffer = '';
1172
- }
1173
- write(html) {
1174
- this.buffer += stringify(html);
1175
- }
1176
- }
1177
-
1178
- class StreamedEmitter {
1179
- constructor() {
1180
- this.buffer = '';
1181
- this.stream = new stream.PassThrough();
1182
- }
1183
- write(html) {
1184
- this.stream.write(stringify(html));
1185
- }
1186
- error(err) {
1187
- this.stream.emit('error', err);
1188
- }
1189
- end() {
1190
- this.stream.end();
1191
- }
1192
- }
1193
-
1194
- class KeepingTypeEmitter {
1195
- constructor() {
1196
- this.buffer = '';
1197
- }
1198
- write(html) {
1199
- html = toValue(html);
1200
- // This will only preserve the type if the value is isolated.
1201
- // I.E:
1202
- // {{ my-port }} -> 42
1203
- // {{ my-host }}:{{ my-port }} -> 'host:42'
1204
- if (typeof html !== 'string' && this.buffer === '') {
1205
- this.buffer = html;
1206
- }
1207
- else {
1208
- this.buffer = stringify(this.buffer) + stringify(html);
1209
- }
1210
- }
1211
- }
1212
-
1213
1269
  class Render {
1214
1270
  renderTemplatesToNodeStream(templates, ctx) {
1215
1271
  const emitter = new StreamedEmitter();
@@ -1221,6 +1277,7 @@ class Render {
1221
1277
  if (!emitter) {
1222
1278
  emitter = ctx.opts.keepOutputType ? new KeepingTypeEmitter() : new SimpleEmitter();
1223
1279
  }
1280
+ ctx.renderLimit.check(getPerformance().now());
1224
1281
  const errors = [];
1225
1282
  for (const tpl of templates) {
1226
1283
  ctx.renderLimit.check(getPerformance().now());
@@ -1247,6 +1304,33 @@ class Render {
1247
1304
  }
1248
1305
  }
1249
1306
 
1307
+ function isKeyValuePair(arr) {
1308
+ return isArray(arr);
1309
+ }
1310
+
1311
+ class Filter {
1312
+ constructor(token, options, liquid) {
1313
+ this.token = token;
1314
+ this.name = token.name;
1315
+ this.handler = isFunction(options)
1316
+ ? options
1317
+ : (isFunction(options?.handler) ? options.handler : identify);
1318
+ this.raw = !isFunction(options) && !!options?.raw;
1319
+ this.args = token.args;
1320
+ this.liquid = liquid;
1321
+ }
1322
+ *render(value, context) {
1323
+ const argv = [];
1324
+ for (const arg of this.args) {
1325
+ if (isKeyValuePair(arg))
1326
+ argv.push([arg[0], yield evalToken(arg[1], context)]);
1327
+ else
1328
+ argv.push(yield evalToken(arg, context));
1329
+ }
1330
+ return yield this.handler.apply({ context, token: this.token, liquid: this.liquid }, [value, ...argv]);
1331
+ }
1332
+ }
1333
+
1250
1334
  class Expression {
1251
1335
  constructor(tokens) {
1252
1336
  this.postfix = [...toPostfix(tokens)];
@@ -1277,6 +1361,11 @@ class Expression {
1277
1361
  return !!this.postfix.length;
1278
1362
  }
1279
1363
  }
1364
+ /**
1365
+ * Evaluate `token`, preserving Drops.
1366
+ * Operators use this so `Comparable` drops (`empty`, `nil`, `blank`) and custom drops
1367
+ * are compared as drops rather than as their `valueOf()` result.
1368
+ */
1280
1369
  function* evalToken(token, ctx, lenient = false) {
1281
1370
  if (!token)
1282
1371
  return;
@@ -1286,11 +1375,32 @@ function* evalToken(token, ctx, lenient = false) {
1286
1375
  return yield evalPropertyAccessToken(token, ctx, lenient);
1287
1376
  if (isRangeToken(token))
1288
1377
  return yield evalRangeToken(token, ctx);
1378
+ if (isFilteredValueToken(token))
1379
+ return yield evalFilteredValueToken(token, ctx, lenient);
1380
+ }
1381
+ function* evalFilteredValueToken(token, ctx, lenient) {
1382
+ assert(ctx.liquid, 'FilteredValueToken evaluation requires liquid instance in context');
1383
+ lenient = lenient || (ctx.opts.lenientIf && token.filters.length > 0 && token.filters[0].name === 'default');
1384
+ let val = yield token.initial.evaluate(ctx, lenient);
1385
+ for (const filterToken of token.filters) {
1386
+ const filterImpl = ctx.liquid.filters[filterToken.name];
1387
+ assert(filterImpl || !ctx.liquid.options.strictFilters, () => `undefined filter: ${filterToken.name}`);
1388
+ const filter = new Filter(filterToken, filterImpl, ctx.liquid);
1389
+ val = yield filter.render(val, ctx);
1390
+ }
1391
+ return val;
1392
+ }
1393
+ /**
1394
+ * Evaluate `token` to the value filters and tags consume.
1395
+ * Awaits a promise returned by `Drop.valueOf()`.
1396
+ */
1397
+ function* evalTokenValue(token, ctx, lenient = false) {
1398
+ return yield toValue(yield evalToken(token, ctx, lenient));
1289
1399
  }
1290
1400
  function* evalPropertyAccessToken(token, ctx, lenient) {
1291
1401
  const props = [];
1292
1402
  for (const prop of token.props) {
1293
- props.push((yield evalToken(prop, ctx, false)));
1403
+ props.push((yield evalTokenValue(prop, ctx, false)));
1294
1404
  }
1295
1405
  try {
1296
1406
  if (token.variable) {
@@ -1311,8 +1421,8 @@ function evalQuotedToken(token) {
1311
1421
  return token.content;
1312
1422
  }
1313
1423
  function* evalRangeToken(token, ctx) {
1314
- const low = yield evalToken(token.lhs, ctx);
1315
- const high = yield evalToken(token.rhs, ctx);
1424
+ const low = (yield evalTokenValue(token.lhs, ctx));
1425
+ const high = (yield evalTokenValue(token.rhs, ctx));
1316
1426
  ctx.memoryLimit.use(high - low + 1);
1317
1427
  return range(+low, +high + 1);
1318
1428
  }
@@ -1347,67 +1457,75 @@ function isFalsy(val, ctx) {
1347
1457
  }
1348
1458
 
1349
1459
  const defaultOperators = {
1350
- '==': equals,
1351
- '!=': (l, r) => !equals(l, r),
1352
- '>': (l, r) => {
1460
+ '==': function* (l, r) { return yield equals(l, r); },
1461
+ '!=': function* (l, r) { return !(yield equals(l, r)); },
1462
+ '>': function* (l, r) {
1353
1463
  if (isComparable(l))
1354
- return l.gt(r);
1464
+ return yield l.gt(r);
1355
1465
  if (isComparable(r))
1356
- return r.lt(l);
1357
- return toValue(l) > toValue(r);
1466
+ return yield r.lt(l);
1467
+ return (yield toValue(l)) > (yield toValue(r));
1358
1468
  },
1359
- '<': (l, r) => {
1469
+ '<': function* (l, r) {
1360
1470
  if (isComparable(l))
1361
- return l.lt(r);
1471
+ return yield l.lt(r);
1362
1472
  if (isComparable(r))
1363
- return r.gt(l);
1364
- return toValue(l) < toValue(r);
1473
+ return yield r.gt(l);
1474
+ return (yield toValue(l)) < (yield toValue(r));
1365
1475
  },
1366
- '>=': (l, r) => {
1476
+ '>=': function* (l, r) {
1367
1477
  if (isComparable(l))
1368
- return l.geq(r);
1478
+ return yield l.geq(r);
1369
1479
  if (isComparable(r))
1370
- return r.leq(l);
1371
- return toValue(l) >= toValue(r);
1480
+ return yield r.leq(l);
1481
+ return (yield toValue(l)) >= (yield toValue(r));
1372
1482
  },
1373
- '<=': (l, r) => {
1483
+ '<=': function* (l, r) {
1374
1484
  if (isComparable(l))
1375
- return l.leq(r);
1485
+ return yield l.leq(r);
1376
1486
  if (isComparable(r))
1377
- return r.geq(l);
1378
- return toValue(l) <= toValue(r);
1487
+ return yield r.geq(l);
1488
+ return (yield toValue(l)) <= (yield toValue(r));
1379
1489
  },
1380
- 'contains': (l, r) => {
1381
- l = toValue(l);
1490
+ 'contains': function* (l, r) {
1491
+ l = yield toValue(l);
1382
1492
  if (isArray(l))
1383
- return l.some((i) => equals(i, r));
1493
+ return yield arrayIncludes(l, r);
1384
1494
  if (isFunction(l?.indexOf))
1385
- return l.indexOf(toValue(r)) > -1;
1495
+ return l.indexOf(yield toValue(r)) > -1;
1386
1496
  return false;
1387
1497
  },
1388
- 'not': (v, ctx) => isFalsy(toValue(v), ctx),
1389
- 'and': (l, r, ctx) => isTruthy(toValue(l), ctx) && isTruthy(toValue(r), ctx),
1390
- 'or': (l, r, ctx) => isTruthy(toValue(l), ctx) || isTruthy(toValue(r), ctx)
1498
+ 'not': function* (v, ctx) { return isFalsy(yield toValue(v), ctx); },
1499
+ 'and': function* (l, r, ctx) { return isTruthy(yield toValue(l), ctx) && isTruthy(yield toValue(r), ctx); },
1500
+ 'or': function* (l, r, ctx) { return isTruthy(yield toValue(l), ctx) || isTruthy(yield toValue(r), ctx); }
1391
1501
  };
1392
- function equals(lhs, rhs) {
1502
+ function* equals(lhs, rhs) {
1393
1503
  if (isComparable(lhs))
1394
- return lhs.equals(rhs);
1504
+ return yield lhs.equals(rhs);
1395
1505
  if (isComparable(rhs))
1396
- return rhs.equals(lhs);
1397
- lhs = toValue(lhs);
1398
- rhs = toValue(rhs);
1506
+ return yield rhs.equals(lhs);
1507
+ lhs = yield toValue(lhs);
1508
+ rhs = yield toValue(rhs);
1399
1509
  if (isArray(lhs)) {
1400
- return isArray(rhs) && arrayEquals(lhs, rhs);
1510
+ return isArray(rhs) && (yield arrayEquals(lhs, rhs));
1401
1511
  }
1402
1512
  return lhs === rhs;
1403
1513
  }
1404
- function arrayEquals(lhs, rhs) {
1514
+ function* arrayEquals(lhs, rhs) {
1405
1515
  if (lhs.length !== rhs.length)
1406
1516
  return false;
1407
- return !lhs.some((value, i) => !equals(value, rhs[i]));
1517
+ for (let i = 0; i < lhs.length; i++) {
1518
+ if (!(yield equals(lhs[i], rhs[i])))
1519
+ return false;
1520
+ }
1521
+ return true;
1408
1522
  }
1409
- function arrayIncludes(arr, item) {
1410
- return arr.some(value => equals(value, item));
1523
+ function* arrayIncludes(arr, item) {
1524
+ for (const value of arr) {
1525
+ if (yield equals(value, item))
1526
+ return true;
1527
+ }
1528
+ return false;
1411
1529
  }
1412
1530
 
1413
1531
  class Node {
@@ -1510,10 +1628,28 @@ function fallback(file) {
1510
1628
  function dirname(filepath) {
1511
1629
  return path.dirname(filepath);
1512
1630
  }
1513
- function contains(root, file) {
1514
- root = path.resolve(root);
1515
- root = root.endsWith(path.sep) ? root : root + path.sep;
1516
- return file.startsWith(root);
1631
+ const realpathAsync = promisify(fs$1.realpath);
1632
+ async function contains(root, file) {
1633
+ try {
1634
+ const realRoot = await realpathAsync(root);
1635
+ const realFile = await realpathAsync(file);
1636
+ const prefix = realRoot.endsWith(path.sep) ? realRoot : realRoot + path.sep;
1637
+ return realFile.startsWith(prefix);
1638
+ }
1639
+ catch {
1640
+ return false;
1641
+ }
1642
+ }
1643
+ function containsSync(root, file) {
1644
+ try {
1645
+ const realRoot = fs$1.realpathSync(root);
1646
+ const realFile = fs$1.realpathSync(file);
1647
+ const prefix = realRoot.endsWith(path.sep) ? realRoot : realRoot + path.sep;
1648
+ return realFile.startsWith(prefix);
1649
+ }
1650
+ catch {
1651
+ return false;
1652
+ }
1517
1653
  }
1518
1654
 
1519
1655
  var fs = /*#__PURE__*/Object.freeze({
@@ -1526,9 +1662,24 @@ var fs = /*#__PURE__*/Object.freeze({
1526
1662
  fallback: fallback,
1527
1663
  dirname: dirname,
1528
1664
  contains: contains,
1665
+ containsSync: containsSync,
1529
1666
  sep: path.sep
1530
1667
  });
1531
1668
 
1669
+ function chargeJsonReplacerValue(memoryLimit, val) {
1670
+ if (typeof val === 'string') {
1671
+ memoryLimit.use(val.length);
1672
+ }
1673
+ else if (val === null || typeof val === 'number' || typeof val === 'boolean') {
1674
+ memoryLimit.use(JSON.stringify(val).length);
1675
+ }
1676
+ else if (Array.isArray(val)) {
1677
+ memoryLimit.use(val.length + 1);
1678
+ }
1679
+ else if (typeof val === 'object') {
1680
+ memoryLimit.use(2);
1681
+ }
1682
+ }
1532
1683
  function defaultFilter(value, defaultValue, ...args) {
1533
1684
  value = toValue(value);
1534
1685
  if (isArray(value) || isString(value))
@@ -1538,19 +1689,29 @@ function defaultFilter(value, defaultValue, ...args) {
1538
1689
  return isFalsy(value, this.context) ? defaultValue : value;
1539
1690
  }
1540
1691
  function json(value, space = 0) {
1541
- return JSON.stringify(value, null, space);
1692
+ const memoryLimit = this.context.memoryLimit;
1693
+ return JSON.stringify(value, (_key, val) => {
1694
+ chargeJsonReplacerValue(memoryLimit, val);
1695
+ return val;
1696
+ }, space);
1542
1697
  }
1543
1698
  function inspect(value, space = 0) {
1699
+ const memoryLimit = this.context.memoryLimit;
1544
1700
  const ancestors = [];
1545
1701
  return JSON.stringify(value, function (_key, value) {
1546
- if (typeof value !== 'object' || value === null)
1702
+ if (typeof value !== 'object' || value === null) {
1703
+ chargeJsonReplacerValue(memoryLimit, value);
1547
1704
  return value;
1705
+ }
1548
1706
  // `this` is the object that value is contained in, i.e., its direct parent.
1549
1707
  while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this)
1550
1708
  ancestors.pop();
1551
- if (ancestors.includes(value))
1709
+ if (ancestors.includes(value)) {
1710
+ memoryLimit.use('[Circular]'.length);
1552
1711
  return '[Circular]';
1712
+ }
1553
1713
  ancestors.push(value);
1714
+ chargeJsonReplacerValue(memoryLimit, value);
1554
1715
  return value;
1555
1716
  }, space);
1556
1717
  }
@@ -1605,10 +1766,33 @@ function newline_to_br(v) {
1605
1766
  this.context.memoryLimit.use(str.length);
1606
1767
  return str.replace(/\r?\n/gm, '<br />\n');
1607
1768
  }
1769
+ // Raw-text blocks (HTML5) plus '<...>' as the catch-all kind; a regex
1770
+ // equivalent is O(n^2) in V8 on unclosed openers.
1608
1771
  function strip_html(v) {
1609
1772
  const str = stringify(v);
1610
1773
  this.context.memoryLimit.use(str.length);
1611
- return str.replace(/<script[\s\S]*?<\/script>|<style[\s\S]*?<\/style>|<.*?>|<!--[\s\S]*?-->/g, '');
1774
+ const blocks = new Map([['<script', '</script>'], ['<style', '</style>'], ['<!--', '-->'], ['<', '>']]);
1775
+ let out = '';
1776
+ let i = 0;
1777
+ while (i < str.length) {
1778
+ const lt = str.indexOf('<', i);
1779
+ if (lt < 0)
1780
+ return out + str.slice(i);
1781
+ out += str.slice(i, lt);
1782
+ for (const [opener, closer] of blocks) {
1783
+ if (!str.startsWith(opener, lt))
1784
+ continue;
1785
+ const e = str.indexOf(closer, lt + opener.length);
1786
+ if (e >= 0) {
1787
+ i = e + closer.length;
1788
+ break;
1789
+ }
1790
+ blocks.delete(opener);
1791
+ }
1792
+ if (i <= lt)
1793
+ return out + str.slice(lt);
1794
+ }
1795
+ return out;
1612
1796
  }
1613
1797
 
1614
1798
  var htmlFilters = /*#__PURE__*/Object.freeze({
@@ -1695,6 +1879,7 @@ const defaultOptions = {
1695
1879
  globals: {},
1696
1880
  keepOutputType: false,
1697
1881
  operators: defaultOperators,
1882
+ groupedExpressions: false,
1698
1883
  memoryLimit: Infinity,
1699
1884
  parseLimit: Infinity,
1700
1885
  renderLimit: Infinity
@@ -1790,7 +1975,7 @@ function trimRight(token, greedy) {
1790
1975
  }
1791
1976
 
1792
1977
  class Tokenizer {
1793
- constructor(input, operators = defaultOptions.operators, file, range) {
1978
+ constructor(input, operators = defaultOptions.operators, file, range, groupedExpressions = false) {
1794
1979
  this.input = input;
1795
1980
  this.file = file;
1796
1981
  this.rawBeginAt = -1;
@@ -1798,6 +1983,7 @@ class Tokenizer {
1798
1983
  this.N = range ? range[1] : input.length;
1799
1984
  this.opTrie = createTrie(operators);
1800
1985
  this.literalTrie = createTrie(literalValues);
1986
+ this.groupedExpressions = groupedExpressions;
1801
1987
  }
1802
1988
  readExpression() {
1803
1989
  return new Expression(this.readExpressionTokens());
@@ -1859,6 +2045,8 @@ class Tokenizer {
1859
2045
  this.skipBlank();
1860
2046
  if (this.end())
1861
2047
  return null;
2048
+ if (this.peek() === ')' || this.peek() === ',')
2049
+ return null;
1862
2050
  this.assert(this.read() === '|', `expected "|" before filter`);
1863
2051
  const name = this.readIdentifier();
1864
2052
  if (!name.size()) {
@@ -1873,10 +2061,10 @@ class Tokenizer {
1873
2061
  const arg = this.readFilterArg();
1874
2062
  arg && args.push(arg);
1875
2063
  this.skipBlank();
1876
- this.assert(this.end() || this.peek() === ',' || this.peek() === '|', () => `unexpected character ${this.snapshot()}`);
2064
+ this.assert(this.end() || this.peek() === ',' || this.peek() === '|' || this.peek() === ')', () => `unexpected character ${this.snapshot()}`);
1877
2065
  } while (this.peek() === ',');
1878
2066
  }
1879
- else if (this.peek() === '|' || this.end()) ;
2067
+ else if (this.peek() === '|' || this.peek() === ')' || this.peek() === ',' || this.end()) ;
1880
2068
  else {
1881
2069
  throw this.error('expected ":" after filter name');
1882
2070
  }
@@ -2082,7 +2270,10 @@ class Tokenizer {
2082
2270
  readValue() {
2083
2271
  this.skipBlank();
2084
2272
  const begin = this.p;
2085
- const variable = this.readLiteral() || this.readQuoted() || this.readRange() || this.readNumber();
2273
+ let variable = this.readLiteral() || this.readQuoted() || this.readNumber();
2274
+ if (!variable && this.peek() === '(') {
2275
+ variable = this.readGroupOrRange();
2276
+ }
2086
2277
  const props = this.readProperties(!variable);
2087
2278
  if (!props.length)
2088
2279
  return variable;
@@ -2161,17 +2352,30 @@ class Tokenizer {
2161
2352
  this.p = end;
2162
2353
  return literal;
2163
2354
  }
2164
- readRange() {
2355
+ readGroupOrRange() {
2165
2356
  this.skipBlank();
2166
2357
  const begin = this.p;
2167
2358
  if (this.peek() !== '(')
2168
2359
  return;
2169
2360
  ++this.p;
2170
2361
  const lhs = this.readValueOrThrow();
2171
- this.p += 2;
2172
- const rhs = this.readValueOrThrow();
2173
- ++this.p;
2174
- return new RangeToken(this.input, begin, this.p, lhs, rhs, this.file);
2362
+ this.skipBlank();
2363
+ if (this.peek() === '.' && this.peek(1) === '.') {
2364
+ this.p += 2;
2365
+ const rhs = this.readValueOrThrow();
2366
+ this.skipBlank();
2367
+ this.assert(this.read() === ')', 'invalid range syntax');
2368
+ return new RangeToken(this.input, begin, this.p, lhs, rhs, this.file);
2369
+ }
2370
+ if (this.groupedExpressions) {
2371
+ const initial = new Expression([lhs, ...this.readExpressionTokens()]);
2372
+ this.assert(initial.valid(), () => `invalid value expression: ${this.snapshot()}`);
2373
+ const filters = this.readFilters();
2374
+ this.skipBlank();
2375
+ this.assert(this.read() === ')', 'unbalanced parentheses');
2376
+ return new FilteredValueToken(initial, filters, this.input, begin, this.p, this.file);
2377
+ }
2378
+ throw this.error('invalid range syntax');
2175
2379
  }
2176
2380
  readValueOrThrow() {
2177
2381
  const value = this.readValue();
@@ -2198,7 +2402,7 @@ class Tokenizer {
2198
2402
  }
2199
2403
  *readFileNameTemplate(options) {
2200
2404
  const { outputDelimiterLeft } = options;
2201
- const htmlStopStrings = [',', ' ', outputDelimiterLeft];
2405
+ const htmlStopStrings = [',', ' ', '\r', '\n', '\t', outputDelimiterLeft];
2202
2406
  const htmlStopStringSet = new Set(htmlStopStrings);
2203
2407
  // break on ',' and ' ', outputDelimiterLeft only stops HTML token
2204
2408
  while (this.p < this.N && !htmlStopStringSet.has(this.peek())) {
@@ -2325,33 +2529,6 @@ function createTagClass(options) {
2325
2529
  };
2326
2530
  }
2327
2531
 
2328
- function isKeyValuePair(arr) {
2329
- return isArray(arr);
2330
- }
2331
-
2332
- class Filter {
2333
- constructor(token, options, liquid) {
2334
- this.token = token;
2335
- this.name = token.name;
2336
- this.handler = isFunction(options)
2337
- ? options
2338
- : (isFunction(options?.handler) ? options.handler : identify);
2339
- this.raw = !isFunction(options) && !!options?.raw;
2340
- this.args = token.args;
2341
- this.liquid = liquid;
2342
- }
2343
- *render(value, context) {
2344
- const argv = [];
2345
- for (const arg of this.args) {
2346
- if (isKeyValuePair(arg))
2347
- argv.push([arg[0], yield evalToken(arg[1], context)]);
2348
- else
2349
- argv.push(yield evalToken(arg, context));
2350
- }
2351
- return yield this.handler.apply({ context, token: this.token, liquid: this.liquid }, [value, ...argv]);
2352
- }
2353
- }
2354
-
2355
2532
  class Value {
2356
2533
  /**
2357
2534
  * @param str the value to be valuated, eg.: "foobar" | truncate: 3
@@ -2359,7 +2536,7 @@ class Value {
2359
2536
  constructor(input, liquid) {
2360
2537
  this.filters = [];
2361
2538
  const token = typeof input === 'string'
2362
- ? new Tokenizer(input, liquid.options.operators).readFilteredValue()
2539
+ ? new Tokenizer(input, liquid.options.operators, undefined, undefined, liquid.options.groupedExpressions).readFilteredValue()
2363
2540
  : input;
2364
2541
  this.initial = token.initial;
2365
2542
  this.filters = token.filters.map(token => new Filter(token, this.getFilter(liquid, token.name), liquid));
@@ -2382,7 +2559,7 @@ class Value {
2382
2559
  class Output extends TemplateImpl {
2383
2560
  constructor(token, liquid) {
2384
2561
  super(token);
2385
- const tokenizer = new Tokenizer(token.input, liquid.options.operators, token.file, token.contentRange);
2562
+ const tokenizer = new Tokenizer(token.input, liquid.options.operators, token.file, token.contentRange, liquid.options.groupedExpressions);
2386
2563
  this.value = new Value(tokenizer.readFilteredValue(), liquid);
2387
2564
  const filters = this.value.filters;
2388
2565
  const outputEscape = liquid.options.outputEscape;
@@ -2392,7 +2569,7 @@ class Output extends TemplateImpl {
2392
2569
  }
2393
2570
  }
2394
2571
  *render(ctx, emitter) {
2395
- const val = yield this.value.value(ctx, false);
2572
+ const val = yield toValue(yield this.value.value(ctx, false));
2396
2573
  emitter.write(val);
2397
2574
  }
2398
2575
  *arguments() {
@@ -2667,10 +2844,31 @@ function* extractValueTokenVariables(token) {
2667
2844
  yield* extractValueTokenVariables(token.lhs);
2668
2845
  yield* extractValueTokenVariables(token.rhs);
2669
2846
  }
2847
+ else if (isFilteredValueToken(token)) {
2848
+ yield* extractGroupedExpressionTokenVariables(token);
2849
+ }
2670
2850
  else if (isPropertyAccessToken(token)) {
2671
2851
  yield extractPropertyAccessVariable(token);
2672
2852
  }
2673
2853
  }
2854
+ function* extractGroupedExpressionTokenVariables(token) {
2855
+ if (!isFilteredValueToken(token))
2856
+ return;
2857
+ for (const t of token.initial.postfix) {
2858
+ if (isValueToken(t))
2859
+ yield* extractValueTokenVariables(t);
2860
+ }
2861
+ for (const filter of token.filters) {
2862
+ for (const arg of filter.args) {
2863
+ if (isKeyValuePair(arg) && arg[1]) {
2864
+ yield* extractValueTokenVariables(arg[1]);
2865
+ }
2866
+ else if (isValueToken(arg)) {
2867
+ yield* extractValueTokenVariables(arg);
2868
+ }
2869
+ }
2870
+ }
2871
+ }
2674
2872
  function extractPropertyAccessVariable(token) {
2675
2873
  const segments = [];
2676
2874
  // token is not guaranteed to have `file` set. We'll try to get it from a prop if not.
@@ -2739,52 +2937,53 @@ function segmentsString(segments, bracketedRoot = false) {
2739
2937
  return buf.join('');
2740
2938
  }
2741
2939
 
2742
- var LookupType;
2743
2940
  (function (LookupType) {
2744
2941
  LookupType["Partials"] = "partials";
2745
2942
  LookupType["Layouts"] = "layouts";
2746
2943
  LookupType["Root"] = "root";
2747
- })(LookupType || (LookupType = {}));
2944
+ })(exports.LookupType || (exports.LookupType = {}));
2748
2945
  class Loader {
2749
2946
  constructor(options) {
2750
2947
  this.options = options;
2751
2948
  if (options.relativeReference) {
2752
2949
  const sep = options.fs.sep;
2753
2950
  assert(sep, '`fs.sep` is required for relative reference');
2754
- const rRelativePath = new RegExp(['.' + sep, '..' + sep, './', '../'].map(prefix => escapeRegex(prefix)).join('|'));
2755
- this.shouldLoadRelative = (referencedFile) => rRelativePath.test(referencedFile);
2951
+ const prefixes = ['.' + sep, '..' + sep, './', '../'];
2952
+ this.shouldLoadRelative = (referencedFile) => prefixes.some(prefix => referencedFile.startsWith(prefix));
2756
2953
  }
2757
2954
  else {
2758
2955
  this.shouldLoadRelative = (_referencedFile) => false;
2759
2956
  }
2760
- this.contains = this.options.fs.contains || (() => true);
2957
+ const fs = options.fs;
2958
+ this.contains = toLiquidAsync(fs.contains?.bind(fs) || (async () => true), fs.containsSync?.bind(fs) || (() => true));
2959
+ this.exists = toLiquidAsync(fs.exists?.bind(fs) || (async () => false), fs.existsSync?.bind(fs));
2761
2960
  }
2762
2961
  *lookup(file, type, sync, currentFile) {
2763
- const { fs } = this.options;
2764
2962
  const dirs = this.options[type];
2765
- for (const filepath of this.candidates(file, dirs, currentFile, type !== LookupType.Root)) {
2766
- if (sync ? fs.existsSync(filepath) : yield fs.exists(filepath))
2963
+ for (const filepath of this.candidates(file, dirs, currentFile)) {
2964
+ let allowed = false;
2965
+ for (const dir of dirs) {
2966
+ if (yield this.contains(!!sync, dir, filepath)) {
2967
+ allowed = true;
2968
+ break;
2969
+ }
2970
+ }
2971
+ if (!allowed)
2972
+ continue;
2973
+ if (yield this.exists(!!sync, filepath))
2767
2974
  return filepath;
2768
2975
  }
2769
2976
  throw this.lookupError(file, dirs);
2770
2977
  }
2771
- *candidates(file, dirs, currentFile, enforceRoot) {
2978
+ *candidates(file, dirs, currentFile) {
2772
2979
  const { fs, extname } = this.options;
2773
2980
  if (this.shouldLoadRelative(file) && currentFile) {
2774
2981
  const referenced = fs.resolve(this.dirname(currentFile), file, extname);
2775
- for (const dir of dirs) {
2776
- if (!enforceRoot || this.contains(dir, referenced)) {
2777
- // the relatively referenced file is within one of root dirs
2778
- yield referenced;
2779
- break;
2780
- }
2781
- }
2982
+ yield referenced;
2782
2983
  }
2783
2984
  for (const dir of dirs) {
2784
2985
  const referenced = fs.resolve(dir, file, extname);
2785
- if (!enforceRoot || this.contains(dir, referenced)) {
2786
- yield referenced;
2787
- }
2986
+ yield referenced;
2788
2987
  }
2789
2988
  if (fs.fallback !== undefined) {
2790
2989
  const filepath = fs.fallback(file);
@@ -2813,11 +3012,12 @@ class Parser {
2813
3012
  this.parseFile = this.cache ? this._parseFileCached : this._parseFile;
2814
3013
  this.loader = new Loader(this.liquid.options);
2815
3014
  this.parseLimit = new Limiter('parse length', liquid.options.parseLimit);
3015
+ this.readFile = toLiquidAsync(this.fs.readFile?.bind(this.fs) || (async () => { throw new Error('readFile not implemented'); }), this.fs.readFileSync?.bind(this.fs));
2816
3016
  }
2817
3017
  parse(html, filepath) {
2818
3018
  html = String(html);
2819
3019
  this.parseLimit.use(html.length);
2820
- const tokenizer = new Tokenizer(html, this.liquid.options.operators, filepath);
3020
+ const tokenizer = new Tokenizer(html, this.liquid.options.operators, filepath, undefined, this.liquid.options.groupedExpressions);
2821
3021
  const tokens = tokenizer.readTopLevelTokens(this.liquid.options);
2822
3022
  return this.parseTokens(tokens);
2823
3023
  }
@@ -2861,7 +3061,7 @@ class Parser {
2861
3061
  parseStream(tokens) {
2862
3062
  return new ParseStream(tokens, (token, tokens) => this.parseToken(token, tokens));
2863
3063
  }
2864
- *_parseFileCached(file, sync, type = LookupType.Root, currentFile) {
3064
+ *_parseFileCached(file, sync, type = exports.LookupType.Root, currentFile) {
2865
3065
  const cache = this.cache;
2866
3066
  const key = this.loader.shouldLoadRelative(file) ? currentFile + ',' + file : type + ':' + file;
2867
3067
  const tpls = yield cache.read(key);
@@ -2881,9 +3081,9 @@ class Parser {
2881
3081
  throw err;
2882
3082
  }
2883
3083
  }
2884
- *_parseFile(file, sync, type = LookupType.Root, currentFile) {
3084
+ *_parseFile(file, sync, type = exports.LookupType.Root, currentFile) {
2885
3085
  const filepath = yield this.loader.lookup(file, type, sync, currentFile);
2886
- return this.parse(sync ? this.fs.readFileSync(filepath) : yield this.fs.readFile(filepath), filepath);
3086
+ return this.parse(yield this.readFile(!!sync, filepath), filepath);
2887
3087
  }
2888
3088
  }
2889
3089
 
@@ -2901,6 +3101,7 @@ class Parser {
2901
3101
  TokenKind[TokenKind["Quoted"] = 1024] = "Quoted";
2902
3102
  TokenKind[TokenKind["Operator"] = 2048] = "Operator";
2903
3103
  TokenKind[TokenKind["FilteredValue"] = 4096] = "FilteredValue";
3104
+ TokenKind[TokenKind["GroupedExpression"] = 8192] = "GroupedExpression";
2904
3105
  TokenKind[TokenKind["Delimited"] = 12] = "Delimited";
2905
3106
  })(exports.TokenKind || (exports.TokenKind = {}));
2906
3107
 
@@ -2937,9 +3138,12 @@ function isWordToken(val) {
2937
3138
  function isRangeToken(val) {
2938
3139
  return getKind(val) === exports.TokenKind.Range;
2939
3140
  }
3141
+ function isFilteredValueToken(val) {
3142
+ return getKind(val) === exports.TokenKind.FilteredValue;
3143
+ }
2940
3144
  function isValueToken(val) {
2941
- // valueTokenBitMask = TokenKind.Number | TokenKind.Literal | TokenKind.Quoted | TokenKind.PropertyAccess | TokenKind.Range
2942
- return (getKind(val) & 1667) > 0;
3145
+ // valueTokenBitMask = TokenKind.Number | TokenKind.Literal | TokenKind.Quoted | TokenKind.PropertyAccess | TokenKind.Range | TokenKind.FilteredValue
3146
+ return (getKind(val) & 5763) > 0;
2943
3147
  }
2944
3148
  function getKind(val) {
2945
3149
  return val ? val.kind : -1;
@@ -2958,6 +3162,7 @@ var typeGuards = /*#__PURE__*/Object.freeze({
2958
3162
  isPropertyAccessToken: isPropertyAccessToken,
2959
3163
  isWordToken: isWordToken,
2960
3164
  isRangeToken: isRangeToken,
3165
+ isFilteredValueToken: isFilteredValueToken,
2961
3166
  isValueToken: isValueToken
2962
3167
  });
2963
3168
 
@@ -2987,13 +3192,20 @@ var __assign = function() {
2987
3192
  return __assign.apply(this, arguments);
2988
3193
  };
2989
3194
 
3195
+ function createScope(from) {
3196
+ const scope = Object.create(null);
3197
+ if (from)
3198
+ Object.assign(scope, from);
3199
+ return scope;
3200
+ }
3201
+
2990
3202
  class Context {
2991
- constructor(env = {}, opts = defaultOptions, renderOptions = {}, { memoryLimit, renderLimit } = {}) {
3203
+ constructor(env = {}, opts = defaultOptions, renderOptions = {}, { memoryLimit, renderLimit, liquid } = {}) {
2992
3204
  /**
2993
3205
  * insert a Context-level empty scope,
2994
3206
  * for tags like `{% capture %}` `{% assign %}` to operate
2995
3207
  */
2996
- this.scopes = [{}];
3208
+ this.scopes = [createScope()];
2997
3209
  this.registers = {};
2998
3210
  this.breakCalled = false;
2999
3211
  this.continueCalled = false;
@@ -3005,9 +3217,10 @@ class Context {
3005
3217
  this.ownPropertyOnly = renderOptions.ownPropertyOnly ?? opts.ownPropertyOnly;
3006
3218
  this.memoryLimit = memoryLimit ?? new Limiter('memory alloc', renderOptions.memoryLimit ?? opts.memoryLimit);
3007
3219
  this.renderLimit = renderLimit ?? new Limiter('template render', getPerformance().now() + (renderOptions.renderLimit ?? opts.renderLimit));
3220
+ this.liquid = liquid;
3008
3221
  }
3009
- getRegister(key) {
3010
- return (this.registers[key] = this.registers[key] || {});
3222
+ getRegister(key, defaultValue = undefined) {
3223
+ return (this.registers[key] = this.registers[key] || defaultValue);
3011
3224
  }
3012
3225
  setRegister(key, value) {
3013
3226
  return (this.registers[key] = value);
@@ -3045,7 +3258,7 @@ class Context {
3045
3258
  if (isString(paths))
3046
3259
  paths = paths.split('.');
3047
3260
  for (let i = 0; i < paths.length; i++) {
3048
- scope = yield readProperty(scope, paths[i], this.ownPropertyOnly);
3261
+ scope = yield this.readProperty(scope, paths[i]);
3049
3262
  if (strictVariables && isUndefined(scope)) {
3050
3263
  throw new InternalUndefinedVariableError(paths.slice(0, i + 1).join('.'));
3051
3264
  }
@@ -3065,10 +3278,12 @@ class Context {
3065
3278
  return new Context(scope, this.opts, {
3066
3279
  sync: this.sync,
3067
3280
  globals: this.globals,
3068
- strictVariables: this.strictVariables
3281
+ strictVariables: this.strictVariables,
3282
+ ownPropertyOnly: this.ownPropertyOnly
3069
3283
  }, {
3070
3284
  renderLimit: this.renderLimit,
3071
- memoryLimit: this.memoryLimit
3285
+ memoryLimit: this.memoryLimit,
3286
+ liquid: this.liquid
3072
3287
  });
3073
3288
  }
3074
3289
  findScope(key) {
@@ -3081,41 +3296,41 @@ class Context {
3081
3296
  return this.environments;
3082
3297
  return this.globals;
3083
3298
  }
3084
- }
3085
- function readProperty(obj, key, ownPropertyOnly) {
3086
- obj = toLiquid(obj);
3087
- key = toValue(key);
3088
- if (isNil(obj))
3089
- return obj;
3090
- if (isArray(obj) && key < 0)
3091
- return obj[obj.length + +key];
3092
- const value = readJSProperty(obj, key, ownPropertyOnly);
3093
- if (value === undefined && obj instanceof Drop)
3094
- return obj.liquidMethodMissing(key);
3095
- if (isFunction(value))
3096
- return value.call(obj);
3097
- if (key === 'size')
3098
- return readSize(obj);
3099
- else if (key === 'first')
3100
- return readFirst(obj);
3101
- else if (key === 'last')
3102
- return readLast(obj);
3103
- return value;
3299
+ readProperty(obj, key) {
3300
+ obj = toLiquid(obj);
3301
+ key = toValue(key);
3302
+ if (isNil(obj))
3303
+ return obj;
3304
+ if (isArray(obj) && isNumber(key))
3305
+ return readArrayElement(obj, key, this.ownPropertyOnly);
3306
+ const value = readJSProperty(obj, key, this.ownPropertyOnly);
3307
+ if (value === undefined && obj instanceof Drop)
3308
+ return obj.liquidMethodMissing(key, this);
3309
+ if (isFunction(value))
3310
+ return value.call(obj);
3311
+ if (key === 'size')
3312
+ return readSize(obj);
3313
+ else if (key === 'first')
3314
+ return readFirst(obj, this.ownPropertyOnly);
3315
+ else if (key === 'last')
3316
+ return readLast(obj, this.ownPropertyOnly);
3317
+ return value;
3318
+ }
3104
3319
  }
3105
3320
  function readJSProperty(obj, key, ownPropertyOnly) {
3106
3321
  if (ownPropertyOnly && !hasOwnProperty.call(obj, key) && !(obj instanceof Drop))
3107
3322
  return undefined;
3108
3323
  return obj[key];
3109
3324
  }
3110
- function readFirst(obj) {
3325
+ function readFirst(obj, ownPropertyOnly) {
3111
3326
  if (isArray(obj))
3112
- return obj[0];
3113
- return obj['first'];
3327
+ return readArrayElement(obj, 0, ownPropertyOnly);
3328
+ return readJSProperty(obj, 'first', ownPropertyOnly);
3114
3329
  }
3115
- function readLast(obj) {
3330
+ function readLast(obj, ownPropertyOnly) {
3116
3331
  if (isArray(obj))
3117
- return obj[obj.length - 1];
3118
- return obj['last'];
3332
+ return readArrayElement(obj, -1, ownPropertyOnly);
3333
+ return readJSProperty(obj, 'last', ownPropertyOnly);
3119
3334
  }
3120
3335
  function readSize(obj) {
3121
3336
  if (hasOwnProperty.call(obj, 'size') || obj['size'] !== undefined)
@@ -3134,25 +3349,28 @@ var BlockMode;
3134
3349
  BlockMode[BlockMode["STORE"] = 1] = "STORE";
3135
3350
  })(BlockMode || (BlockMode = {}));
3136
3351
 
3137
- const abs = argumentsToValue(Math.abs);
3138
- const at_least = argumentsToValue(Math.max);
3139
- const at_most = argumentsToValue(Math.min);
3140
- const ceil = argumentsToValue(Math.ceil);
3141
- const divided_by = argumentsToValue((dividend, divisor, integerArithmetic = false) => integerArithmetic ? Math.floor(dividend / divisor) : dividend / divisor);
3142
- const floor = argumentsToValue(Math.floor);
3143
- const minus = argumentsToValue((v, arg) => v - arg);
3144
- const modulo = argumentsToValue((v, arg) => v % arg);
3145
- const times = argumentsToValue((v, arg) => v * arg);
3146
- function round(v, arg = 0) {
3147
- v = toValue(v);
3148
- arg = toValue(arg);
3149
- const amp = Math.pow(10, arg);
3150
- return Math.round(v * amp) / amp;
3352
+ const abs = argumentsToNumber(Math.abs);
3353
+ const at_least = argumentsToNumber(Math.max);
3354
+ const at_most = argumentsToNumber(Math.min);
3355
+ const ceil = argumentsToNumber(Math.ceil);
3356
+ const divided_by = argumentsToNumber((dividend, divisor, integerArithmetic = false) => integerArithmetic ? Math.floor(dividend / divisor) : dividend / divisor);
3357
+ const floor = argumentsToNumber(Math.floor);
3358
+ const minus = argumentsToNumber((v, arg) => v - arg);
3359
+ const plus = argumentsToNumber((lhs, rhs) => lhs + rhs);
3360
+ const modulo = argumentsToNumber((v, arg) => ((v % arg) + arg) % arg);
3361
+ const times = argumentsToNumber((v, arg) => v * arg);
3362
+ function shiftDecimal(n, exponent) {
3363
+ const [mantissa, power = '0'] = String(n).split('e');
3364
+ return Number(`${mantissa}e${Number(power) + exponent}`);
3151
3365
  }
3152
- function plus(v, arg) {
3153
- v = toValue(v);
3154
- arg = toValue(arg);
3155
- return Number(v) + Number(arg);
3366
+ function round(v, arg = 0) {
3367
+ v = toNumber(v);
3368
+ arg = toNumber(arg);
3369
+ const scaled = shiftDecimal(Math.abs(v), arg);
3370
+ if (!(scaled <= Number.MAX_SAFE_INTEGER))
3371
+ return v;
3372
+ const rounded = shiftDecimal(Math.round(scaled), -arg);
3373
+ return v < 0 ? -rounded : rounded;
3156
3374
  }
3157
3375
 
3158
3376
  var mathFilters = /*#__PURE__*/Object.freeze({
@@ -3164,13 +3382,13 @@ var mathFilters = /*#__PURE__*/Object.freeze({
3164
3382
  divided_by: divided_by,
3165
3383
  floor: floor,
3166
3384
  minus: minus,
3385
+ plus: plus,
3167
3386
  modulo: modulo,
3168
3387
  times: times,
3169
- round: round,
3170
- plus: plus
3388
+ round: round
3171
3389
  });
3172
3390
 
3173
- const url_decode = (x) => decodeURIComponent(stringify(x)).replace(/\+/g, ' ');
3391
+ const url_decode = (x) => decodeURIComponent(stringify(x).replace(/\+/g, ' '));
3174
3392
  const url_encode = (x) => encodeURIComponent(stringify(x)).replace(/%20/g, '+');
3175
3393
  const cgi_escape = (x) => encodeURIComponent(stringify(x))
3176
3394
  .replace(/%20/g, '+')
@@ -3228,18 +3446,24 @@ var urlFilters = /*#__PURE__*/Object.freeze({
3228
3446
  const join = argumentsToValue(function (v, arg) {
3229
3447
  const array = toArray(v);
3230
3448
  const sep = isNil(arg) ? ' ' : stringify(arg);
3231
- const complexity = array.length * (1 + sep.length);
3232
- this.context.memoryLimit.use(complexity);
3233
- return array.join(sep);
3449
+ let outputSize = sep.length * Math.max(array.length - 1, 0);
3450
+ for (let i = 0; i < array.length; i++)
3451
+ outputSize += String(array[i]).length;
3452
+ this.context.memoryLimit.use(outputSize);
3453
+ return Array.prototype.join.call(array, sep);
3454
+ });
3455
+ const last = argumentsToValue(function (v) {
3456
+ return isArrayLike(v) ? readArrayElement(v, -1, this.context.ownPropertyOnly) : '';
3457
+ });
3458
+ const first = argumentsToValue(function (v) {
3459
+ return isArrayLike(v) ? readArrayElement(v, 0, this.context.ownPropertyOnly) : '';
3234
3460
  });
3235
- const last$1 = argumentsToValue((v) => isArrayLike(v) ? last(v) : '');
3236
- const first = argumentsToValue((v) => isArrayLike(v) ? v[0] : '');
3237
3461
  const reverse = argumentsToValue(function (v) {
3238
3462
  const array = toArray(v);
3239
3463
  this.context.memoryLimit.use(array.length);
3240
3464
  return [...array].reverse();
3241
3465
  });
3242
- function* sort(arr, property) {
3466
+ function* sortBy(arr, property, comparator) {
3243
3467
  const values = [];
3244
3468
  const array = toArray(arr);
3245
3469
  this.context.memoryLimit.use(array.length);
@@ -3249,20 +3473,13 @@ function* sort(arr, property) {
3249
3473
  property ? yield this.context._getFromScope(item, stringify(property).split('.'), false) : item
3250
3474
  ]);
3251
3475
  }
3252
- return values.sort((lhs, rhs) => {
3253
- const lvalue = lhs[1];
3254
- const rvalue = rhs[1];
3255
- return lvalue < rvalue ? -1 : (lvalue > rvalue ? 1 : 0);
3256
- }).map(tuple => tuple[0]);
3257
- }
3258
- function sort_natural(input, property) {
3259
- const propertyString = stringify(property);
3260
- const compare = property === undefined
3261
- ? caseInsensitiveCompare
3262
- : (lhs, rhs) => caseInsensitiveCompare(lhs[propertyString], rhs[propertyString]);
3263
- const array = toArray(input);
3264
- this.context.memoryLimit.use(array.length);
3265
- return [...array].sort(compare);
3476
+ return values.sort((lhs, rhs) => comparator(lhs[1], rhs[1])).map(tuple => tuple[0]);
3477
+ }
3478
+ function* sort(arr, property) {
3479
+ return yield* sortBy.call(this, arr, property, orderedCompare);
3480
+ }
3481
+ function* sort_natural(arr, property) {
3482
+ return yield* sortBy.call(this, arr, property, caseInsensitiveCompare);
3266
3483
  }
3267
3484
  const size = (v) => (v && v.length) || 0;
3268
3485
  function* map(arr, property) {
@@ -3286,13 +3503,13 @@ function* sum(arr, property) {
3286
3503
  function compact(arr) {
3287
3504
  const array = toArray(arr);
3288
3505
  this.context.memoryLimit.use(array.length);
3289
- return array.filter(x => !isNil(toValue(x)));
3506
+ return Array.prototype.filter.call(array, x => !isNil(toValue(x)));
3290
3507
  }
3291
3508
  function concat(v, arg = []) {
3292
3509
  const lhs = toArray(v);
3293
3510
  const rhs = toArray(arg);
3294
3511
  this.context.memoryLimit.use(lhs.length + rhs.length);
3295
- return lhs.concat(rhs);
3512
+ return Array.prototype.concat.call(lhs, rhs);
3296
3513
  }
3297
3514
  function push(v, arg) {
3298
3515
  return concat.call(this, v, [arg]);
@@ -3305,7 +3522,9 @@ function unshift(v, arg) {
3305
3522
  return clone;
3306
3523
  }
3307
3524
  function pop(v) {
3308
- const clone = [...toArray(v)];
3525
+ const array = toArray(v);
3526
+ this.context.memoryLimit.use(array.length);
3527
+ const clone = [...array];
3309
3528
  clone.pop();
3310
3529
  return clone;
3311
3530
  }
@@ -3322,39 +3541,65 @@ function slice(v, begin, length = 1) {
3322
3541
  return [];
3323
3542
  if (!isArray(v))
3324
3543
  v = stringify(v);
3325
- begin = begin < 0 ? v.length + begin : begin;
3544
+ begin = begin < 0 ? (isArray(v) ? v.length : codePointLength(v)) + begin : begin;
3545
+ if (begin < 0 || length < 0)
3546
+ return isArray(v) ? [] : '';
3326
3547
  this.context.memoryLimit.use(length);
3327
- return v.slice(begin, begin + length);
3328
- }
3329
- function* where(arr, property, expected) {
3330
- const values = [];
3548
+ if (isArray(v))
3549
+ return Array.prototype.slice.call(v, begin, begin + length);
3550
+ const start = advanceCodePoints(v, 0, begin);
3551
+ return v.slice(start, advanceCodePoints(v, start, length));
3552
+ }
3553
+ function* matches(value, expected) {
3554
+ if (this.context.opts.jekyllWhere) {
3555
+ if (EmptyDrop.is(expected))
3556
+ return yield equals(value, expected);
3557
+ if (isArray(value))
3558
+ return yield arrayIncludes(value, expected);
3559
+ return yield equals(value, expected);
3560
+ }
3561
+ if (expected === undefined)
3562
+ return isTruthy(value, this.context);
3563
+ return yield equals(value, expected);
3564
+ }
3565
+ function* filter(include, arr, property, expected) {
3331
3566
  arr = toArray(arr);
3332
3567
  this.context.memoryLimit.use(arr.length);
3333
3568
  const token = new Tokenizer(stringify(property)).readScopeValue();
3569
+ const result = [];
3334
3570
  for (const item of arr) {
3335
- values.push(yield evalToken(token, this.context.spawn(item)));
3336
- }
3337
- const matcher = this.context.opts.jekyllWhere
3338
- ? (v) => EmptyDrop.is(expected) ? equals(v, expected) : (isArray(v) ? arrayIncludes(v, expected) : equals(v, expected))
3339
- : (v) => equals(v, expected);
3340
- return arr.filter((_, i) => {
3341
- if (expected === undefined)
3342
- return isTruthy(values[i], this.context);
3343
- return matcher(values[i]);
3344
- });
3571
+ const value = yield evalToken(token, this.context.spawn(item));
3572
+ if ((yield matches.call(this, value, expected)) === include)
3573
+ result.push(item);
3574
+ }
3575
+ return result;
3345
3576
  }
3346
- function* where_exp(arr, itemName, exp) {
3577
+ function* filter_exp(include, arr, itemName, exp) {
3347
3578
  const filtered = [];
3348
3579
  const keyTemplate = new Value(stringify(exp), this.liquid);
3349
3580
  const array = toArray(arr);
3350
3581
  this.context.memoryLimit.use(array.length);
3351
3582
  for (const item of array) {
3352
- const value = yield keyTemplate.value(this.context.spawn({ [itemName]: item }));
3353
- if (value)
3583
+ this.context.push({ [itemName]: item });
3584
+ const value = yield keyTemplate.value(this.context);
3585
+ this.context.pop();
3586
+ if (value === include)
3354
3587
  filtered.push(item);
3355
3588
  }
3356
3589
  return filtered;
3357
3590
  }
3591
+ function* where(arr, property, expected) {
3592
+ return yield* filter.call(this, true, arr, property, expected);
3593
+ }
3594
+ function* reject(arr, property, expected) {
3595
+ return yield* filter.call(this, false, arr, property, expected);
3596
+ }
3597
+ function* where_exp(arr, itemName, exp) {
3598
+ return yield* filter_exp.call(this, true, arr, itemName, exp);
3599
+ }
3600
+ function* reject_exp(arr, itemName, exp) {
3601
+ return yield* filter_exp.call(this, false, arr, itemName, exp);
3602
+ }
3358
3603
  function* group_by(arr, property) {
3359
3604
  const map = new Map();
3360
3605
  arr = toEnumerable(arr);
@@ -3374,31 +3619,59 @@ function* group_by_exp(arr, itemName, exp) {
3374
3619
  arr = toEnumerable(arr);
3375
3620
  this.context.memoryLimit.use(arr.length);
3376
3621
  for (const item of arr) {
3377
- const key = yield keyTemplate.value(this.context.spawn({ [itemName]: item }));
3622
+ this.context.push({ [itemName]: item });
3623
+ const key = yield keyTemplate.value(this.context);
3624
+ this.context.pop();
3378
3625
  if (!map.has(key))
3379
3626
  map.set(key, []);
3380
3627
  map.get(key).push(item);
3381
3628
  }
3382
3629
  return [...map.entries()].map(([name, items]) => ({ name, items }));
3383
3630
  }
3384
- function* find(arr, property, expected) {
3631
+ function* search(arr, property, expected) {
3385
3632
  const token = new Tokenizer(stringify(property)).readScopeValue();
3386
3633
  const array = toArray(arr);
3387
- for (const item of array) {
3388
- const value = yield evalToken(token, this.context.spawn(item));
3389
- if (equals(value, expected))
3390
- return item;
3634
+ for (let index = 0; index < array.length; index++) {
3635
+ const value = yield evalToken(token, this.context.spawn(array[index]));
3636
+ if (yield matches.call(this, value, expected))
3637
+ return [index, array[index]];
3391
3638
  }
3392
3639
  }
3393
- function* find_exp(arr, itemName, exp) {
3640
+ function* search_exp(arr, itemName, exp) {
3394
3641
  const predicate = new Value(stringify(exp), this.liquid);
3395
3642
  const array = toArray(arr);
3396
- for (const item of array) {
3397
- const value = yield predicate.value(this.context.spawn({ [itemName]: item }));
3643
+ for (let index = 0; index < array.length; index++) {
3644
+ this.context.push({ [itemName]: array[index] });
3645
+ const value = yield predicate.value(this.context);
3646
+ this.context.pop();
3398
3647
  if (value)
3399
- return item;
3648
+ return [index, array[index]];
3400
3649
  }
3401
3650
  }
3651
+ function* has(arr, property, expected) {
3652
+ const result = yield* search.call(this, arr, property, expected);
3653
+ return !!result;
3654
+ }
3655
+ function* has_exp(arr, itemName, exp) {
3656
+ const result = yield* search_exp.call(this, arr, itemName, exp);
3657
+ return !!result;
3658
+ }
3659
+ function* find_index(arr, property, expected) {
3660
+ const result = yield* search.call(this, arr, property, expected);
3661
+ return result ? result[0] : undefined;
3662
+ }
3663
+ function* find_index_exp(arr, itemName, exp) {
3664
+ const result = yield* search_exp.call(this, arr, itemName, exp);
3665
+ return result ? result[0] : undefined;
3666
+ }
3667
+ function* find(arr, property, expected) {
3668
+ const result = yield* search.call(this, arr, property, expected);
3669
+ return result ? result[1] : undefined;
3670
+ }
3671
+ function* find_exp(arr, itemName, exp) {
3672
+ const result = yield* search_exp.call(this, arr, itemName, exp);
3673
+ return result ? result[1] : undefined;
3674
+ }
3402
3675
  function uniq(arr) {
3403
3676
  arr = toArray(arr);
3404
3677
  this.context.memoryLimit.use(arr.length);
@@ -3410,7 +3683,7 @@ function sample(v, count = 1) {
3410
3683
  return [];
3411
3684
  if (!isArray(v))
3412
3685
  v = stringify(v);
3413
- this.context.memoryLimit.use(count);
3686
+ this.context.memoryLimit.use(v.length);
3414
3687
  const shuffled = [...v].sort(() => Math.random() - 0.5);
3415
3688
  if (count === 1)
3416
3689
  return shuffled[0];
@@ -3420,7 +3693,7 @@ function sample(v, count = 1) {
3420
3693
  var arrayFilters = /*#__PURE__*/Object.freeze({
3421
3694
  __proto__: null,
3422
3695
  join: join,
3423
- last: last$1,
3696
+ last: last,
3424
3697
  first: first,
3425
3698
  reverse: reverse,
3426
3699
  sort: sort,
@@ -3436,9 +3709,15 @@ var arrayFilters = /*#__PURE__*/Object.freeze({
3436
3709
  shift: shift,
3437
3710
  slice: slice,
3438
3711
  where: where,
3712
+ reject: reject,
3439
3713
  where_exp: where_exp,
3714
+ reject_exp: reject_exp,
3440
3715
  group_by: group_by,
3441
3716
  group_by_exp: group_by_exp,
3717
+ has: has,
3718
+ has_exp: has_exp,
3719
+ find_index: find_index,
3720
+ find_index_exp: find_index_exp,
3442
3721
  find: find,
3443
3722
  find_exp: find_exp,
3444
3723
  uniq: uniq,
@@ -3446,20 +3725,24 @@ var arrayFilters = /*#__PURE__*/Object.freeze({
3446
3725
  });
3447
3726
 
3448
3727
  function date(v, format, timezoneOffset) {
3449
- const size = (v?.length ?? 0) + (format?.length ?? 0) + (timezoneOffset?.length ?? 0);
3728
+ const size = (v?.length ?? 0) + (timezoneOffset?.length ?? 0);
3450
3729
  this.context.memoryLimit.use(size);
3451
3730
  const date = parseDate(v, this.context.opts, timezoneOffset);
3452
3731
  if (!date)
3453
3732
  return v;
3454
3733
  format = toValue(format);
3455
3734
  format = isNil(format) ? this.context.opts.dateFormat : stringify(format);
3456
- return strftime(date, format);
3735
+ this.context.memoryLimit.use(format.length);
3736
+ return strftime(date, format, this.context.memoryLimit);
3457
3737
  }
3458
3738
  function date_to_xmlschema(v) {
3459
3739
  return date.call(this, v, '%Y-%m-%dT%H:%M:%S%:z');
3460
3740
  }
3461
3741
  function date_to_rfc822(v) {
3462
- return date.call(this, v, '%a, %d %b %Y %H:%M:%S %z');
3742
+ const date = parseDate(v, this.context.opts, undefined, 'en-US');
3743
+ if (!date)
3744
+ return v;
3745
+ return strftime(date, '%a, %d %b %Y %H:%M:%S %z', this.context.memoryLimit);
3463
3746
  }
3464
3747
  function date_to_string(v, type, style) {
3465
3748
  return stringify_date.call(this, v, '%b', type, style);
@@ -3471,20 +3754,23 @@ function stringify_date(v, month_type, type, style) {
3471
3754
  const date = parseDate(v, this.context.opts);
3472
3755
  if (!date)
3473
3756
  return v;
3757
+ const ml = this.context.memoryLimit;
3474
3758
  if (type === 'ordinal') {
3475
3759
  const d = date.getDate();
3476
3760
  return style === 'US'
3477
- ? strftime(date, `${month_type} ${d}%q, %Y`)
3478
- : strftime(date, `${d}%q ${month_type} %Y`);
3761
+ ? strftime(date, `${month_type} ${d}%q, %Y`, ml)
3762
+ : strftime(date, `${d}%q ${month_type} %Y`, ml);
3479
3763
  }
3480
- return strftime(date, `%d ${month_type} %Y`);
3764
+ return strftime(date, `%d ${month_type} %Y`, ml);
3481
3765
  }
3482
- function parseDate(v, opts, timezoneOffset) {
3766
+ function parseDate(v, opts, timezoneOffset, locale = opts.locale) {
3483
3767
  let date;
3484
3768
  const defaultTimezoneOffset = timezoneOffset ?? opts.timezoneOffset;
3485
- const locale = opts.locale;
3486
3769
  v = toValue(v);
3487
- if (v === 'now' || v === 'today') {
3770
+ if (isNil(v)) {
3771
+ return undefined;
3772
+ }
3773
+ else if (v === 'now' || v === 'today') {
3488
3774
  date = new LiquidDate(Date.now(), locale, defaultTimezoneOffset);
3489
3775
  }
3490
3776
  else if (isNumber(v)) {
@@ -3538,14 +3824,34 @@ function prepend(v, arg) {
3538
3824
  this.context.memoryLimit.use(lhs.length + rhs.length);
3539
3825
  return rhs + lhs;
3540
3826
  }
3827
+ function getTrimStart(str, chars) {
3828
+ let start = 0;
3829
+ for (const char of str) {
3830
+ if (!chars.has(char))
3831
+ break;
3832
+ start += char.length;
3833
+ }
3834
+ return start;
3835
+ }
3836
+ function getTrimEnd(str, chars, start = 0) {
3837
+ let end = str.length;
3838
+ while (end > start) {
3839
+ const size = end - start >= 2 && str.codePointAt(end - 2) > 0xFFFF ? 2 : 1;
3840
+ if (!chars.has(str.slice(end - size, end)))
3841
+ break;
3842
+ end -= size;
3843
+ }
3844
+ return end;
3845
+ }
3541
3846
  function lstrip(v, chars) {
3542
3847
  const str = stringify(v);
3543
3848
  this.context.memoryLimit.use(str.length);
3544
3849
  if (chars) {
3545
- chars = escapeRegExp(stringify(chars));
3546
- return str.replace(new RegExp(`^[${chars}]+`, 'g'), '');
3850
+ chars = stringify(chars);
3851
+ this.context.memoryLimit.use(chars.length);
3852
+ return str.slice(getTrimStart(str, new Set(chars)));
3547
3853
  }
3548
- return str.replace(/^\s+/, '');
3854
+ return str.trimStart();
3549
3855
  }
3550
3856
  function downcase(v) {
3551
3857
  const str = stringify(v);
@@ -3559,18 +3865,20 @@ function upcase(v) {
3559
3865
  }
3560
3866
  function remove(v, arg) {
3561
3867
  const str = stringify(v);
3562
- this.context.memoryLimit.use(str.length);
3563
- return str.split(stringify(arg)).join('');
3868
+ arg = stringify(arg);
3869
+ this.context.memoryLimit.use(str.length + arg.length);
3870
+ return str.split(arg).join('');
3564
3871
  }
3565
3872
  function remove_first(v, l) {
3566
3873
  const str = stringify(v);
3567
- this.context.memoryLimit.use(str.length);
3568
- return str.replace(stringify(l), '');
3874
+ l = stringify(l);
3875
+ this.context.memoryLimit.use(str.length + l.length);
3876
+ return str.replace(l, '');
3569
3877
  }
3570
3878
  function remove_last(v, l) {
3571
3879
  const str = stringify(v);
3572
- this.context.memoryLimit.use(str.length);
3573
3880
  const pattern = stringify(l);
3881
+ this.context.memoryLimit.use(str.length + pattern.length);
3574
3882
  const index = str.lastIndexOf(pattern);
3575
3883
  if (index === -1)
3576
3884
  return str;
@@ -3580,10 +3888,11 @@ function rstrip(str, chars) {
3580
3888
  str = stringify(str);
3581
3889
  this.context.memoryLimit.use(str.length);
3582
3890
  if (chars) {
3583
- chars = escapeRegExp(stringify(chars));
3584
- return str.replace(new RegExp(`[${chars}]+$`, 'g'), '');
3891
+ chars = stringify(chars);
3892
+ this.context.memoryLimit.use(chars.length);
3893
+ return str.slice(0, getTrimEnd(str, new Set(chars)));
3585
3894
  }
3586
- return str.replace(/\s+$/, '');
3895
+ return str.trimEnd();
3587
3896
  }
3588
3897
  function split(v, arg) {
3589
3898
  const str = stringify(v);
@@ -3599,10 +3908,10 @@ function strip(v, chars) {
3599
3908
  const str = stringify(v);
3600
3909
  this.context.memoryLimit.use(str.length);
3601
3910
  if (chars) {
3602
- chars = escapeRegExp(stringify(chars));
3603
- return str
3604
- .replace(new RegExp(`^[${chars}]+`, 'g'), '')
3605
- .replace(new RegExp(`[${chars}]+$`, 'g'), '');
3911
+ const set = new Set(stringify(chars));
3912
+ this.context.memoryLimit.use(set.size);
3913
+ const start = getTrimStart(str, set);
3914
+ return str.slice(start, getTrimEnd(str, set, start));
3606
3915
  }
3607
3916
  return str.trim();
3608
3917
  }
@@ -3611,6 +3920,11 @@ function strip_newlines(v) {
3611
3920
  this.context.memoryLimit.use(str.length);
3612
3921
  return str.replace(/\r?\n/gm, '');
3613
3922
  }
3923
+ function squish(v) {
3924
+ const str = stringify(v);
3925
+ this.context.memoryLimit.use(str.length);
3926
+ return str.replace(/\s+/g, ' ').trim();
3927
+ }
3614
3928
  function capitalize(str) {
3615
3929
  str = stringify(str);
3616
3930
  this.context.memoryLimit.use(str.length);
@@ -3618,35 +3932,43 @@ function capitalize(str) {
3618
3932
  }
3619
3933
  function replace(v, pattern, replacement) {
3620
3934
  const str = stringify(v);
3621
- this.context.memoryLimit.use(str.length);
3622
- return str.split(stringify(pattern)).join(replacement);
3935
+ pattern = stringify(pattern);
3936
+ replacement = stringify(replacement);
3937
+ const parts = str.split(pattern);
3938
+ const outputSize = str.length + (parts.length - 1) * (replacement.length - pattern.length);
3939
+ this.context.memoryLimit.use(outputSize);
3940
+ return parts.join(replacement);
3623
3941
  }
3624
3942
  function replace_first(v, arg1, arg2) {
3625
3943
  const str = stringify(v);
3626
- this.context.memoryLimit.use(str.length);
3627
- return str.replace(stringify(arg1), arg2);
3944
+ arg1 = stringify(arg1);
3945
+ arg2 = stringify(arg2);
3946
+ this.context.memoryLimit.use(str.length + arg1.length + arg2.length);
3947
+ return str.replace(arg1, () => arg2);
3628
3948
  }
3629
3949
  function replace_last(v, arg1, arg2) {
3630
3950
  const str = stringify(v);
3631
- this.context.memoryLimit.use(str.length);
3632
3951
  const pattern = stringify(arg1);
3952
+ const replacement = stringify(arg2);
3953
+ this.context.memoryLimit.use(str.length + pattern.length + replacement.length);
3633
3954
  const index = str.lastIndexOf(pattern);
3634
3955
  if (index === -1)
3635
3956
  return str;
3636
- const replacement = stringify(arg2);
3637
3957
  return str.substring(0, index) + replacement + str.substring(index + pattern.length);
3638
3958
  }
3639
3959
  function truncate(v, l = 50, o = '...') {
3640
3960
  const str = stringify(v);
3641
- this.context.memoryLimit.use(str.length);
3642
- if (str.length <= l)
3961
+ o = stringify(o);
3962
+ this.context.memoryLimit.use(str.length + o.length);
3963
+ if (str.length <= l || advanceCodePoints(str, 0, l) >= str.length)
3643
3964
  return v;
3644
- return str.substring(0, l - o.length) + o;
3965
+ return str.slice(0, advanceCodePoints(str, 0, Math.max(l - codePointLength(o), 0))) + o;
3645
3966
  }
3646
3967
  function truncatewords(v, words = 15, o = '...') {
3647
3968
  const str = stringify(v);
3648
- this.context.memoryLimit.use(str.length);
3649
- const arr = str.split(/\s+/);
3969
+ o = stringify(o);
3970
+ this.context.memoryLimit.use(str.length + o.length);
3971
+ const arr = str.trimStart().split(/\s+/);
3650
3972
  if (words <= 0)
3651
3973
  words = 1;
3652
3974
  let ret = arr.slice(0, words).join(' ');
@@ -3679,8 +4001,13 @@ function number_of_words(input, mode) {
3679
4001
  return input.split(/\s+/).length;
3680
4002
  }
3681
4003
  }
3682
- function array_to_sentence_string(array, connector = 'and') {
3683
- this.context.memoryLimit.use(array.length);
4004
+ function array_to_sentence_string(input, connector = 'and') {
4005
+ const array = toArray(input);
4006
+ connector = stringify(connector);
4007
+ let outputSize = connector.length + array.length * 2;
4008
+ for (let i = 0; i < array.length; i++)
4009
+ outputSize += stringify(array[i]).length;
4010
+ this.context.memoryLimit.use(outputSize);
3684
4011
  switch (array.length) {
3685
4012
  case 0:
3686
4013
  return '';
@@ -3707,6 +4034,7 @@ var stringFilters = /*#__PURE__*/Object.freeze({
3707
4034
  split: split,
3708
4035
  strip: strip,
3709
4036
  strip_newlines: strip_newlines,
4037
+ squish: squish,
3710
4038
  capitalize: capitalize,
3711
4039
  replace: replace,
3712
4040
  replace_first: replace_first,
@@ -3718,6 +4046,76 @@ var stringFilters = /*#__PURE__*/Object.freeze({
3718
4046
  array_to_sentence_string: array_to_sentence_string
3719
4047
  });
3720
4048
 
4049
+ function base64Encode(str) {
4050
+ return Buffer.from(str, 'utf8').toString('base64');
4051
+ }
4052
+ function base64Decode(str) {
4053
+ return Buffer.from(str, 'base64').toString('utf8');
4054
+ }
4055
+
4056
+ /**
4057
+ * Base64 related filters
4058
+ *
4059
+ * Implements base64_encode and base64_decode filters for Shopify compatibility
4060
+ */
4061
+ function base64_encode(value) {
4062
+ if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) {
4063
+ this.context.memoryLimit.use(value.byteLength);
4064
+ return value.toString('base64');
4065
+ }
4066
+ const str = stringify(value);
4067
+ this.context.memoryLimit.use(str.length);
4068
+ return base64Encode(str);
4069
+ }
4070
+ function base64_decode(value) {
4071
+ const str = stringify(value);
4072
+ this.context.memoryLimit.use(str.length);
4073
+ assert(isBase64(str), 'invalid base64 provided to base64_decode');
4074
+ return base64Decode(str);
4075
+ }
4076
+ function isBase64(str) {
4077
+ str = str.replace(/[\t\n\f\r ]/g, '');
4078
+ if (str.length % 4 === 0)
4079
+ str = str.replace(/==?$/, '');
4080
+ return str.length % 4 !== 1 && /^[A-Za-z0-9+/]*$/.test(str);
4081
+ }
4082
+
4083
+ var base64Filters = /*#__PURE__*/Object.freeze({
4084
+ __proto__: null,
4085
+ base64_encode: base64_encode,
4086
+ base64_decode: base64_decode
4087
+ });
4088
+
4089
+ function sha256(str) {
4090
+ return crypto.createHash('sha256').update(str, 'utf8').digest('hex');
4091
+ }
4092
+ function hmacSha256(str, key) {
4093
+ return crypto.createHmac('sha256', key).update(str, 'utf8').digest('hex');
4094
+ }
4095
+
4096
+ /**
4097
+ * Crypto related filters
4098
+ *
4099
+ * Implements sha256 and hmac_sha256 filters for Shopify compatibility
4100
+ */
4101
+ function sha256$1(value) {
4102
+ const str = stringify(value);
4103
+ this.context.memoryLimit.use(str.length);
4104
+ return sha256(str);
4105
+ }
4106
+ function hmac_sha256(value, key) {
4107
+ const str = stringify(value);
4108
+ const keyStr = stringify(key);
4109
+ this.context.memoryLimit.use(str.length + keyStr.length);
4110
+ return hmacSha256(str, keyStr);
4111
+ }
4112
+
4113
+ var cryptoFilters = /*#__PURE__*/Object.freeze({
4114
+ __proto__: null,
4115
+ sha256: sha256$1,
4116
+ hmac_sha256: hmac_sha256
4117
+ });
4118
+
3721
4119
  const filters = {
3722
4120
  ...htmlFilters,
3723
4121
  ...mathFilters,
@@ -3725,6 +4123,8 @@ const filters = {
3725
4123
  ...arrayFilters,
3726
4124
  ...dateFilters,
3727
4125
  ...stringFilters,
4126
+ ...base64Filters,
4127
+ ...cryptoFilters,
3728
4128
  ...misc
3729
4129
  };
3730
4130
 
@@ -3782,8 +4182,8 @@ class ForTag extends Tag {
3782
4182
  return;
3783
4183
  }
3784
4184
  const continueKey = 'continue-' + this.variable + '-' + this.collection.getText();
3785
- ctx.push({ continue: ctx.getRegister(continueKey) });
3786
- const hash = yield this.hash.render(ctx);
4185
+ ctx.push(createScope({ continue: ctx.getRegister(continueKey, {}) }));
4186
+ const hash = (yield this.hash.render(ctx));
3787
4187
  ctx.pop();
3788
4188
  const modifiers = this.liquid.options.orderedFilterParameters
3789
4189
  ? Object.keys(hash).filter(x => MODIFIERS.includes(x))
@@ -3796,7 +4196,7 @@ class ForTag extends Tag {
3796
4196
  return reversed(collection);
3797
4197
  }, collection);
3798
4198
  ctx.setRegister(continueKey, (hash['offset'] || 0) + collection.length);
3799
- const scope = { forloop: new ForloopDrop(collection.length, this.collection.getText(), this.variable) };
4199
+ const scope = createScope({ forloop: new ForloopDrop(collection.length, this.collection.getText(), this.variable) });
3800
4200
  ctx.push(scope);
3801
4201
  for (const item of collection) {
3802
4202
  scope[this.variable] = item;
@@ -3922,12 +4322,12 @@ class CaseTag extends Tag {
3922
4322
  }
3923
4323
  *render(ctx, emitter) {
3924
4324
  const r = this.liquid.renderer;
3925
- const target = toValue(yield this.value.value(ctx, ctx.opts.lenientIf));
4325
+ const target = yield toValue(yield this.value.value(ctx, ctx.opts.lenientIf));
3926
4326
  let branchHit = false;
3927
4327
  for (const branch of this.branches) {
3928
4328
  for (const valueToken of branch.values) {
3929
4329
  const value = yield evalToken(valueToken, ctx, ctx.opts.lenientIf);
3930
- if (equals(target, value)) {
4330
+ if (yield equals(target, value)) {
3931
4331
  yield r.renderTemplates(branch.templates, ctx, emitter);
3932
4332
  branchHit = true;
3933
4333
  break;
@@ -3988,7 +4388,11 @@ class RenderTag extends Tag {
3988
4388
  alias = tokenizer.readIdentifier();
3989
4389
  else
3990
4390
  tokenizer.p = beforeAs;
3991
- this[keyword.content] = { value, alias: alias && alias.content };
4391
+ const binding = { value, alias: alias && alias.content };
4392
+ if (keyword.content === 'with')
4393
+ this.with = binding;
4394
+ else
4395
+ this.forBinding = binding;
3992
4396
  tokenizer.skipBlank();
3993
4397
  if (tokenizer.peek() === ',')
3994
4398
  tokenizer.advance();
@@ -4006,42 +4410,42 @@ class RenderTag extends Tag {
4006
4410
  }
4007
4411
  *render(ctx, emitter) {
4008
4412
  const { liquid, hash } = this;
4009
- const filepath = (yield renderFilePath(this['file'], ctx, liquid));
4413
+ const filepath = (yield renderFilePath(this.file, ctx, liquid));
4010
4414
  assert(filepath, () => `illegal file path "${filepath}"`);
4011
4415
  const childCtx = ctx.spawn();
4012
4416
  const scope = childCtx.bottom();
4013
4417
  __assign(scope, yield hash.render(ctx));
4014
- if (this['with']) {
4015
- const { value, alias } = this['with'];
4418
+ if (this.with) {
4419
+ const { value, alias } = this.with;
4016
4420
  scope[alias || filepath] = yield evalToken(value, ctx);
4017
4421
  }
4018
- if (this['for']) {
4019
- const { value, alias } = this['for'];
4422
+ if (this.forBinding) {
4423
+ const { value, alias } = this.forBinding;
4020
4424
  const collection = toEnumerable(yield evalToken(value, ctx));
4021
4425
  scope['forloop'] = new ForloopDrop(collection.length, value.getText(), alias);
4022
4426
  for (const item of collection) {
4023
4427
  scope[alias] = item;
4024
- const templates = (yield liquid._parsePartialFile(filepath, childCtx.sync, this['currentFile']));
4428
+ const templates = (yield liquid._parsePartialFile(filepath, childCtx.sync, this.currentFile));
4025
4429
  yield liquid.renderer.renderTemplates(templates, childCtx, emitter);
4026
4430
  scope['forloop'].next();
4027
4431
  }
4028
4432
  }
4029
4433
  else {
4030
- const templates = (yield liquid._parsePartialFile(filepath, childCtx.sync, this['currentFile']));
4434
+ const templates = (yield liquid._parsePartialFile(filepath, childCtx.sync, this.currentFile));
4031
4435
  yield liquid.renderer.renderTemplates(templates, childCtx, emitter);
4032
4436
  }
4033
4437
  }
4034
4438
  *children(partials, sync) {
4035
- if (partials && isString(this['file'])) {
4036
- return (yield this.liquid._parsePartialFile(this['file'], sync, this['currentFile']));
4439
+ if (partials && isString(this.file)) {
4440
+ return (yield this.liquid._parsePartialFile(this.file, sync, this.currentFile));
4037
4441
  }
4038
4442
  return [];
4039
4443
  }
4040
4444
  partialScope() {
4041
- if (isString(this['file'])) {
4445
+ if (isString(this.file)) {
4042
4446
  const names = Object.keys(this.hash.hash);
4043
- if (this['with']) {
4044
- const { value, alias } = this['with'];
4447
+ if (this.with) {
4448
+ const { value, alias } = this.with;
4045
4449
  if (isString(alias)) {
4046
4450
  names.push([alias, value]);
4047
4451
  }
@@ -4049,8 +4453,8 @@ class RenderTag extends Tag {
4049
4453
  names.push([this.file, value]);
4050
4454
  }
4051
4455
  }
4052
- if (this['for']) {
4053
- const { value, alias } = this['for'];
4456
+ if (this.forBinding) {
4457
+ const { value, alias } = this.forBinding;
4054
4458
  if (isString(alias)) {
4055
4459
  names.push([alias, value]);
4056
4460
  }
@@ -4058,7 +4462,7 @@ class RenderTag extends Tag {
4058
4462
  names.push([this.file, value]);
4059
4463
  }
4060
4464
  }
4061
- return { name: this['file'], isolated: true, scope: names };
4465
+ return { name: this.file, isolated: true, scope: names };
4062
4466
  }
4063
4467
  }
4064
4468
  *arguments() {
@@ -4067,14 +4471,14 @@ class RenderTag extends Tag {
4067
4471
  yield v;
4068
4472
  }
4069
4473
  }
4070
- if (this['with']) {
4071
- const { value } = this['with'];
4474
+ if (this.with) {
4475
+ const { value } = this.with;
4072
4476
  if (isValueToken(value)) {
4073
4477
  yield value;
4074
4478
  }
4075
4479
  }
4076
- if (this['for']) {
4077
- const { value } = this['for'];
4480
+ if (this.forBinding) {
4481
+ const { value } = this.forBinding;
4078
4482
  if (isValueToken(value)) {
4079
4483
  yield value;
4080
4484
  }
@@ -4122,8 +4526,8 @@ class IncludeTag extends Tag {
4122
4526
  constructor(token, remainTokens, liquid, parser) {
4123
4527
  super(token, remainTokens, liquid);
4124
4528
  const { tokenizer } = token;
4125
- this['file'] = parseFilePath(tokenizer, this.liquid, parser);
4126
- this['currentFile'] = token.file;
4529
+ this.file = parseFilePath(tokenizer, this.liquid, parser);
4530
+ this.currentFile = token.file;
4127
4531
  const begin = tokenizer.p;
4128
4532
  const withStr = tokenizer.readIdentifier();
4129
4533
  if (withStr.content === 'with') {
@@ -4141,28 +4545,28 @@ class IncludeTag extends Tag {
4141
4545
  *render(ctx, emitter) {
4142
4546
  const { liquid, hash, withVar } = this;
4143
4547
  const { renderer } = liquid;
4144
- const filepath = (yield renderFilePath(this['file'], ctx, liquid));
4548
+ const filepath = (yield renderFilePath(this.file, ctx, liquid));
4145
4549
  assert(filepath, () => `illegal file path "${filepath}"`);
4146
4550
  const saved = ctx.saveRegister('blocks', 'blockMode');
4147
4551
  ctx.setRegister('blocks', {});
4148
4552
  ctx.setRegister('blockMode', BlockMode.OUTPUT);
4149
- const scope = (yield hash.render(ctx));
4553
+ const scope = createScope((yield hash.render(ctx)));
4150
4554
  if (withVar)
4151
4555
  scope[filepath] = yield evalToken(withVar, ctx);
4152
- const templates = (yield liquid._parsePartialFile(filepath, ctx.sync, this['currentFile']));
4153
- ctx.push(ctx.opts.jekyllInclude ? { include: scope } : scope);
4556
+ const templates = (yield liquid._parsePartialFile(filepath, ctx.sync, this.currentFile));
4557
+ ctx.push(ctx.opts.jekyllInclude ? createScope({ include: scope }) : scope);
4154
4558
  yield renderer.renderTemplates(templates, ctx, emitter);
4155
4559
  ctx.pop();
4156
4560
  ctx.restoreRegister(saved);
4157
4561
  }
4158
4562
  *children(partials, sync) {
4159
- if (partials && isString(this['file'])) {
4160
- return (yield this.liquid._parsePartialFile(this['file'], sync, this['currentFile']));
4563
+ if (partials && isString(this.file)) {
4564
+ return (yield this.liquid._parsePartialFile(this.file, sync, this.currentFile));
4161
4565
  }
4162
4566
  return [];
4163
4567
  }
4164
4568
  partialScope() {
4165
- if (isString(this['file'])) {
4569
+ if (isString(this.file)) {
4166
4570
  let names;
4167
4571
  if (this.liquid.options.jekyllInclude) {
4168
4572
  names = ['include'];
@@ -4170,16 +4574,16 @@ class IncludeTag extends Tag {
4170
4574
  else {
4171
4575
  names = Object.keys(this.hash.hash);
4172
4576
  if (this.withVar) {
4173
- names.push([this['file'], this.withVar]);
4577
+ names.push([this.file, this.withVar]);
4174
4578
  }
4175
4579
  }
4176
- return { name: this['file'], isolated: false, scope: names };
4580
+ return { name: this.file, isolated: false, scope: names };
4177
4581
  }
4178
4582
  }
4179
4583
  *arguments() {
4180
4584
  yield* Object.values(this.hash.hash).filter(isValueToken);
4181
- if (isValueToken(this['file'])) {
4182
- yield this['file'];
4585
+ if (isValueToken(this.file)) {
4586
+ yield this.file;
4183
4587
  }
4184
4588
  if (isValueToken(this.withVar)) {
4185
4589
  yield this.withVar;
@@ -4209,6 +4613,7 @@ class CycleTag extends Tag {
4209
4613
  constructor(token, remainTokens, liquid) {
4210
4614
  super(token, remainTokens, liquid);
4211
4615
  this.candidates = [];
4616
+ const begin = this.tokenizer.p;
4212
4617
  const group = this.tokenizer.readValue();
4213
4618
  this.tokenizer.skipBlank();
4214
4619
  if (group) {
@@ -4217,28 +4622,32 @@ class CycleTag extends Tag {
4217
4622
  this.tokenizer.advance();
4218
4623
  }
4219
4624
  else
4220
- this.candidates.push(group);
4625
+ this.tokenizer.p = begin;
4221
4626
  }
4222
4627
  while (!this.tokenizer.end()) {
4223
- const value = this.tokenizer.readValue();
4224
- if (value)
4225
- this.candidates.push(value);
4628
+ const value = this.tokenizer.readFilteredValue();
4629
+ this.candidates.push(value);
4226
4630
  this.tokenizer.readTo(',');
4227
4631
  }
4228
4632
  this.tokenizer.assert(this.candidates.length, () => `empty candidates: "${token.getText()}"`);
4229
4633
  }
4230
4634
  *render(ctx, emitter) {
4231
4635
  const group = (yield evalToken(this.group, ctx));
4232
- const fingerprint = `cycle:${group}:` + this.candidates.join(',');
4233
- const groups = ctx.getRegister('cycle');
4636
+ const fingerprint = `cycle:${group}:` + this.candidates.map(c => c.getText()).join(',');
4637
+ const groups = ctx.getRegister('cycle', {});
4234
4638
  let idx = groups[fingerprint];
4235
4639
  if (idx === undefined) {
4236
4640
  idx = groups[fingerprint] = 0;
4237
4641
  }
4238
- const candidate = this.candidates[idx];
4642
+ const candidate = new Value(this.candidates[idx], this.liquid);
4239
4643
  idx = (idx + 1) % this.candidates.length;
4240
4644
  groups[fingerprint] = idx;
4241
- return yield evalToken(candidate, ctx);
4645
+ let val = yield toValue(yield candidate.value(ctx, false));
4646
+ const outputEscape = this.liquid.options.outputEscape;
4647
+ if (outputEscape && !candidate.filters[candidate.filters.length - 1]?.raw) {
4648
+ val = yield outputEscape.call({ context: ctx, token: this.token, liquid: this.liquid }, val);
4649
+ }
4650
+ return val;
4242
4651
  }
4243
4652
  *arguments() {
4244
4653
  yield* this.candidates;
@@ -4278,7 +4687,7 @@ class IfTag extends Tag {
4278
4687
  *render(ctx, emitter) {
4279
4688
  const r = this.liquid.renderer;
4280
4689
  for (const { value, templates } of this.branches) {
4281
- const v = yield value.value(ctx, ctx.opts.lenientIf);
4690
+ const v = yield toValue(yield value.value(ctx, ctx.opts.lenientIf));
4282
4691
  if (isTruthy(v, ctx)) {
4283
4692
  yield r.renderTemplates(templates, ctx, emitter);
4284
4693
  return;
@@ -4322,7 +4731,7 @@ class LayoutTag extends Tag {
4322
4731
  constructor(token, remainTokens, liquid, parser) {
4323
4732
  super(token, remainTokens, liquid);
4324
4733
  this.file = parseFilePath(this.tokenizer, this.liquid, parser);
4325
- this['currentFile'] = token.file;
4734
+ this.currentFile = token.file;
4326
4735
  this.args = new Hash(this.tokenizer, liquid.options.keyValueSeparator);
4327
4736
  this.templates = parser.parseTokens(remainTokens);
4328
4737
  }
@@ -4336,24 +4745,24 @@ class LayoutTag extends Tag {
4336
4745
  }
4337
4746
  const filepath = (yield renderFilePath(this.file, ctx, liquid));
4338
4747
  assert(filepath, () => `illegal file path "${filepath}"`);
4339
- const templates = (yield liquid._parseLayoutFile(filepath, ctx.sync, this['currentFile']));
4748
+ const templates = (yield liquid._parseLayoutFile(filepath, ctx.sync, this.currentFile));
4340
4749
  // render remaining contents and store rendered results
4341
4750
  ctx.setRegister('blockMode', BlockMode.STORE);
4342
4751
  const html = yield renderer.renderTemplates(this.templates, ctx);
4343
- const blocks = ctx.getRegister('blocks');
4752
+ const blocks = ctx.getRegister('blocks', {});
4344
4753
  // set whole content to anonymous block if anonymous doesn't specified
4345
4754
  if (blocks[''] === undefined)
4346
4755
  blocks[''] = (parent, emitter) => emitter.write(html);
4347
4756
  ctx.setRegister('blockMode', BlockMode.OUTPUT);
4348
4757
  // render the layout file use stored blocks
4349
- ctx.push((yield args.render(ctx)));
4758
+ ctx.push(createScope((yield args.render(ctx))));
4350
4759
  yield renderer.renderTemplates(templates, ctx, emitter);
4351
4760
  ctx.pop();
4352
4761
  }
4353
4762
  *children(partials) {
4354
4763
  const templates = this.templates.slice();
4355
4764
  if (partials && isString(this.file)) {
4356
- templates.push(...(yield this.liquid._parsePartialFile(this.file, true, this['currentFile'])));
4765
+ templates.push(...(yield this.liquid._parsePartialFile(this.file, true, this.currentFile)));
4357
4766
  }
4358
4767
  return templates;
4359
4768
  }
@@ -4392,23 +4801,28 @@ class BlockTag extends Tag {
4392
4801
  *render(ctx, emitter) {
4393
4802
  const blockRender = this.getBlockRender(ctx);
4394
4803
  if (ctx.getRegister('blockMode') === BlockMode.STORE) {
4395
- ctx.getRegister('blocks')[this.block] = blockRender;
4804
+ ctx.getRegister('blocks', {})[this.block] = blockRender;
4396
4805
  }
4397
4806
  else {
4398
4807
  yield blockRender(new BlockDrop(), emitter);
4399
4808
  }
4400
4809
  }
4401
4810
  getBlockRender(ctx) {
4811
+ const self = this;
4402
4812
  const { liquid, templates } = this;
4403
- const renderChild = ctx.getRegister('blocks')[this.block];
4813
+ const renderChild = ctx.getRegister('blocks', {})[this.block];
4404
4814
  const renderCurrent = function* (superBlock, emitter) {
4405
- // add {{ block.super }} support when rendering
4406
- ctx.push({ block: superBlock });
4815
+ const stack = ctx.getRegister('blockStack', []);
4816
+ if (stack.includes(self))
4817
+ throw new Error('block tag cannot be nested');
4818
+ stack.push(self);
4819
+ ctx.push(createScope({ block: superBlock }));
4407
4820
  yield liquid.renderer.renderTemplates(templates, ctx, emitter);
4408
4821
  ctx.pop();
4822
+ stack.pop();
4409
4823
  };
4410
4824
  return renderChild
4411
- ? (superBlock, emitter) => renderChild(new BlockDrop(() => renderCurrent(superBlock, emitter)), emitter)
4825
+ ? (superBlock, emitter) => renderChild(new BlockDrop((emitter) => renderCurrent(superBlock, emitter)), emitter)
4412
4826
  : renderCurrent;
4413
4827
  }
4414
4828
  *children() {
@@ -4492,7 +4906,7 @@ class TablerowTag extends Tag {
4492
4906
  const cols = args.cols || collection.length;
4493
4907
  const r = this.liquid.renderer;
4494
4908
  const tablerowloop = new TablerowloopDrop(collection.length, cols, this.collection.getText(), this.variable);
4495
- const scope = { tablerowloop };
4909
+ const scope = createScope({ tablerowloop });
4496
4910
  ctx.push(scope);
4497
4911
  for (let idx = 0; idx < collection.length; idx++, tablerowloop.next()) {
4498
4912
  scope[this.variable] = collection[idx];
@@ -4565,7 +4979,7 @@ class UnlessTag extends Tag {
4565
4979
  *render(ctx, emitter) {
4566
4980
  const r = this.liquid.renderer;
4567
4981
  for (const { value, test, templates } of this.branches) {
4568
- const v = yield value.value(ctx, ctx.opts.lenientIf);
4982
+ const v = yield toValue(yield value.value(ctx, ctx.opts.lenientIf));
4569
4983
  if (test(v, ctx)) {
4570
4984
  yield r.renderTemplates(templates, ctx, emitter);
4571
4985
  return;
@@ -4608,7 +5022,11 @@ class EchoTag extends Tag {
4608
5022
  *render(ctx, emitter) {
4609
5023
  if (!this.value)
4610
5024
  return;
4611
- const val = yield this.value.value(ctx, false);
5025
+ let val = yield toValue(yield this.value.value(ctx, false));
5026
+ const outputEscape = this.liquid.options.outputEscape;
5027
+ if (outputEscape && !this.value.filters[this.value.filters.length - 1]?.raw) {
5028
+ val = yield outputEscape.call({ context: ctx, token: this.token, liquid: this.liquid }, val);
5029
+ }
4612
5030
  emitter.write(val);
4613
5031
  }
4614
5032
  *arguments() {
@@ -4669,8 +5087,8 @@ const tags = {
4669
5087
  class Liquid {
4670
5088
  constructor(opts = {}) {
4671
5089
  this.renderer = new Render();
4672
- this.filters = {};
4673
- this.tags = {};
5090
+ this.filters = Object.create(null);
5091
+ this.tags = Object.create(null);
4674
5092
  this.options = normalize(opts);
4675
5093
  // eslint-disable-next-line deprecation/deprecation
4676
5094
  this.parser = new Parser(this);
@@ -4682,7 +5100,7 @@ class Liquid {
4682
5100
  return parser.parse(html, filepath);
4683
5101
  }
4684
5102
  _render(tpl, scope, renderOptions) {
4685
- const ctx = scope instanceof Context ? scope : new Context(scope, this.options, renderOptions);
5103
+ const ctx = scope instanceof Context ? scope : new Context(scope, this.options, renderOptions, { liquid: this });
4686
5104
  return this.renderer.renderTemplates(tpl, ctx);
4687
5105
  }
4688
5106
  async render(tpl, scope, renderOptions) {
@@ -4692,7 +5110,7 @@ class Liquid {
4692
5110
  return toValueSync(this._render(tpl, scope, { ...renderOptions, sync: true }));
4693
5111
  }
4694
5112
  renderToNodeStream(tpl, scope, renderOptions = {}) {
4695
- const ctx = new Context(scope, this.options, renderOptions);
5113
+ const ctx = new Context(scope, this.options, renderOptions, { liquid: this });
4696
5114
  return this.renderer.renderTemplatesToNodeStream(tpl, ctx);
4697
5115
  }
4698
5116
  _parseAndRender(html, scope, renderOptions) {
@@ -4706,10 +5124,10 @@ class Liquid {
4706
5124
  return toValueSync(this._parseAndRender(html, scope, { ...renderOptions, sync: true }));
4707
5125
  }
4708
5126
  _parsePartialFile(file, sync, currentFile) {
4709
- return new Parser(this).parseFile(file, sync, LookupType.Partials, currentFile);
5127
+ return new Parser(this).parseFile(file, sync, exports.LookupType.Partials, currentFile);
4710
5128
  }
4711
5129
  _parseLayoutFile(file, sync, currentFile) {
4712
- return new Parser(this).parseFile(file, sync, LookupType.Layouts, currentFile);
5130
+ return new Parser(this).parseFile(file, sync, exports.LookupType.Layouts, currentFile);
4713
5131
  }
4714
5132
  _parseFile(file, sync, lookupType, currentFile) {
4715
5133
  return new Parser(this).parseFile(file, sync, lookupType, currentFile);
@@ -4736,7 +5154,7 @@ class Liquid {
4736
5154
  }
4737
5155
  _evalValue(str, scope) {
4738
5156
  const value = new Value(str, this);
4739
- const ctx = scope instanceof Context ? scope : new Context(scope, this.options);
5157
+ const ctx = scope instanceof Context ? scope : new Context(scope, this.options, {}, { liquid: this });
4740
5158
  return value.value(ctx);
4741
5159
  }
4742
5160
  async evalValue(str, scope) {
@@ -4748,6 +5166,9 @@ class Liquid {
4748
5166
  registerFilter(name, filter) {
4749
5167
  this.filters[name] = filter;
4750
5168
  }
5169
+ unregisterFilter(name) {
5170
+ delete this.filters[name];
5171
+ }
4751
5172
  registerTag(name, tag) {
4752
5173
  this.tags[name] = isFunction(tag) ? tag : createTagClass(tag);
4753
5174
  }
@@ -4843,7 +5264,7 @@ class Liquid {
4843
5264
  }
4844
5265
 
4845
5266
  /* istanbul ignore file */
4846
- const version = '10.20.1';
5267
+ const version = '10.30.1';
4847
5268
 
4848
5269
  exports.AssertionError = AssertionError;
4849
5270
  exports.AssignTag = AssignTag;
@@ -4896,6 +5317,7 @@ exports.defaultOperators = defaultOperators;
4896
5317
  exports.defaultOptions = defaultOptions;
4897
5318
  exports.evalQuotedToken = evalQuotedToken;
4898
5319
  exports.evalToken = evalToken;
5320
+ exports.evalTokenValue = evalTokenValue;
4899
5321
  exports.filters = filters;
4900
5322
  exports.isFalsy = isFalsy;
4901
5323
  exports.isTruthy = isTruthy;