@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,12 +1,13 @@
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
  import { PassThrough } from 'stream';
7
7
  import { sep, extname, resolve as resolve$1, dirname as dirname$1 } from 'path';
8
- import { statSync, readFileSync as readFileSync$1, stat, readFile as readFile$1 } from 'fs';
8
+ import { statSync, readFileSync as readFileSync$1, realpathSync, stat, readFile as readFile$1, realpath } from 'fs';
9
9
  import { createRequire } from 'module';
10
+ import { createHash, createHmac } from 'crypto';
10
11
 
11
12
  class Token {
12
13
  constructor(kind, input, begin, end, file) {
@@ -37,7 +38,7 @@ class Token {
37
38
  }
38
39
 
39
40
  class Drop {
40
- liquidMethodMissing(key) {
41
+ liquidMethodMissing(key, context) {
41
42
  return undefined;
42
43
  }
43
44
  }
@@ -58,9 +59,6 @@ function isPromise(val) {
58
59
  function isIterator(val) {
59
60
  return val && isFunction(val.next) && isFunction(val.throw) && isFunction(val.return);
60
61
  }
61
- function escapeRegex(str) {
62
- return str.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
63
- }
64
62
  function promisify(fn) {
65
63
  return function (...args) {
66
64
  return new Promise((resolve, reject) => {
@@ -80,6 +78,25 @@ function stringify(value) {
80
78
  return value.map(x => stringify(x)).join('');
81
79
  return String(value);
82
80
  }
81
+ function advanceCodePoints(str, from, count) {
82
+ let index = from;
83
+ for (let i = 0; i < count && index < str.length; i++)
84
+ index += str.codePointAt(index) > 0xFFFF ? 2 : 1;
85
+ return index;
86
+ }
87
+ function codePointLength(str) {
88
+ let length = 0;
89
+ for (let index = 0; index < str.length; index = advanceCodePoints(str, index, 1))
90
+ length++;
91
+ return length;
92
+ }
93
+ function readArrayElement(arr, index, ownPropertyOnly) {
94
+ if (index < 0)
95
+ index = arr.length + index;
96
+ if (ownPropertyOnly && !hasOwnProperty.call(arr, index))
97
+ return undefined;
98
+ return arr[index];
99
+ }
83
100
  function toEnumerable(val) {
84
101
  val = toValue(val);
85
102
  if (isArray(val))
@@ -104,8 +121,7 @@ function toValue(value) {
104
121
  return (value instanceof Drop && isFunction(value.valueOf)) ? value.valueOf() : value;
105
122
  }
106
123
  function toNumber(value) {
107
- value = Number(value);
108
- return isNaN(value) ? 0 : value;
124
+ return +toValue(value) || 0;
109
125
  }
110
126
  function isNumber(value) {
111
127
  return typeof value === 'number';
@@ -149,9 +165,6 @@ function forOwn(obj, iteratee) {
149
165
  }
150
166
  return obj;
151
167
  }
152
- function last(arr) {
153
- return arr[arr.length - 1];
154
- }
155
168
  /*
156
169
  * Checks if value is the language type of Object.
157
170
  * (e.g. arrays, functions, objects, regexes, new Number(0), and new String(''))
@@ -177,10 +190,10 @@ function padEnd(str, length, ch = ' ') {
177
190
  }
178
191
  function pad(str, length, ch, add) {
179
192
  str = String(str);
180
- let n = length - str.length;
181
- while (n-- > 0)
182
- str = add(str, ch);
183
- return str;
193
+ const n = length - str.length;
194
+ if (n <= 0)
195
+ return str;
196
+ return add(str, ch.repeat(n));
184
197
  }
185
198
  function identify(val) {
186
199
  return val;
@@ -192,13 +205,26 @@ function changeCase(str) {
192
205
  function ellipsis(str, N) {
193
206
  return str.length > N ? str.slice(0, N - 3) + '...' : str;
194
207
  }
208
+ function orderedCompare(a, b) {
209
+ if (isNil(a) && isNil(b))
210
+ return 0;
211
+ if (isNil(a))
212
+ return 1;
213
+ if (isNil(b))
214
+ return -1;
215
+ if (a < b)
216
+ return -1;
217
+ if (a > b)
218
+ return 1;
219
+ return 0;
220
+ }
195
221
  // compare string in case-insensitive way, undefined values to the tail
196
222
  function caseInsensitiveCompare(a, b) {
197
- if (a == null && b == null)
223
+ if (isNil(a) && isNil(b))
198
224
  return 0;
199
- if (a == null)
225
+ if (isNil(a))
200
226
  return 1;
201
- if (b == null)
227
+ if (isNil(b))
202
228
  return -1;
203
229
  a = toLowerCase.call(a);
204
230
  b = toLowerCase.call(b);
@@ -211,8 +237,8 @@ function caseInsensitiveCompare(a, b) {
211
237
  function argumentsToValue(fn) {
212
238
  return function (...args) { return fn.call(this, ...args.map(toValue)); };
213
239
  }
214
- function escapeRegExp(text) {
215
- return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
240
+ function argumentsToNumber(fn) {
241
+ return function (...args) { return fn.call(this, ...args.map(toNumber)); };
216
242
  }
217
243
  /** Return an array containing unique elements from _array_. Works with nested arrays and objects. */
218
244
  function* strictUniq(array) {
@@ -378,8 +404,8 @@ function assertEmpty(predicate, message = `unexpected ${JSON.stringify(predicate
378
404
  }
379
405
 
380
406
  class NullDrop extends Drop {
381
- equals(value) {
382
- return isNil(toValue(value));
407
+ *equals(value) {
408
+ return isNil(yield toValue(value));
383
409
  }
384
410
  gt() {
385
411
  return false;
@@ -399,10 +425,10 @@ class NullDrop extends Drop {
399
425
  }
400
426
 
401
427
  class EmptyDrop extends Drop {
402
- equals(value) {
428
+ *equals(value) {
403
429
  if (value instanceof EmptyDrop)
404
430
  return false;
405
- value = toValue(value);
431
+ value = yield toValue(value);
406
432
  if (isString(value) || isArray(value))
407
433
  return value.length === 0;
408
434
  if (isObject(value))
@@ -430,14 +456,14 @@ class EmptyDrop extends Drop {
430
456
  }
431
457
 
432
458
  class BlankDrop extends EmptyDrop {
433
- equals(value) {
459
+ *equals(value) {
434
460
  if (value === false)
435
461
  return true;
436
- if (isNil(toValue(value)))
462
+ if (isNil(yield toValue(value)))
437
463
  return true;
438
464
  if (isString(value))
439
465
  return /^\s*$/.test(value);
440
- return super.equals(value);
466
+ return yield super.equals(value);
441
467
  }
442
468
  static is(value) {
443
469
  return value instanceof BlankDrop;
@@ -477,6 +503,50 @@ class ForloopDrop extends Drop {
477
503
  }
478
504
  }
479
505
 
506
+ class SimpleEmitter {
507
+ constructor() {
508
+ this.buffer = '';
509
+ }
510
+ write(html) {
511
+ this.buffer += stringify(html);
512
+ }
513
+ }
514
+
515
+ class StreamedEmitter {
516
+ constructor() {
517
+ this.buffer = '';
518
+ this.stream = new PassThrough();
519
+ }
520
+ write(html) {
521
+ this.stream.write(stringify(html));
522
+ }
523
+ error(err) {
524
+ this.stream.emit('error', err);
525
+ }
526
+ end() {
527
+ this.stream.end();
528
+ }
529
+ }
530
+
531
+ class KeepingTypeEmitter {
532
+ constructor() {
533
+ this.buffer = '';
534
+ }
535
+ write(html) {
536
+ html = toValue(html);
537
+ // This will only preserve the type if the value is isolated.
538
+ // I.E:
539
+ // {{ my-port }} -> 42
540
+ // {{ my-host }}:{{ my-port }} -> 'host:42'
541
+ if (typeof html !== 'string' && this.buffer === '') {
542
+ this.buffer = html;
543
+ }
544
+ else {
545
+ this.buffer = stringify(this.buffer) + stringify(html);
546
+ }
547
+ }
548
+ }
549
+
480
550
  class BlockDrop extends Drop {
481
551
  constructor(
482
552
  // the block render from layout template
@@ -488,8 +558,10 @@ class BlockDrop extends Drop {
488
558
  * Provide parent access in child block by
489
559
  * {{ block.super }}
490
560
  */
491
- super() {
492
- return this.superBlockRender();
561
+ *super() {
562
+ const emitter = new SimpleEmitter();
563
+ yield this.superBlockRender(emitter);
564
+ return emitter.buffer;
493
565
  }
494
566
  }
495
567
 
@@ -512,7 +584,16 @@ const literalValues = {
512
584
  'blank': new BlankDrop()
513
585
  };
514
586
 
587
+ // Tries are built once per input object and reused: the Tokenizer rebuilds them
588
+ // on every instantiation, but `input` (operators/literalValues) is a stable
589
+ // reference. WeakMap-keying by `input` lets short-lived operator objects (and
590
+ // their tries) be garbage collected. The returned trie is treated as read-only
591
+ // by callers (matchTrie only reads it); do not mutate it.
592
+ const trieCache = new WeakMap();
515
593
  function createTrie(input) {
594
+ const cached = trieCache.get(input);
595
+ if (cached)
596
+ return cached;
516
597
  const trie = {};
517
598
  for (const [name, data] of Object.entries(input)) {
518
599
  let node = trie;
@@ -527,6 +608,7 @@ function createTrie(input) {
527
608
  node.data = data;
528
609
  node.end = true;
529
610
  }
611
+ trieCache.set(input, trie);
530
612
  return trie;
531
613
  }
532
614
 
@@ -566,6 +648,12 @@ function __awaiter(thisArg, _arguments, P, generator) {
566
648
  });
567
649
  }
568
650
 
651
+ function toLiquidAsync(asyncFn, syncFn) {
652
+ const syncImpl = syncFn || asyncFn;
653
+ return (sync, ...args) => {
654
+ return sync ? syncImpl(...args) : asyncFn(...args);
655
+ };
656
+ }
569
657
  // convert an async iterator to a Promise
570
658
  function toPromise(val) {
571
659
  return __awaiter(this, void 0, void 0, function* () {
@@ -576,7 +664,7 @@ function toPromise(val) {
576
664
  let next = 'next';
577
665
  do {
578
666
  const state = val[next](value);
579
- done = state.done;
667
+ done = !!state.done;
580
668
  value = state.value;
581
669
  next = 'next';
582
670
  try {
@@ -602,7 +690,7 @@ function toValueSync(val) {
602
690
  let next = 'next';
603
691
  do {
604
692
  const state = val[next](value);
605
- done = state.done;
693
+ done = !!state.done;
606
694
  value = state.value;
607
695
  next = 'next';
608
696
  if (isIterator(value)) {
@@ -632,12 +720,9 @@ function getDayOfYear(d) {
632
720
  return num + d.getDate();
633
721
  }
634
722
  function getWeekOfYear(d, startDay) {
635
- // Skip to startDay of this week
636
- const now = getDayOfYear(d) + (startDay - d.getDay());
637
- // Find the first startDay of the year
638
- const jan1 = new Date(d.getFullYear(), 0, 1);
639
- const then = (7 - jan1.getDay() + startDay);
640
- return String(Math.floor((now - then) / 7) + 1);
723
+ // Days before the first startDay of the year belong to week 0
724
+ const daysSinceStartDay = (d.getDay() - startDay + 7) % 7;
725
+ return String(Math.floor((getDayOfYear(d) - 1 - daysSinceStartDay + 7) / 7));
641
726
  }
642
727
  function isLeapYear(d) {
643
728
  const year = d.getFullYear();
@@ -655,10 +740,11 @@ function ordinal(d) {
655
740
  }
656
741
  }
657
742
  function century(d) {
658
- return parseInt(d.getFullYear().toString().substring(0, 2), 10);
743
+ return Math.floor(d.getFullYear() / 100);
659
744
  }
660
745
  // default to 0
661
746
  const padWidths = {
747
+ C: 2,
662
748
  d: 2,
663
749
  e: 2,
664
750
  H: 2,
@@ -671,7 +757,9 @@ const padWidths = {
671
757
  M: 2,
672
758
  S: 2,
673
759
  U: 2,
674
- W: 2
760
+ W: 2,
761
+ y: 2,
762
+ Y: 4
675
763
  };
676
764
  const padSpaceChars = new Set('aAbBceklpP');
677
765
  function getTimezoneOffset(d, opts) {
@@ -701,14 +789,16 @@ const formatCodes = {
701
789
  m: (d) => d.getMonth() + 1,
702
790
  M: (d) => d.getMinutes(),
703
791
  N: (d, opts) => {
792
+ var _a;
704
793
  const width = Number(opts.width) || 9;
705
- const str = String(d.getMilliseconds()).slice(0, width);
794
+ const str = padStart(String(d.getMilliseconds()), 3, '0').slice(0, width);
795
+ (_a = opts.memoryLimit) === null || _a === void 0 ? void 0 : _a.use(width - str.length);
706
796
  return padEnd(str, width, '0');
707
797
  },
708
798
  p: (d) => (d.getHours() < 12 ? 'AM' : 'PM'),
709
799
  P: (d) => (d.getHours() < 12 ? 'am' : 'pm'),
710
800
  q: (d) => ordinal(d),
711
- s: (d) => Math.round(d.getTime() / 1000),
801
+ s: (d) => Math.floor(d.dateValue() / 1000),
712
802
  S: (d) => d.getSeconds(),
713
803
  u: (d) => d.getDay() || 7,
714
804
  U: (d) => getWeekOfYear(d, 0),
@@ -716,7 +806,7 @@ const formatCodes = {
716
806
  W: (d) => getWeekOfYear(d, 1),
717
807
  x: (d) => d.toLocaleDateString(),
718
808
  X: (d) => d.toLocaleTimeString(),
719
- y: (d) => d.getFullYear().toString().slice(2, 4),
809
+ y: (d) => (d.getFullYear() % 100 + 100) % 100,
720
810
  Y: (d) => d.getFullYear(),
721
811
  z: getTimezoneOffset,
722
812
  Z: (d, opts) => d.getTimeZoneName() || getTimezoneOffset(d, opts),
@@ -725,18 +815,18 @@ const formatCodes = {
725
815
  '%': () => '%'
726
816
  };
727
817
  formatCodes.h = formatCodes.b;
728
- function strftime(d, formatStr) {
818
+ function strftime(d, formatStr, memoryLimit) {
729
819
  let output = '';
730
820
  let remaining = formatStr;
731
821
  let match;
732
822
  while ((match = rFormat.exec(remaining))) {
733
823
  output += remaining.slice(0, match.index);
734
824
  remaining = remaining.slice(match.index + match[0].length);
735
- output += format(d, match);
825
+ output += format(d, match, memoryLimit);
736
826
  }
737
827
  return output + remaining;
738
828
  }
739
- function format(d, match) {
829
+ function format(d, match, memoryLimit) {
740
830
  const [input, flagStr = '', width, modifier, conversion] = match;
741
831
  const convert = formatCodes[conversion];
742
832
  if (!convert)
@@ -744,9 +834,9 @@ function format(d, match) {
744
834
  const flags = {};
745
835
  for (const flag of flagStr)
746
836
  flags[flag] = true;
747
- let ret = String(convert(d, { flags, width, modifier }));
837
+ let ret = String(convert(d, { flags, width, modifier, memoryLimit }));
748
838
  let padChar = padSpaceChars.has(conversion) ? ' ' : '0';
749
- let padWidth = width || padWidths[conversion] || 0;
839
+ let padWidth = Number(width) || padWidths[conversion] || 0;
750
840
  if (flags['^'])
751
841
  ret = ret.toUpperCase();
752
842
  else if (flags['#'])
@@ -757,6 +847,9 @@ function format(d, match) {
757
847
  padChar = '0';
758
848
  if (flags['-'])
759
849
  padWidth = 0;
850
+ memoryLimit === null || memoryLimit === void 0 ? void 0 : memoryLimit.use(Number(padWidth) - ret.length);
851
+ if (padChar === '0' && ret[0] === '-')
852
+ return '-' + padStart(ret.slice(1), padWidth - 1, padChar);
760
853
  return padStart(ret, padWidth, padChar);
761
854
  }
762
855
 
@@ -805,6 +898,15 @@ class LiquidDate {
805
898
  getTime() {
806
899
  return this.displayDate.getTime();
807
900
  }
901
+ /**
902
+ * The underlying UTC timestamp in milliseconds, unaffected by the display
903
+ * timezone. Use this (not `getTime()`) for timezone-invariant values like
904
+ * `%s`: `getTime()` reads `displayDate`, which is deliberately shifted by
905
+ * the display timezone offset so wall-clock getters can delegate to Date.
906
+ */
907
+ dateValue() {
908
+ return this.date.getTime();
909
+ }
808
910
  getMilliseconds() {
809
911
  return this.displayDate.getMilliseconds();
810
912
  }
@@ -916,13 +1018,15 @@ class Limiter {
916
1018
  this.limit = limit;
917
1019
  }
918
1020
  use(count) {
919
- count = toNumber(count);
920
- assert(this.base + count <= this.limit, this.message);
921
- this.base += count;
1021
+ if (+count > 0) {
1022
+ assert(this.base + +count <= this.limit, this.message);
1023
+ this.base += +count;
1024
+ }
922
1025
  }
923
1026
  check(count) {
924
- count = toNumber(count);
925
- assert(count <= this.limit, this.message);
1027
+ if (+count > 0) {
1028
+ assert(+count <= this.limit, this.message);
1029
+ }
926
1030
  }
927
1031
  }
928
1032
 
@@ -953,13 +1057,11 @@ class TagToken extends DelimitedToken {
953
1057
  const { trimTagLeft, trimTagRight, tagDelimiterLeft, tagDelimiterRight } = options;
954
1058
  const [valueBegin, valueEnd] = [begin + tagDelimiterLeft.length, end - tagDelimiterRight.length];
955
1059
  super(TokenKind.Tag, [valueBegin, valueEnd], input, begin, end, trimTagLeft, trimTagRight, file);
956
- this.tokenizer = new Tokenizer(input, options.operators, file, this.contentRange);
1060
+ this.tokenizer = new Tokenizer(input, options.operators, file, this.contentRange, options.groupedExpressions);
957
1061
  this.name = this.tokenizer.readTagName();
958
1062
  this.tokenizer.assert(this.name, `illegal tag syntax, tag name expected`);
959
1063
  this.tokenizer.skipBlank();
960
- }
961
- get args() {
962
- return this.tokenizer.input.slice(this.tokenizer.p, this.contentRange[1]);
1064
+ this.args = this.tokenizer.input.slice(this.tokenizer.p, this.contentRange[1]);
963
1065
  }
964
1066
  }
965
1067
 
@@ -1054,8 +1156,7 @@ class OperatorToken extends Token {
1054
1156
  this.operator = this.getText();
1055
1157
  }
1056
1158
  getPrecedence() {
1057
- const key = this.getText();
1058
- return key in operatorPrecedences ? operatorPrecedences[key] : 1;
1159
+ return this.operator in operatorPrecedences ? operatorPrecedences[this.operator] : 1;
1059
1160
  }
1060
1161
  }
1061
1162
 
@@ -1169,7 +1270,7 @@ class RangeToken extends Token {
1169
1270
  class LiquidTagToken extends DelimitedToken {
1170
1271
  constructor(input, begin, end, options, file) {
1171
1272
  super(TokenKind.Tag, [begin, end], input, begin, end, false, false, file);
1172
- this.tokenizer = new Tokenizer(input, options.operators, file, this.contentRange);
1273
+ this.tokenizer = new Tokenizer(input, options.operators, file, this.contentRange, options.groupedExpressions);
1173
1274
  this.name = this.tokenizer.readTagName();
1174
1275
  this.tokenizer.assert(this.name, 'illegal liquid tag syntax');
1175
1276
  this.tokenizer.skipBlank();
@@ -1205,50 +1306,6 @@ function getPerformance() {
1205
1306
  polyfill;
1206
1307
  }
1207
1308
 
1208
- class SimpleEmitter {
1209
- constructor() {
1210
- this.buffer = '';
1211
- }
1212
- write(html) {
1213
- this.buffer += stringify(html);
1214
- }
1215
- }
1216
-
1217
- class StreamedEmitter {
1218
- constructor() {
1219
- this.buffer = '';
1220
- this.stream = new PassThrough();
1221
- }
1222
- write(html) {
1223
- this.stream.write(stringify(html));
1224
- }
1225
- error(err) {
1226
- this.stream.emit('error', err);
1227
- }
1228
- end() {
1229
- this.stream.end();
1230
- }
1231
- }
1232
-
1233
- class KeepingTypeEmitter {
1234
- constructor() {
1235
- this.buffer = '';
1236
- }
1237
- write(html) {
1238
- html = toValue(html);
1239
- // This will only preserve the type if the value is isolated.
1240
- // I.E:
1241
- // {{ my-port }} -> 42
1242
- // {{ my-host }}:{{ my-port }} -> 'host:42'
1243
- if (typeof html !== 'string' && this.buffer === '') {
1244
- this.buffer = html;
1245
- }
1246
- else {
1247
- this.buffer = stringify(this.buffer) + stringify(html);
1248
- }
1249
- }
1250
- }
1251
-
1252
1309
  class Render {
1253
1310
  renderTemplatesToNodeStream(templates, ctx) {
1254
1311
  const emitter = new StreamedEmitter();
@@ -1260,6 +1317,7 @@ class Render {
1260
1317
  if (!emitter) {
1261
1318
  emitter = ctx.opts.keepOutputType ? new KeepingTypeEmitter() : new SimpleEmitter();
1262
1319
  }
1320
+ ctx.renderLimit.check(getPerformance().now());
1263
1321
  const errors = [];
1264
1322
  for (const tpl of templates) {
1265
1323
  ctx.renderLimit.check(getPerformance().now());
@@ -1286,6 +1344,33 @@ class Render {
1286
1344
  }
1287
1345
  }
1288
1346
 
1347
+ function isKeyValuePair(arr) {
1348
+ return isArray(arr);
1349
+ }
1350
+
1351
+ class Filter {
1352
+ constructor(token, options, liquid) {
1353
+ this.token = token;
1354
+ this.name = token.name;
1355
+ this.handler = isFunction(options)
1356
+ ? options
1357
+ : (isFunction(options === null || options === void 0 ? void 0 : options.handler) ? options.handler : identify);
1358
+ this.raw = !isFunction(options) && !!(options === null || options === void 0 ? void 0 : options.raw);
1359
+ this.args = token.args;
1360
+ this.liquid = liquid;
1361
+ }
1362
+ *render(value, context) {
1363
+ const argv = [];
1364
+ for (const arg of this.args) {
1365
+ if (isKeyValuePair(arg))
1366
+ argv.push([arg[0], yield evalToken(arg[1], context)]);
1367
+ else
1368
+ argv.push(yield evalToken(arg, context));
1369
+ }
1370
+ return yield this.handler.apply({ context, token: this.token, liquid: this.liquid }, [value, ...argv]);
1371
+ }
1372
+ }
1373
+
1289
1374
  class Expression {
1290
1375
  constructor(tokens) {
1291
1376
  this.postfix = [...toPostfix(tokens)];
@@ -1316,6 +1401,11 @@ class Expression {
1316
1401
  return !!this.postfix.length;
1317
1402
  }
1318
1403
  }
1404
+ /**
1405
+ * Evaluate `token`, preserving Drops.
1406
+ * Operators use this so `Comparable` drops (`empty`, `nil`, `blank`) and custom drops
1407
+ * are compared as drops rather than as their `valueOf()` result.
1408
+ */
1319
1409
  function* evalToken(token, ctx, lenient = false) {
1320
1410
  if (!token)
1321
1411
  return;
@@ -1325,11 +1415,32 @@ function* evalToken(token, ctx, lenient = false) {
1325
1415
  return yield evalPropertyAccessToken(token, ctx, lenient);
1326
1416
  if (isRangeToken(token))
1327
1417
  return yield evalRangeToken(token, ctx);
1418
+ if (isFilteredValueToken(token))
1419
+ return yield evalFilteredValueToken(token, ctx, lenient);
1420
+ }
1421
+ function* evalFilteredValueToken(token, ctx, lenient) {
1422
+ assert(ctx.liquid, 'FilteredValueToken evaluation requires liquid instance in context');
1423
+ lenient = lenient || (ctx.opts.lenientIf && token.filters.length > 0 && token.filters[0].name === 'default');
1424
+ let val = yield token.initial.evaluate(ctx, lenient);
1425
+ for (const filterToken of token.filters) {
1426
+ const filterImpl = ctx.liquid.filters[filterToken.name];
1427
+ assert(filterImpl || !ctx.liquid.options.strictFilters, () => `undefined filter: ${filterToken.name}`);
1428
+ const filter = new Filter(filterToken, filterImpl, ctx.liquid);
1429
+ val = yield filter.render(val, ctx);
1430
+ }
1431
+ return val;
1432
+ }
1433
+ /**
1434
+ * Evaluate `token` to the value filters and tags consume.
1435
+ * Awaits a promise returned by `Drop.valueOf()`.
1436
+ */
1437
+ function* evalTokenValue(token, ctx, lenient = false) {
1438
+ return yield toValue(yield evalToken(token, ctx, lenient));
1328
1439
  }
1329
1440
  function* evalPropertyAccessToken(token, ctx, lenient) {
1330
1441
  const props = [];
1331
1442
  for (const prop of token.props) {
1332
- props.push((yield evalToken(prop, ctx, false)));
1443
+ props.push((yield evalTokenValue(prop, ctx, false)));
1333
1444
  }
1334
1445
  try {
1335
1446
  if (token.variable) {
@@ -1350,8 +1461,8 @@ function evalQuotedToken(token) {
1350
1461
  return token.content;
1351
1462
  }
1352
1463
  function* evalRangeToken(token, ctx) {
1353
- const low = yield evalToken(token.lhs, ctx);
1354
- const high = yield evalToken(token.rhs, ctx);
1464
+ const low = (yield evalTokenValue(token.lhs, ctx));
1465
+ const high = (yield evalTokenValue(token.rhs, ctx));
1355
1466
  ctx.memoryLimit.use(high - low + 1);
1356
1467
  return range(+low, +high + 1);
1357
1468
  }
@@ -1386,67 +1497,75 @@ function isFalsy(val, ctx) {
1386
1497
  }
1387
1498
 
1388
1499
  const defaultOperators = {
1389
- '==': equals,
1390
- '!=': (l, r) => !equals(l, r),
1391
- '>': (l, r) => {
1500
+ '==': function* (l, r) { return yield equals(l, r); },
1501
+ '!=': function* (l, r) { return !(yield equals(l, r)); },
1502
+ '>': function* (l, r) {
1392
1503
  if (isComparable(l))
1393
- return l.gt(r);
1504
+ return yield l.gt(r);
1394
1505
  if (isComparable(r))
1395
- return r.lt(l);
1396
- return toValue(l) > toValue(r);
1506
+ return yield r.lt(l);
1507
+ return (yield toValue(l)) > (yield toValue(r));
1397
1508
  },
1398
- '<': (l, r) => {
1509
+ '<': function* (l, r) {
1399
1510
  if (isComparable(l))
1400
- return l.lt(r);
1511
+ return yield l.lt(r);
1401
1512
  if (isComparable(r))
1402
- return r.gt(l);
1403
- return toValue(l) < toValue(r);
1513
+ return yield r.gt(l);
1514
+ return (yield toValue(l)) < (yield toValue(r));
1404
1515
  },
1405
- '>=': (l, r) => {
1516
+ '>=': function* (l, r) {
1406
1517
  if (isComparable(l))
1407
- return l.geq(r);
1518
+ return yield l.geq(r);
1408
1519
  if (isComparable(r))
1409
- return r.leq(l);
1410
- return toValue(l) >= toValue(r);
1520
+ return yield r.leq(l);
1521
+ return (yield toValue(l)) >= (yield toValue(r));
1411
1522
  },
1412
- '<=': (l, r) => {
1523
+ '<=': function* (l, r) {
1413
1524
  if (isComparable(l))
1414
- return l.leq(r);
1525
+ return yield l.leq(r);
1415
1526
  if (isComparable(r))
1416
- return r.geq(l);
1417
- return toValue(l) <= toValue(r);
1527
+ return yield r.geq(l);
1528
+ return (yield toValue(l)) <= (yield toValue(r));
1418
1529
  },
1419
- 'contains': (l, r) => {
1420
- l = toValue(l);
1530
+ 'contains': function* (l, r) {
1531
+ l = yield toValue(l);
1421
1532
  if (isArray(l))
1422
- return l.some((i) => equals(i, r));
1533
+ return yield arrayIncludes(l, r);
1423
1534
  if (isFunction(l === null || l === void 0 ? void 0 : l.indexOf))
1424
- return l.indexOf(toValue(r)) > -1;
1535
+ return l.indexOf(yield toValue(r)) > -1;
1425
1536
  return false;
1426
1537
  },
1427
- 'not': (v, ctx) => isFalsy(toValue(v), ctx),
1428
- 'and': (l, r, ctx) => isTruthy(toValue(l), ctx) && isTruthy(toValue(r), ctx),
1429
- 'or': (l, r, ctx) => isTruthy(toValue(l), ctx) || isTruthy(toValue(r), ctx)
1538
+ 'not': function* (v, ctx) { return isFalsy(yield toValue(v), ctx); },
1539
+ 'and': function* (l, r, ctx) { return isTruthy(yield toValue(l), ctx) && isTruthy(yield toValue(r), ctx); },
1540
+ 'or': function* (l, r, ctx) { return isTruthy(yield toValue(l), ctx) || isTruthy(yield toValue(r), ctx); }
1430
1541
  };
1431
- function equals(lhs, rhs) {
1542
+ function* equals(lhs, rhs) {
1432
1543
  if (isComparable(lhs))
1433
- return lhs.equals(rhs);
1544
+ return yield lhs.equals(rhs);
1434
1545
  if (isComparable(rhs))
1435
- return rhs.equals(lhs);
1436
- lhs = toValue(lhs);
1437
- rhs = toValue(rhs);
1546
+ return yield rhs.equals(lhs);
1547
+ lhs = yield toValue(lhs);
1548
+ rhs = yield toValue(rhs);
1438
1549
  if (isArray(lhs)) {
1439
- return isArray(rhs) && arrayEquals(lhs, rhs);
1550
+ return isArray(rhs) && (yield arrayEquals(lhs, rhs));
1440
1551
  }
1441
1552
  return lhs === rhs;
1442
1553
  }
1443
- function arrayEquals(lhs, rhs) {
1554
+ function* arrayEquals(lhs, rhs) {
1444
1555
  if (lhs.length !== rhs.length)
1445
1556
  return false;
1446
- return !lhs.some((value, i) => !equals(value, rhs[i]));
1557
+ for (let i = 0; i < lhs.length; i++) {
1558
+ if (!(yield equals(lhs[i], rhs[i])))
1559
+ return false;
1560
+ }
1561
+ return true;
1447
1562
  }
1448
- function arrayIncludes(arr, item) {
1449
- return arr.some(value => equals(value, item));
1563
+ function* arrayIncludes(arr, item) {
1564
+ for (const value of arr) {
1565
+ if (yield equals(value, item))
1566
+ return true;
1567
+ }
1568
+ return false;
1450
1569
  }
1451
1570
 
1452
1571
  class Node {
@@ -1554,10 +1673,30 @@ function fallback(file) {
1554
1673
  function dirname(filepath) {
1555
1674
  return dirname$1(filepath);
1556
1675
  }
1676
+ const realpathAsync = promisify(realpath);
1557
1677
  function contains(root, file) {
1558
- root = resolve$1(root);
1559
- root = root.endsWith(sep) ? root : root + sep;
1560
- return file.startsWith(root);
1678
+ return __awaiter(this, void 0, void 0, function* () {
1679
+ try {
1680
+ const realRoot = yield realpathAsync(root);
1681
+ const realFile = yield realpathAsync(file);
1682
+ const prefix = realRoot.endsWith(sep) ? realRoot : realRoot + sep;
1683
+ return realFile.startsWith(prefix);
1684
+ }
1685
+ catch (_a) {
1686
+ return false;
1687
+ }
1688
+ });
1689
+ }
1690
+ function containsSync(root, file) {
1691
+ try {
1692
+ const realRoot = realpathSync(root);
1693
+ const realFile = realpathSync(file);
1694
+ const prefix = realRoot.endsWith(sep) ? realRoot : realRoot + sep;
1695
+ return realFile.startsWith(prefix);
1696
+ }
1697
+ catch (_a) {
1698
+ return false;
1699
+ }
1561
1700
  }
1562
1701
 
1563
1702
  var fs = /*#__PURE__*/Object.freeze({
@@ -1570,9 +1709,24 @@ var fs = /*#__PURE__*/Object.freeze({
1570
1709
  fallback: fallback,
1571
1710
  dirname: dirname,
1572
1711
  contains: contains,
1712
+ containsSync: containsSync,
1573
1713
  sep: sep
1574
1714
  });
1575
1715
 
1716
+ function chargeJsonReplacerValue(memoryLimit, val) {
1717
+ if (typeof val === 'string') {
1718
+ memoryLimit.use(val.length);
1719
+ }
1720
+ else if (val === null || typeof val === 'number' || typeof val === 'boolean') {
1721
+ memoryLimit.use(JSON.stringify(val).length);
1722
+ }
1723
+ else if (Array.isArray(val)) {
1724
+ memoryLimit.use(val.length + 1);
1725
+ }
1726
+ else if (typeof val === 'object') {
1727
+ memoryLimit.use(2);
1728
+ }
1729
+ }
1576
1730
  function defaultFilter(value, defaultValue, ...args) {
1577
1731
  value = toValue(value);
1578
1732
  if (isArray(value) || isString(value))
@@ -1582,19 +1736,29 @@ function defaultFilter(value, defaultValue, ...args) {
1582
1736
  return isFalsy(value, this.context) ? defaultValue : value;
1583
1737
  }
1584
1738
  function json(value, space = 0) {
1585
- return JSON.stringify(value, null, space);
1739
+ const memoryLimit = this.context.memoryLimit;
1740
+ return JSON.stringify(value, (_key, val) => {
1741
+ chargeJsonReplacerValue(memoryLimit, val);
1742
+ return val;
1743
+ }, space);
1586
1744
  }
1587
1745
  function inspect(value, space = 0) {
1746
+ const memoryLimit = this.context.memoryLimit;
1588
1747
  const ancestors = [];
1589
1748
  return JSON.stringify(value, function (_key, value) {
1590
- if (typeof value !== 'object' || value === null)
1749
+ if (typeof value !== 'object' || value === null) {
1750
+ chargeJsonReplacerValue(memoryLimit, value);
1591
1751
  return value;
1752
+ }
1592
1753
  // `this` is the object that value is contained in, i.e., its direct parent.
1593
1754
  while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this)
1594
1755
  ancestors.pop();
1595
- if (ancestors.includes(value))
1756
+ if (ancestors.includes(value)) {
1757
+ memoryLimit.use('[Circular]'.length);
1596
1758
  return '[Circular]';
1759
+ }
1597
1760
  ancestors.push(value);
1761
+ chargeJsonReplacerValue(memoryLimit, value);
1598
1762
  return value;
1599
1763
  }, space);
1600
1764
  }
@@ -1649,10 +1813,33 @@ function newline_to_br(v) {
1649
1813
  this.context.memoryLimit.use(str.length);
1650
1814
  return str.replace(/\r?\n/gm, '<br />\n');
1651
1815
  }
1816
+ // Raw-text blocks (HTML5) plus '<...>' as the catch-all kind; a regex
1817
+ // equivalent is O(n^2) in V8 on unclosed openers.
1652
1818
  function strip_html(v) {
1653
1819
  const str = stringify(v);
1654
1820
  this.context.memoryLimit.use(str.length);
1655
- return str.replace(/<script[\s\S]*?<\/script>|<style[\s\S]*?<\/style>|<.*?>|<!--[\s\S]*?-->/g, '');
1821
+ const blocks = new Map([['<script', '</script>'], ['<style', '</style>'], ['<!--', '-->'], ['<', '>']]);
1822
+ let out = '';
1823
+ let i = 0;
1824
+ while (i < str.length) {
1825
+ const lt = str.indexOf('<', i);
1826
+ if (lt < 0)
1827
+ return out + str.slice(i);
1828
+ out += str.slice(i, lt);
1829
+ for (const [opener, closer] of blocks) {
1830
+ if (!str.startsWith(opener, lt))
1831
+ continue;
1832
+ const e = str.indexOf(closer, lt + opener.length);
1833
+ if (e >= 0) {
1834
+ i = e + closer.length;
1835
+ break;
1836
+ }
1837
+ blocks.delete(opener);
1838
+ }
1839
+ if (i <= lt)
1840
+ return out + str.slice(lt);
1841
+ }
1842
+ return out;
1656
1843
  }
1657
1844
 
1658
1845
  var htmlFilters = /*#__PURE__*/Object.freeze({
@@ -1743,6 +1930,7 @@ const defaultOptions = {
1743
1930
  globals: {},
1744
1931
  keepOutputType: false,
1745
1932
  operators: defaultOperators,
1933
+ groupedExpressions: false,
1746
1934
  memoryLimit: Infinity,
1747
1935
  parseLimit: Infinity,
1748
1936
  renderLimit: Infinity
@@ -1839,7 +2027,7 @@ function trimRight(token, greedy) {
1839
2027
  }
1840
2028
 
1841
2029
  class Tokenizer {
1842
- constructor(input, operators = defaultOptions.operators, file, range) {
2030
+ constructor(input, operators = defaultOptions.operators, file, range, groupedExpressions = false) {
1843
2031
  this.input = input;
1844
2032
  this.file = file;
1845
2033
  this.rawBeginAt = -1;
@@ -1847,6 +2035,7 @@ class Tokenizer {
1847
2035
  this.N = range ? range[1] : input.length;
1848
2036
  this.opTrie = createTrie(operators);
1849
2037
  this.literalTrie = createTrie(literalValues);
2038
+ this.groupedExpressions = groupedExpressions;
1850
2039
  }
1851
2040
  readExpression() {
1852
2041
  return new Expression(this.readExpressionTokens());
@@ -1908,6 +2097,8 @@ class Tokenizer {
1908
2097
  this.skipBlank();
1909
2098
  if (this.end())
1910
2099
  return null;
2100
+ if (this.peek() === ')' || this.peek() === ',')
2101
+ return null;
1911
2102
  this.assert(this.read() === '|', `expected "|" before filter`);
1912
2103
  const name = this.readIdentifier();
1913
2104
  if (!name.size()) {
@@ -1922,10 +2113,10 @@ class Tokenizer {
1922
2113
  const arg = this.readFilterArg();
1923
2114
  arg && args.push(arg);
1924
2115
  this.skipBlank();
1925
- this.assert(this.end() || this.peek() === ',' || this.peek() === '|', () => `unexpected character ${this.snapshot()}`);
2116
+ this.assert(this.end() || this.peek() === ',' || this.peek() === '|' || this.peek() === ')', () => `unexpected character ${this.snapshot()}`);
1926
2117
  } while (this.peek() === ',');
1927
2118
  }
1928
- else if (this.peek() === '|' || this.end()) ;
2119
+ else if (this.peek() === '|' || this.peek() === ')' || this.peek() === ',' || this.end()) ;
1929
2120
  else {
1930
2121
  throw this.error('expected ":" after filter name');
1931
2122
  }
@@ -2131,7 +2322,10 @@ class Tokenizer {
2131
2322
  readValue() {
2132
2323
  this.skipBlank();
2133
2324
  const begin = this.p;
2134
- const variable = this.readLiteral() || this.readQuoted() || this.readRange() || this.readNumber();
2325
+ let variable = this.readLiteral() || this.readQuoted() || this.readNumber();
2326
+ if (!variable && this.peek() === '(') {
2327
+ variable = this.readGroupOrRange();
2328
+ }
2135
2329
  const props = this.readProperties(!variable);
2136
2330
  if (!props.length)
2137
2331
  return variable;
@@ -2210,17 +2404,30 @@ class Tokenizer {
2210
2404
  this.p = end;
2211
2405
  return literal;
2212
2406
  }
2213
- readRange() {
2407
+ readGroupOrRange() {
2214
2408
  this.skipBlank();
2215
2409
  const begin = this.p;
2216
2410
  if (this.peek() !== '(')
2217
2411
  return;
2218
2412
  ++this.p;
2219
2413
  const lhs = this.readValueOrThrow();
2220
- this.p += 2;
2221
- const rhs = this.readValueOrThrow();
2222
- ++this.p;
2223
- return new RangeToken(this.input, begin, this.p, lhs, rhs, this.file);
2414
+ this.skipBlank();
2415
+ if (this.peek() === '.' && this.peek(1) === '.') {
2416
+ this.p += 2;
2417
+ const rhs = this.readValueOrThrow();
2418
+ this.skipBlank();
2419
+ this.assert(this.read() === ')', 'invalid range syntax');
2420
+ return new RangeToken(this.input, begin, this.p, lhs, rhs, this.file);
2421
+ }
2422
+ if (this.groupedExpressions) {
2423
+ const initial = new Expression([lhs, ...this.readExpressionTokens()]);
2424
+ this.assert(initial.valid(), () => `invalid value expression: ${this.snapshot()}`);
2425
+ const filters = this.readFilters();
2426
+ this.skipBlank();
2427
+ this.assert(this.read() === ')', 'unbalanced parentheses');
2428
+ return new FilteredValueToken(initial, filters, this.input, begin, this.p, this.file);
2429
+ }
2430
+ throw this.error('invalid range syntax');
2224
2431
  }
2225
2432
  readValueOrThrow() {
2226
2433
  const value = this.readValue();
@@ -2247,7 +2454,7 @@ class Tokenizer {
2247
2454
  }
2248
2455
  *readFileNameTemplate(options) {
2249
2456
  const { outputDelimiterLeft } = options;
2250
- const htmlStopStrings = [',', ' ', outputDelimiterLeft];
2457
+ const htmlStopStrings = [',', ' ', '\r', '\n', '\t', outputDelimiterLeft];
2251
2458
  const htmlStopStringSet = new Set(htmlStopStrings);
2252
2459
  // break on ',' and ' ', outputDelimiterLeft only stops HTML token
2253
2460
  while (this.p < this.N && !htmlStopStringSet.has(this.peek())) {
@@ -2374,33 +2581,6 @@ function createTagClass(options) {
2374
2581
  };
2375
2582
  }
2376
2583
 
2377
- function isKeyValuePair(arr) {
2378
- return isArray(arr);
2379
- }
2380
-
2381
- class Filter {
2382
- constructor(token, options, liquid) {
2383
- this.token = token;
2384
- this.name = token.name;
2385
- this.handler = isFunction(options)
2386
- ? options
2387
- : (isFunction(options === null || options === void 0 ? void 0 : options.handler) ? options.handler : identify);
2388
- this.raw = !isFunction(options) && !!(options === null || options === void 0 ? void 0 : options.raw);
2389
- this.args = token.args;
2390
- this.liquid = liquid;
2391
- }
2392
- *render(value, context) {
2393
- const argv = [];
2394
- for (const arg of this.args) {
2395
- if (isKeyValuePair(arg))
2396
- argv.push([arg[0], yield evalToken(arg[1], context)]);
2397
- else
2398
- argv.push(yield evalToken(arg, context));
2399
- }
2400
- return yield this.handler.apply({ context, token: this.token, liquid: this.liquid }, [value, ...argv]);
2401
- }
2402
- }
2403
-
2404
2584
  class Value {
2405
2585
  /**
2406
2586
  * @param str the value to be valuated, eg.: "foobar" | truncate: 3
@@ -2408,7 +2588,7 @@ class Value {
2408
2588
  constructor(input, liquid) {
2409
2589
  this.filters = [];
2410
2590
  const token = typeof input === 'string'
2411
- ? new Tokenizer(input, liquid.options.operators).readFilteredValue()
2591
+ ? new Tokenizer(input, liquid.options.operators, undefined, undefined, liquid.options.groupedExpressions).readFilteredValue()
2412
2592
  : input;
2413
2593
  this.initial = token.initial;
2414
2594
  this.filters = token.filters.map(token => new Filter(token, this.getFilter(liquid, token.name), liquid));
@@ -2432,7 +2612,7 @@ class Output extends TemplateImpl {
2432
2612
  constructor(token, liquid) {
2433
2613
  var _a;
2434
2614
  super(token);
2435
- const tokenizer = new Tokenizer(token.input, liquid.options.operators, token.file, token.contentRange);
2615
+ const tokenizer = new Tokenizer(token.input, liquid.options.operators, token.file, token.contentRange, liquid.options.groupedExpressions);
2436
2616
  this.value = new Value(tokenizer.readFilteredValue(), liquid);
2437
2617
  const filters = this.value.filters;
2438
2618
  const outputEscape = liquid.options.outputEscape;
@@ -2442,7 +2622,7 @@ class Output extends TemplateImpl {
2442
2622
  }
2443
2623
  }
2444
2624
  *render(ctx, emitter) {
2445
- const val = yield this.value.value(ctx, false);
2625
+ const val = yield toValue(yield this.value.value(ctx, false));
2446
2626
  emitter.write(val);
2447
2627
  }
2448
2628
  *arguments() {
@@ -2718,10 +2898,31 @@ function* extractValueTokenVariables(token) {
2718
2898
  yield* extractValueTokenVariables(token.lhs);
2719
2899
  yield* extractValueTokenVariables(token.rhs);
2720
2900
  }
2901
+ else if (isFilteredValueToken(token)) {
2902
+ yield* extractGroupedExpressionTokenVariables(token);
2903
+ }
2721
2904
  else if (isPropertyAccessToken(token)) {
2722
2905
  yield extractPropertyAccessVariable(token);
2723
2906
  }
2724
2907
  }
2908
+ function* extractGroupedExpressionTokenVariables(token) {
2909
+ if (!isFilteredValueToken(token))
2910
+ return;
2911
+ for (const t of token.initial.postfix) {
2912
+ if (isValueToken(t))
2913
+ yield* extractValueTokenVariables(t);
2914
+ }
2915
+ for (const filter of token.filters) {
2916
+ for (const arg of filter.args) {
2917
+ if (isKeyValuePair(arg) && arg[1]) {
2918
+ yield* extractValueTokenVariables(arg[1]);
2919
+ }
2920
+ else if (isValueToken(arg)) {
2921
+ yield* extractValueTokenVariables(arg);
2922
+ }
2923
+ }
2924
+ }
2925
+ }
2725
2926
  function extractPropertyAccessVariable(token) {
2726
2927
  const segments = [];
2727
2928
  // token is not guaranteed to have `file` set. We'll try to get it from a prop if not.
@@ -2798,44 +2999,47 @@ var LookupType;
2798
2999
  })(LookupType || (LookupType = {}));
2799
3000
  class Loader {
2800
3001
  constructor(options) {
3002
+ var _a, _b, _c, _d;
2801
3003
  this.options = options;
2802
3004
  if (options.relativeReference) {
2803
3005
  const sep = options.fs.sep;
2804
3006
  assert(sep, '`fs.sep` is required for relative reference');
2805
- const rRelativePath = new RegExp(['.' + sep, '..' + sep, './', '../'].map(prefix => escapeRegex(prefix)).join('|'));
2806
- this.shouldLoadRelative = (referencedFile) => rRelativePath.test(referencedFile);
3007
+ const prefixes = ['.' + sep, '..' + sep, './', '../'];
3008
+ this.shouldLoadRelative = (referencedFile) => prefixes.some(prefix => referencedFile.startsWith(prefix));
2807
3009
  }
2808
3010
  else {
2809
3011
  this.shouldLoadRelative = (_referencedFile) => false;
2810
3012
  }
2811
- this.contains = this.options.fs.contains || (() => true);
3013
+ const fs = options.fs;
3014
+ this.contains = toLiquidAsync(((_a = fs.contains) === null || _a === void 0 ? void 0 : _a.bind(fs)) || (() => __awaiter(this, void 0, void 0, function* () { return true; })), ((_b = fs.containsSync) === null || _b === void 0 ? void 0 : _b.bind(fs)) || (() => true));
3015
+ this.exists = toLiquidAsync(((_c = fs.exists) === null || _c === void 0 ? void 0 : _c.bind(fs)) || (() => __awaiter(this, void 0, void 0, function* () { return false; })), (_d = fs.existsSync) === null || _d === void 0 ? void 0 : _d.bind(fs));
2812
3016
  }
2813
3017
  *lookup(file, type, sync, currentFile) {
2814
- const { fs } = this.options;
2815
3018
  const dirs = this.options[type];
2816
- for (const filepath of this.candidates(file, dirs, currentFile, type !== LookupType.Root)) {
2817
- if (sync ? fs.existsSync(filepath) : yield fs.exists(filepath))
3019
+ for (const filepath of this.candidates(file, dirs, currentFile)) {
3020
+ let allowed = false;
3021
+ for (const dir of dirs) {
3022
+ if (yield this.contains(!!sync, dir, filepath)) {
3023
+ allowed = true;
3024
+ break;
3025
+ }
3026
+ }
3027
+ if (!allowed)
3028
+ continue;
3029
+ if (yield this.exists(!!sync, filepath))
2818
3030
  return filepath;
2819
3031
  }
2820
3032
  throw this.lookupError(file, dirs);
2821
3033
  }
2822
- *candidates(file, dirs, currentFile, enforceRoot) {
3034
+ *candidates(file, dirs, currentFile) {
2823
3035
  const { fs, extname } = this.options;
2824
3036
  if (this.shouldLoadRelative(file) && currentFile) {
2825
3037
  const referenced = fs.resolve(this.dirname(currentFile), file, extname);
2826
- for (const dir of dirs) {
2827
- if (!enforceRoot || this.contains(dir, referenced)) {
2828
- // the relatively referenced file is within one of root dirs
2829
- yield referenced;
2830
- break;
2831
- }
2832
- }
3038
+ yield referenced;
2833
3039
  }
2834
3040
  for (const dir of dirs) {
2835
3041
  const referenced = fs.resolve(dir, file, extname);
2836
- if (!enforceRoot || this.contains(dir, referenced)) {
2837
- yield referenced;
2838
- }
3042
+ yield referenced;
2839
3043
  }
2840
3044
  if (fs.fallback !== undefined) {
2841
3045
  const filepath = fs.fallback(file);
@@ -2858,17 +3062,19 @@ class Loader {
2858
3062
 
2859
3063
  class Parser {
2860
3064
  constructor(liquid) {
3065
+ var _a, _b;
2861
3066
  this.liquid = liquid;
2862
3067
  this.cache = this.liquid.options.cache;
2863
3068
  this.fs = this.liquid.options.fs;
2864
3069
  this.parseFile = this.cache ? this._parseFileCached : this._parseFile;
2865
3070
  this.loader = new Loader(this.liquid.options);
2866
3071
  this.parseLimit = new Limiter('parse length', liquid.options.parseLimit);
3072
+ this.readFile = toLiquidAsync(((_a = this.fs.readFile) === null || _a === void 0 ? void 0 : _a.bind(this.fs)) || (() => __awaiter(this, void 0, void 0, function* () { throw new Error('readFile not implemented'); })), (_b = this.fs.readFileSync) === null || _b === void 0 ? void 0 : _b.bind(this.fs));
2867
3073
  }
2868
3074
  parse(html, filepath) {
2869
3075
  html = String(html);
2870
3076
  this.parseLimit.use(html.length);
2871
- const tokenizer = new Tokenizer(html, this.liquid.options.operators, filepath);
3077
+ const tokenizer = new Tokenizer(html, this.liquid.options.operators, filepath, undefined, this.liquid.options.groupedExpressions);
2872
3078
  const tokens = tokenizer.readTopLevelTokens(this.liquid.options);
2873
3079
  return this.parseTokens(tokens);
2874
3080
  }
@@ -2934,7 +3140,7 @@ class Parser {
2934
3140
  }
2935
3141
  *_parseFile(file, sync, type = LookupType.Root, currentFile) {
2936
3142
  const filepath = yield this.loader.lookup(file, type, sync, currentFile);
2937
- return this.parse(sync ? this.fs.readFileSync(filepath) : yield this.fs.readFile(filepath), filepath);
3143
+ return this.parse(yield this.readFile(!!sync, filepath), filepath);
2938
3144
  }
2939
3145
  }
2940
3146
 
@@ -2953,6 +3159,7 @@ var TokenKind;
2953
3159
  TokenKind[TokenKind["Quoted"] = 1024] = "Quoted";
2954
3160
  TokenKind[TokenKind["Operator"] = 2048] = "Operator";
2955
3161
  TokenKind[TokenKind["FilteredValue"] = 4096] = "FilteredValue";
3162
+ TokenKind[TokenKind["GroupedExpression"] = 8192] = "GroupedExpression";
2956
3163
  TokenKind[TokenKind["Delimited"] = 12] = "Delimited";
2957
3164
  })(TokenKind || (TokenKind = {}));
2958
3165
 
@@ -2989,9 +3196,12 @@ function isWordToken(val) {
2989
3196
  function isRangeToken(val) {
2990
3197
  return getKind(val) === TokenKind.Range;
2991
3198
  }
3199
+ function isFilteredValueToken(val) {
3200
+ return getKind(val) === TokenKind.FilteredValue;
3201
+ }
2992
3202
  function isValueToken(val) {
2993
- // valueTokenBitMask = TokenKind.Number | TokenKind.Literal | TokenKind.Quoted | TokenKind.PropertyAccess | TokenKind.Range
2994
- return (getKind(val) & 1667) > 0;
3203
+ // valueTokenBitMask = TokenKind.Number | TokenKind.Literal | TokenKind.Quoted | TokenKind.PropertyAccess | TokenKind.Range | TokenKind.FilteredValue
3204
+ return (getKind(val) & 5763) > 0;
2995
3205
  }
2996
3206
  function getKind(val) {
2997
3207
  return val ? val.kind : -1;
@@ -3010,17 +3220,25 @@ var typeGuards = /*#__PURE__*/Object.freeze({
3010
3220
  isPropertyAccessToken: isPropertyAccessToken,
3011
3221
  isWordToken: isWordToken,
3012
3222
  isRangeToken: isRangeToken,
3223
+ isFilteredValueToken: isFilteredValueToken,
3013
3224
  isValueToken: isValueToken
3014
3225
  });
3015
3226
 
3227
+ function createScope(from) {
3228
+ const scope = Object.create(null);
3229
+ if (from)
3230
+ Object.assign(scope, from);
3231
+ return scope;
3232
+ }
3233
+
3016
3234
  class Context {
3017
- constructor(env = {}, opts = defaultOptions, renderOptions = {}, { memoryLimit, renderLimit } = {}) {
3235
+ constructor(env = {}, opts = defaultOptions, renderOptions = {}, { memoryLimit, renderLimit, liquid } = {}) {
3018
3236
  var _a, _b, _c, _d, _e;
3019
3237
  /**
3020
3238
  * insert a Context-level empty scope,
3021
3239
  * for tags like `{% capture %}` `{% assign %}` to operate
3022
3240
  */
3023
- this.scopes = [{}];
3241
+ this.scopes = [createScope()];
3024
3242
  this.registers = {};
3025
3243
  this.breakCalled = false;
3026
3244
  this.continueCalled = false;
@@ -3032,9 +3250,10 @@ class Context {
3032
3250
  this.ownPropertyOnly = (_c = renderOptions.ownPropertyOnly) !== null && _c !== void 0 ? _c : opts.ownPropertyOnly;
3033
3251
  this.memoryLimit = memoryLimit !== null && memoryLimit !== void 0 ? memoryLimit : new Limiter('memory alloc', (_d = renderOptions.memoryLimit) !== null && _d !== void 0 ? _d : opts.memoryLimit);
3034
3252
  this.renderLimit = renderLimit !== null && renderLimit !== void 0 ? renderLimit : new Limiter('template render', getPerformance().now() + ((_e = renderOptions.renderLimit) !== null && _e !== void 0 ? _e : opts.renderLimit));
3253
+ this.liquid = liquid;
3035
3254
  }
3036
- getRegister(key) {
3037
- return (this.registers[key] = this.registers[key] || {});
3255
+ getRegister(key, defaultValue = undefined) {
3256
+ return (this.registers[key] = this.registers[key] || defaultValue);
3038
3257
  }
3039
3258
  setRegister(key, value) {
3040
3259
  return (this.registers[key] = value);
@@ -3072,7 +3291,7 @@ class Context {
3072
3291
  if (isString(paths))
3073
3292
  paths = paths.split('.');
3074
3293
  for (let i = 0; i < paths.length; i++) {
3075
- scope = yield readProperty(scope, paths[i], this.ownPropertyOnly);
3294
+ scope = yield this.readProperty(scope, paths[i]);
3076
3295
  if (strictVariables && isUndefined(scope)) {
3077
3296
  throw new InternalUndefinedVariableError(paths.slice(0, i + 1).join('.'));
3078
3297
  }
@@ -3092,10 +3311,12 @@ class Context {
3092
3311
  return new Context(scope, this.opts, {
3093
3312
  sync: this.sync,
3094
3313
  globals: this.globals,
3095
- strictVariables: this.strictVariables
3314
+ strictVariables: this.strictVariables,
3315
+ ownPropertyOnly: this.ownPropertyOnly
3096
3316
  }, {
3097
3317
  renderLimit: this.renderLimit,
3098
- memoryLimit: this.memoryLimit
3318
+ memoryLimit: this.memoryLimit,
3319
+ liquid: this.liquid
3099
3320
  });
3100
3321
  }
3101
3322
  findScope(key) {
@@ -3108,41 +3329,41 @@ class Context {
3108
3329
  return this.environments;
3109
3330
  return this.globals;
3110
3331
  }
3111
- }
3112
- function readProperty(obj, key, ownPropertyOnly) {
3113
- obj = toLiquid(obj);
3114
- key = toValue(key);
3115
- if (isNil(obj))
3116
- return obj;
3117
- if (isArray(obj) && key < 0)
3118
- return obj[obj.length + +key];
3119
- const value = readJSProperty(obj, key, ownPropertyOnly);
3120
- if (value === undefined && obj instanceof Drop)
3121
- return obj.liquidMethodMissing(key);
3122
- if (isFunction(value))
3123
- return value.call(obj);
3124
- if (key === 'size')
3125
- return readSize(obj);
3126
- else if (key === 'first')
3127
- return readFirst(obj);
3128
- else if (key === 'last')
3129
- return readLast(obj);
3130
- return value;
3332
+ readProperty(obj, key) {
3333
+ obj = toLiquid(obj);
3334
+ key = toValue(key);
3335
+ if (isNil(obj))
3336
+ return obj;
3337
+ if (isArray(obj) && isNumber(key))
3338
+ return readArrayElement(obj, key, this.ownPropertyOnly);
3339
+ const value = readJSProperty(obj, key, this.ownPropertyOnly);
3340
+ if (value === undefined && obj instanceof Drop)
3341
+ return obj.liquidMethodMissing(key, this);
3342
+ if (isFunction(value))
3343
+ return value.call(obj);
3344
+ if (key === 'size')
3345
+ return readSize(obj);
3346
+ else if (key === 'first')
3347
+ return readFirst(obj, this.ownPropertyOnly);
3348
+ else if (key === 'last')
3349
+ return readLast(obj, this.ownPropertyOnly);
3350
+ return value;
3351
+ }
3131
3352
  }
3132
3353
  function readJSProperty(obj, key, ownPropertyOnly) {
3133
3354
  if (ownPropertyOnly && !hasOwnProperty.call(obj, key) && !(obj instanceof Drop))
3134
3355
  return undefined;
3135
3356
  return obj[key];
3136
3357
  }
3137
- function readFirst(obj) {
3358
+ function readFirst(obj, ownPropertyOnly) {
3138
3359
  if (isArray(obj))
3139
- return obj[0];
3140
- return obj['first'];
3360
+ return readArrayElement(obj, 0, ownPropertyOnly);
3361
+ return readJSProperty(obj, 'first', ownPropertyOnly);
3141
3362
  }
3142
- function readLast(obj) {
3363
+ function readLast(obj, ownPropertyOnly) {
3143
3364
  if (isArray(obj))
3144
- return obj[obj.length - 1];
3145
- return obj['last'];
3365
+ return readArrayElement(obj, -1, ownPropertyOnly);
3366
+ return readJSProperty(obj, 'last', ownPropertyOnly);
3146
3367
  }
3147
3368
  function readSize(obj) {
3148
3369
  if (hasOwnProperty.call(obj, 'size') || obj['size'] !== undefined)
@@ -3161,25 +3382,28 @@ var BlockMode;
3161
3382
  BlockMode[BlockMode["STORE"] = 1] = "STORE";
3162
3383
  })(BlockMode || (BlockMode = {}));
3163
3384
 
3164
- const abs = argumentsToValue(Math.abs);
3165
- const at_least = argumentsToValue(Math.max);
3166
- const at_most = argumentsToValue(Math.min);
3167
- const ceil = argumentsToValue(Math.ceil);
3168
- const divided_by = argumentsToValue((dividend, divisor, integerArithmetic = false) => integerArithmetic ? Math.floor(dividend / divisor) : dividend / divisor);
3169
- const floor = argumentsToValue(Math.floor);
3170
- const minus = argumentsToValue((v, arg) => v - arg);
3171
- const modulo = argumentsToValue((v, arg) => v % arg);
3172
- const times = argumentsToValue((v, arg) => v * arg);
3173
- function round(v, arg = 0) {
3174
- v = toValue(v);
3175
- arg = toValue(arg);
3176
- const amp = Math.pow(10, arg);
3177
- return Math.round(v * amp) / amp;
3385
+ const abs = argumentsToNumber(Math.abs);
3386
+ const at_least = argumentsToNumber(Math.max);
3387
+ const at_most = argumentsToNumber(Math.min);
3388
+ const ceil = argumentsToNumber(Math.ceil);
3389
+ const divided_by = argumentsToNumber((dividend, divisor, integerArithmetic = false) => integerArithmetic ? Math.floor(dividend / divisor) : dividend / divisor);
3390
+ const floor = argumentsToNumber(Math.floor);
3391
+ const minus = argumentsToNumber((v, arg) => v - arg);
3392
+ const plus = argumentsToNumber((lhs, rhs) => lhs + rhs);
3393
+ const modulo = argumentsToNumber((v, arg) => ((v % arg) + arg) % arg);
3394
+ const times = argumentsToNumber((v, arg) => v * arg);
3395
+ function shiftDecimal(n, exponent) {
3396
+ const [mantissa, power = '0'] = String(n).split('e');
3397
+ return Number(`${mantissa}e${Number(power) + exponent}`);
3178
3398
  }
3179
- function plus(v, arg) {
3180
- v = toValue(v);
3181
- arg = toValue(arg);
3182
- return Number(v) + Number(arg);
3399
+ function round(v, arg = 0) {
3400
+ v = toNumber(v);
3401
+ arg = toNumber(arg);
3402
+ const scaled = shiftDecimal(Math.abs(v), arg);
3403
+ if (!(scaled <= Number.MAX_SAFE_INTEGER))
3404
+ return v;
3405
+ const rounded = shiftDecimal(Math.round(scaled), -arg);
3406
+ return v < 0 ? -rounded : rounded;
3183
3407
  }
3184
3408
 
3185
3409
  var mathFilters = /*#__PURE__*/Object.freeze({
@@ -3191,13 +3415,13 @@ var mathFilters = /*#__PURE__*/Object.freeze({
3191
3415
  divided_by: divided_by,
3192
3416
  floor: floor,
3193
3417
  minus: minus,
3418
+ plus: plus,
3194
3419
  modulo: modulo,
3195
3420
  times: times,
3196
- round: round,
3197
- plus: plus
3421
+ round: round
3198
3422
  });
3199
3423
 
3200
- const url_decode = (x) => decodeURIComponent(stringify(x)).replace(/\+/g, ' ');
3424
+ const url_decode = (x) => decodeURIComponent(stringify(x).replace(/\+/g, ' '));
3201
3425
  const url_encode = (x) => encodeURIComponent(stringify(x)).replace(/%20/g, '+');
3202
3426
  const cgi_escape = (x) => encodeURIComponent(stringify(x))
3203
3427
  .replace(/%20/g, '+')
@@ -3255,18 +3479,24 @@ var urlFilters = /*#__PURE__*/Object.freeze({
3255
3479
  const join = argumentsToValue(function (v, arg) {
3256
3480
  const array = toArray(v);
3257
3481
  const sep = isNil(arg) ? ' ' : stringify(arg);
3258
- const complexity = array.length * (1 + sep.length);
3259
- this.context.memoryLimit.use(complexity);
3260
- return array.join(sep);
3482
+ let outputSize = sep.length * Math.max(array.length - 1, 0);
3483
+ for (let i = 0; i < array.length; i++)
3484
+ outputSize += String(array[i]).length;
3485
+ this.context.memoryLimit.use(outputSize);
3486
+ return Array.prototype.join.call(array, sep);
3487
+ });
3488
+ const last = argumentsToValue(function (v) {
3489
+ return isArrayLike(v) ? readArrayElement(v, -1, this.context.ownPropertyOnly) : '';
3490
+ });
3491
+ const first = argumentsToValue(function (v) {
3492
+ return isArrayLike(v) ? readArrayElement(v, 0, this.context.ownPropertyOnly) : '';
3261
3493
  });
3262
- const last$1 = argumentsToValue((v) => isArrayLike(v) ? last(v) : '');
3263
- const first = argumentsToValue((v) => isArrayLike(v) ? v[0] : '');
3264
3494
  const reverse = argumentsToValue(function (v) {
3265
3495
  const array = toArray(v);
3266
3496
  this.context.memoryLimit.use(array.length);
3267
3497
  return [...array].reverse();
3268
3498
  });
3269
- function* sort(arr, property) {
3499
+ function* sortBy(arr, property, comparator) {
3270
3500
  const values = [];
3271
3501
  const array = toArray(arr);
3272
3502
  this.context.memoryLimit.use(array.length);
@@ -3276,20 +3506,13 @@ function* sort(arr, property) {
3276
3506
  property ? yield this.context._getFromScope(item, stringify(property).split('.'), false) : item
3277
3507
  ]);
3278
3508
  }
3279
- return values.sort((lhs, rhs) => {
3280
- const lvalue = lhs[1];
3281
- const rvalue = rhs[1];
3282
- return lvalue < rvalue ? -1 : (lvalue > rvalue ? 1 : 0);
3283
- }).map(tuple => tuple[0]);
3284
- }
3285
- function sort_natural(input, property) {
3286
- const propertyString = stringify(property);
3287
- const compare = property === undefined
3288
- ? caseInsensitiveCompare
3289
- : (lhs, rhs) => caseInsensitiveCompare(lhs[propertyString], rhs[propertyString]);
3290
- const array = toArray(input);
3291
- this.context.memoryLimit.use(array.length);
3292
- return [...array].sort(compare);
3509
+ return values.sort((lhs, rhs) => comparator(lhs[1], rhs[1])).map(tuple => tuple[0]);
3510
+ }
3511
+ function* sort(arr, property) {
3512
+ return yield* sortBy.call(this, arr, property, orderedCompare);
3513
+ }
3514
+ function* sort_natural(arr, property) {
3515
+ return yield* sortBy.call(this, arr, property, caseInsensitiveCompare);
3293
3516
  }
3294
3517
  const size = (v) => (v && v.length) || 0;
3295
3518
  function* map(arr, property) {
@@ -3313,13 +3536,13 @@ function* sum(arr, property) {
3313
3536
  function compact(arr) {
3314
3537
  const array = toArray(arr);
3315
3538
  this.context.memoryLimit.use(array.length);
3316
- return array.filter(x => !isNil(toValue(x)));
3539
+ return Array.prototype.filter.call(array, x => !isNil(toValue(x)));
3317
3540
  }
3318
3541
  function concat(v, arg = []) {
3319
3542
  const lhs = toArray(v);
3320
3543
  const rhs = toArray(arg);
3321
3544
  this.context.memoryLimit.use(lhs.length + rhs.length);
3322
- return lhs.concat(rhs);
3545
+ return Array.prototype.concat.call(lhs, rhs);
3323
3546
  }
3324
3547
  function push(v, arg) {
3325
3548
  return concat.call(this, v, [arg]);
@@ -3332,7 +3555,9 @@ function unshift(v, arg) {
3332
3555
  return clone;
3333
3556
  }
3334
3557
  function pop(v) {
3335
- const clone = [...toArray(v)];
3558
+ const array = toArray(v);
3559
+ this.context.memoryLimit.use(array.length);
3560
+ const clone = [...array];
3336
3561
  clone.pop();
3337
3562
  return clone;
3338
3563
  }
@@ -3349,39 +3574,65 @@ function slice(v, begin, length = 1) {
3349
3574
  return [];
3350
3575
  if (!isArray(v))
3351
3576
  v = stringify(v);
3352
- begin = begin < 0 ? v.length + begin : begin;
3577
+ begin = begin < 0 ? (isArray(v) ? v.length : codePointLength(v)) + begin : begin;
3578
+ if (begin < 0 || length < 0)
3579
+ return isArray(v) ? [] : '';
3353
3580
  this.context.memoryLimit.use(length);
3354
- return v.slice(begin, begin + length);
3355
- }
3356
- function* where(arr, property, expected) {
3357
- const values = [];
3581
+ if (isArray(v))
3582
+ return Array.prototype.slice.call(v, begin, begin + length);
3583
+ const start = advanceCodePoints(v, 0, begin);
3584
+ return v.slice(start, advanceCodePoints(v, start, length));
3585
+ }
3586
+ function* matches(value, expected) {
3587
+ if (this.context.opts.jekyllWhere) {
3588
+ if (EmptyDrop.is(expected))
3589
+ return yield equals(value, expected);
3590
+ if (isArray(value))
3591
+ return yield arrayIncludes(value, expected);
3592
+ return yield equals(value, expected);
3593
+ }
3594
+ if (expected === undefined)
3595
+ return isTruthy(value, this.context);
3596
+ return yield equals(value, expected);
3597
+ }
3598
+ function* filter(include, arr, property, expected) {
3358
3599
  arr = toArray(arr);
3359
3600
  this.context.memoryLimit.use(arr.length);
3360
3601
  const token = new Tokenizer(stringify(property)).readScopeValue();
3602
+ const result = [];
3361
3603
  for (const item of arr) {
3362
- values.push(yield evalToken(token, this.context.spawn(item)));
3363
- }
3364
- const matcher = this.context.opts.jekyllWhere
3365
- ? (v) => EmptyDrop.is(expected) ? equals(v, expected) : (isArray(v) ? arrayIncludes(v, expected) : equals(v, expected))
3366
- : (v) => equals(v, expected);
3367
- return arr.filter((_, i) => {
3368
- if (expected === undefined)
3369
- return isTruthy(values[i], this.context);
3370
- return matcher(values[i]);
3371
- });
3604
+ const value = yield evalToken(token, this.context.spawn(item));
3605
+ if ((yield matches.call(this, value, expected)) === include)
3606
+ result.push(item);
3607
+ }
3608
+ return result;
3372
3609
  }
3373
- function* where_exp(arr, itemName, exp) {
3610
+ function* filter_exp(include, arr, itemName, exp) {
3374
3611
  const filtered = [];
3375
3612
  const keyTemplate = new Value(stringify(exp), this.liquid);
3376
3613
  const array = toArray(arr);
3377
3614
  this.context.memoryLimit.use(array.length);
3378
3615
  for (const item of array) {
3379
- const value = yield keyTemplate.value(this.context.spawn({ [itemName]: item }));
3380
- if (value)
3616
+ this.context.push({ [itemName]: item });
3617
+ const value = yield keyTemplate.value(this.context);
3618
+ this.context.pop();
3619
+ if (value === include)
3381
3620
  filtered.push(item);
3382
3621
  }
3383
3622
  return filtered;
3384
3623
  }
3624
+ function* where(arr, property, expected) {
3625
+ return yield* filter.call(this, true, arr, property, expected);
3626
+ }
3627
+ function* reject(arr, property, expected) {
3628
+ return yield* filter.call(this, false, arr, property, expected);
3629
+ }
3630
+ function* where_exp(arr, itemName, exp) {
3631
+ return yield* filter_exp.call(this, true, arr, itemName, exp);
3632
+ }
3633
+ function* reject_exp(arr, itemName, exp) {
3634
+ return yield* filter_exp.call(this, false, arr, itemName, exp);
3635
+ }
3385
3636
  function* group_by(arr, property) {
3386
3637
  const map = new Map();
3387
3638
  arr = toEnumerable(arr);
@@ -3401,31 +3652,59 @@ function* group_by_exp(arr, itemName, exp) {
3401
3652
  arr = toEnumerable(arr);
3402
3653
  this.context.memoryLimit.use(arr.length);
3403
3654
  for (const item of arr) {
3404
- const key = yield keyTemplate.value(this.context.spawn({ [itemName]: item }));
3655
+ this.context.push({ [itemName]: item });
3656
+ const key = yield keyTemplate.value(this.context);
3657
+ this.context.pop();
3405
3658
  if (!map.has(key))
3406
3659
  map.set(key, []);
3407
3660
  map.get(key).push(item);
3408
3661
  }
3409
3662
  return [...map.entries()].map(([name, items]) => ({ name, items }));
3410
3663
  }
3411
- function* find(arr, property, expected) {
3664
+ function* search(arr, property, expected) {
3412
3665
  const token = new Tokenizer(stringify(property)).readScopeValue();
3413
3666
  const array = toArray(arr);
3414
- for (const item of array) {
3415
- const value = yield evalToken(token, this.context.spawn(item));
3416
- if (equals(value, expected))
3417
- return item;
3667
+ for (let index = 0; index < array.length; index++) {
3668
+ const value = yield evalToken(token, this.context.spawn(array[index]));
3669
+ if (yield matches.call(this, value, expected))
3670
+ return [index, array[index]];
3418
3671
  }
3419
3672
  }
3420
- function* find_exp(arr, itemName, exp) {
3673
+ function* search_exp(arr, itemName, exp) {
3421
3674
  const predicate = new Value(stringify(exp), this.liquid);
3422
3675
  const array = toArray(arr);
3423
- for (const item of array) {
3424
- const value = yield predicate.value(this.context.spawn({ [itemName]: item }));
3676
+ for (let index = 0; index < array.length; index++) {
3677
+ this.context.push({ [itemName]: array[index] });
3678
+ const value = yield predicate.value(this.context);
3679
+ this.context.pop();
3425
3680
  if (value)
3426
- return item;
3681
+ return [index, array[index]];
3427
3682
  }
3428
3683
  }
3684
+ function* has(arr, property, expected) {
3685
+ const result = yield* search.call(this, arr, property, expected);
3686
+ return !!result;
3687
+ }
3688
+ function* has_exp(arr, itemName, exp) {
3689
+ const result = yield* search_exp.call(this, arr, itemName, exp);
3690
+ return !!result;
3691
+ }
3692
+ function* find_index(arr, property, expected) {
3693
+ const result = yield* search.call(this, arr, property, expected);
3694
+ return result ? result[0] : undefined;
3695
+ }
3696
+ function* find_index_exp(arr, itemName, exp) {
3697
+ const result = yield* search_exp.call(this, arr, itemName, exp);
3698
+ return result ? result[0] : undefined;
3699
+ }
3700
+ function* find(arr, property, expected) {
3701
+ const result = yield* search.call(this, arr, property, expected);
3702
+ return result ? result[1] : undefined;
3703
+ }
3704
+ function* find_exp(arr, itemName, exp) {
3705
+ const result = yield* search_exp.call(this, arr, itemName, exp);
3706
+ return result ? result[1] : undefined;
3707
+ }
3429
3708
  function uniq(arr) {
3430
3709
  arr = toArray(arr);
3431
3710
  this.context.memoryLimit.use(arr.length);
@@ -3437,7 +3716,7 @@ function sample(v, count = 1) {
3437
3716
  return [];
3438
3717
  if (!isArray(v))
3439
3718
  v = stringify(v);
3440
- this.context.memoryLimit.use(count);
3719
+ this.context.memoryLimit.use(v.length);
3441
3720
  const shuffled = [...v].sort(() => Math.random() - 0.5);
3442
3721
  if (count === 1)
3443
3722
  return shuffled[0];
@@ -3447,7 +3726,7 @@ function sample(v, count = 1) {
3447
3726
  var arrayFilters = /*#__PURE__*/Object.freeze({
3448
3727
  __proto__: null,
3449
3728
  join: join,
3450
- last: last$1,
3729
+ last: last,
3451
3730
  first: first,
3452
3731
  reverse: reverse,
3453
3732
  sort: sort,
@@ -3463,9 +3742,15 @@ var arrayFilters = /*#__PURE__*/Object.freeze({
3463
3742
  shift: shift,
3464
3743
  slice: slice,
3465
3744
  where: where,
3745
+ reject: reject,
3466
3746
  where_exp: where_exp,
3747
+ reject_exp: reject_exp,
3467
3748
  group_by: group_by,
3468
3749
  group_by_exp: group_by_exp,
3750
+ has: has,
3751
+ has_exp: has_exp,
3752
+ find_index: find_index,
3753
+ find_index_exp: find_index_exp,
3469
3754
  find: find,
3470
3755
  find_exp: find_exp,
3471
3756
  uniq: uniq,
@@ -3473,21 +3758,25 @@ var arrayFilters = /*#__PURE__*/Object.freeze({
3473
3758
  });
3474
3759
 
3475
3760
  function date(v, format, timezoneOffset) {
3476
- var _a, _b, _c;
3477
- const size = ((_a = v === null || v === void 0 ? void 0 : v.length) !== null && _a !== void 0 ? _a : 0) + ((_b = format === null || format === void 0 ? void 0 : format.length) !== null && _b !== void 0 ? _b : 0) + ((_c = timezoneOffset === null || timezoneOffset === void 0 ? void 0 : timezoneOffset.length) !== null && _c !== void 0 ? _c : 0);
3761
+ var _a, _b;
3762
+ const size = ((_a = v === null || v === void 0 ? void 0 : v.length) !== null && _a !== void 0 ? _a : 0) + ((_b = timezoneOffset === null || timezoneOffset === void 0 ? void 0 : timezoneOffset.length) !== null && _b !== void 0 ? _b : 0);
3478
3763
  this.context.memoryLimit.use(size);
3479
3764
  const date = parseDate(v, this.context.opts, timezoneOffset);
3480
3765
  if (!date)
3481
3766
  return v;
3482
3767
  format = toValue(format);
3483
3768
  format = isNil(format) ? this.context.opts.dateFormat : stringify(format);
3484
- return strftime(date, format);
3769
+ this.context.memoryLimit.use(format.length);
3770
+ return strftime(date, format, this.context.memoryLimit);
3485
3771
  }
3486
3772
  function date_to_xmlschema(v) {
3487
3773
  return date.call(this, v, '%Y-%m-%dT%H:%M:%S%:z');
3488
3774
  }
3489
3775
  function date_to_rfc822(v) {
3490
- return date.call(this, v, '%a, %d %b %Y %H:%M:%S %z');
3776
+ const date = parseDate(v, this.context.opts, undefined, 'en-US');
3777
+ if (!date)
3778
+ return v;
3779
+ return strftime(date, '%a, %d %b %Y %H:%M:%S %z', this.context.memoryLimit);
3491
3780
  }
3492
3781
  function date_to_string(v, type, style) {
3493
3782
  return stringify_date.call(this, v, '%b', type, style);
@@ -3499,20 +3788,23 @@ function stringify_date(v, month_type, type, style) {
3499
3788
  const date = parseDate(v, this.context.opts);
3500
3789
  if (!date)
3501
3790
  return v;
3791
+ const ml = this.context.memoryLimit;
3502
3792
  if (type === 'ordinal') {
3503
3793
  const d = date.getDate();
3504
3794
  return style === 'US'
3505
- ? strftime(date, `${month_type} ${d}%q, %Y`)
3506
- : strftime(date, `${d}%q ${month_type} %Y`);
3795
+ ? strftime(date, `${month_type} ${d}%q, %Y`, ml)
3796
+ : strftime(date, `${d}%q ${month_type} %Y`, ml);
3507
3797
  }
3508
- return strftime(date, `%d ${month_type} %Y`);
3798
+ return strftime(date, `%d ${month_type} %Y`, ml);
3509
3799
  }
3510
- function parseDate(v, opts, timezoneOffset) {
3800
+ function parseDate(v, opts, timezoneOffset, locale = opts.locale) {
3511
3801
  let date;
3512
3802
  const defaultTimezoneOffset = timezoneOffset !== null && timezoneOffset !== void 0 ? timezoneOffset : opts.timezoneOffset;
3513
- const locale = opts.locale;
3514
3803
  v = toValue(v);
3515
- if (v === 'now' || v === 'today') {
3804
+ if (isNil(v)) {
3805
+ return undefined;
3806
+ }
3807
+ else if (v === 'now' || v === 'today') {
3516
3808
  date = new LiquidDate(Date.now(), locale, defaultTimezoneOffset);
3517
3809
  }
3518
3810
  else if (isNumber(v)) {
@@ -3566,14 +3858,34 @@ function prepend(v, arg) {
3566
3858
  this.context.memoryLimit.use(lhs.length + rhs.length);
3567
3859
  return rhs + lhs;
3568
3860
  }
3861
+ function getTrimStart(str, chars) {
3862
+ let start = 0;
3863
+ for (const char of str) {
3864
+ if (!chars.has(char))
3865
+ break;
3866
+ start += char.length;
3867
+ }
3868
+ return start;
3869
+ }
3870
+ function getTrimEnd(str, chars, start = 0) {
3871
+ let end = str.length;
3872
+ while (end > start) {
3873
+ const size = end - start >= 2 && str.codePointAt(end - 2) > 0xFFFF ? 2 : 1;
3874
+ if (!chars.has(str.slice(end - size, end)))
3875
+ break;
3876
+ end -= size;
3877
+ }
3878
+ return end;
3879
+ }
3569
3880
  function lstrip(v, chars) {
3570
3881
  const str = stringify(v);
3571
3882
  this.context.memoryLimit.use(str.length);
3572
3883
  if (chars) {
3573
- chars = escapeRegExp(stringify(chars));
3574
- return str.replace(new RegExp(`^[${chars}]+`, 'g'), '');
3884
+ chars = stringify(chars);
3885
+ this.context.memoryLimit.use(chars.length);
3886
+ return str.slice(getTrimStart(str, new Set(chars)));
3575
3887
  }
3576
- return str.replace(/^\s+/, '');
3888
+ return str.trimStart();
3577
3889
  }
3578
3890
  function downcase(v) {
3579
3891
  const str = stringify(v);
@@ -3587,18 +3899,20 @@ function upcase(v) {
3587
3899
  }
3588
3900
  function remove(v, arg) {
3589
3901
  const str = stringify(v);
3590
- this.context.memoryLimit.use(str.length);
3591
- return str.split(stringify(arg)).join('');
3902
+ arg = stringify(arg);
3903
+ this.context.memoryLimit.use(str.length + arg.length);
3904
+ return str.split(arg).join('');
3592
3905
  }
3593
3906
  function remove_first(v, l) {
3594
3907
  const str = stringify(v);
3595
- this.context.memoryLimit.use(str.length);
3596
- return str.replace(stringify(l), '');
3908
+ l = stringify(l);
3909
+ this.context.memoryLimit.use(str.length + l.length);
3910
+ return str.replace(l, '');
3597
3911
  }
3598
3912
  function remove_last(v, l) {
3599
3913
  const str = stringify(v);
3600
- this.context.memoryLimit.use(str.length);
3601
3914
  const pattern = stringify(l);
3915
+ this.context.memoryLimit.use(str.length + pattern.length);
3602
3916
  const index = str.lastIndexOf(pattern);
3603
3917
  if (index === -1)
3604
3918
  return str;
@@ -3608,10 +3922,11 @@ function rstrip(str, chars) {
3608
3922
  str = stringify(str);
3609
3923
  this.context.memoryLimit.use(str.length);
3610
3924
  if (chars) {
3611
- chars = escapeRegExp(stringify(chars));
3612
- return str.replace(new RegExp(`[${chars}]+$`, 'g'), '');
3925
+ chars = stringify(chars);
3926
+ this.context.memoryLimit.use(chars.length);
3927
+ return str.slice(0, getTrimEnd(str, new Set(chars)));
3613
3928
  }
3614
- return str.replace(/\s+$/, '');
3929
+ return str.trimEnd();
3615
3930
  }
3616
3931
  function split(v, arg) {
3617
3932
  const str = stringify(v);
@@ -3627,10 +3942,10 @@ function strip(v, chars) {
3627
3942
  const str = stringify(v);
3628
3943
  this.context.memoryLimit.use(str.length);
3629
3944
  if (chars) {
3630
- chars = escapeRegExp(stringify(chars));
3631
- return str
3632
- .replace(new RegExp(`^[${chars}]+`, 'g'), '')
3633
- .replace(new RegExp(`[${chars}]+$`, 'g'), '');
3945
+ const set = new Set(stringify(chars));
3946
+ this.context.memoryLimit.use(set.size);
3947
+ const start = getTrimStart(str, set);
3948
+ return str.slice(start, getTrimEnd(str, set, start));
3634
3949
  }
3635
3950
  return str.trim();
3636
3951
  }
@@ -3639,6 +3954,11 @@ function strip_newlines(v) {
3639
3954
  this.context.memoryLimit.use(str.length);
3640
3955
  return str.replace(/\r?\n/gm, '');
3641
3956
  }
3957
+ function squish(v) {
3958
+ const str = stringify(v);
3959
+ this.context.memoryLimit.use(str.length);
3960
+ return str.replace(/\s+/g, ' ').trim();
3961
+ }
3642
3962
  function capitalize(str) {
3643
3963
  str = stringify(str);
3644
3964
  this.context.memoryLimit.use(str.length);
@@ -3646,35 +3966,43 @@ function capitalize(str) {
3646
3966
  }
3647
3967
  function replace(v, pattern, replacement) {
3648
3968
  const str = stringify(v);
3649
- this.context.memoryLimit.use(str.length);
3650
- return str.split(stringify(pattern)).join(replacement);
3969
+ pattern = stringify(pattern);
3970
+ replacement = stringify(replacement);
3971
+ const parts = str.split(pattern);
3972
+ const outputSize = str.length + (parts.length - 1) * (replacement.length - pattern.length);
3973
+ this.context.memoryLimit.use(outputSize);
3974
+ return parts.join(replacement);
3651
3975
  }
3652
3976
  function replace_first(v, arg1, arg2) {
3653
3977
  const str = stringify(v);
3654
- this.context.memoryLimit.use(str.length);
3655
- return str.replace(stringify(arg1), arg2);
3978
+ arg1 = stringify(arg1);
3979
+ arg2 = stringify(arg2);
3980
+ this.context.memoryLimit.use(str.length + arg1.length + arg2.length);
3981
+ return str.replace(arg1, () => arg2);
3656
3982
  }
3657
3983
  function replace_last(v, arg1, arg2) {
3658
3984
  const str = stringify(v);
3659
- this.context.memoryLimit.use(str.length);
3660
3985
  const pattern = stringify(arg1);
3986
+ const replacement = stringify(arg2);
3987
+ this.context.memoryLimit.use(str.length + pattern.length + replacement.length);
3661
3988
  const index = str.lastIndexOf(pattern);
3662
3989
  if (index === -1)
3663
3990
  return str;
3664
- const replacement = stringify(arg2);
3665
3991
  return str.substring(0, index) + replacement + str.substring(index + pattern.length);
3666
3992
  }
3667
3993
  function truncate(v, l = 50, o = '...') {
3668
3994
  const str = stringify(v);
3669
- this.context.memoryLimit.use(str.length);
3670
- if (str.length <= l)
3995
+ o = stringify(o);
3996
+ this.context.memoryLimit.use(str.length + o.length);
3997
+ if (str.length <= l || advanceCodePoints(str, 0, l) >= str.length)
3671
3998
  return v;
3672
- return str.substring(0, l - o.length) + o;
3999
+ return str.slice(0, advanceCodePoints(str, 0, Math.max(l - codePointLength(o), 0))) + o;
3673
4000
  }
3674
4001
  function truncatewords(v, words = 15, o = '...') {
3675
4002
  const str = stringify(v);
3676
- this.context.memoryLimit.use(str.length);
3677
- const arr = str.split(/\s+/);
4003
+ o = stringify(o);
4004
+ this.context.memoryLimit.use(str.length + o.length);
4005
+ const arr = str.trimStart().split(/\s+/);
3678
4006
  if (words <= 0)
3679
4007
  words = 1;
3680
4008
  let ret = arr.slice(0, words).join(' ');
@@ -3707,8 +4035,13 @@ function number_of_words(input, mode) {
3707
4035
  return input.split(/\s+/).length;
3708
4036
  }
3709
4037
  }
3710
- function array_to_sentence_string(array, connector = 'and') {
3711
- this.context.memoryLimit.use(array.length);
4038
+ function array_to_sentence_string(input, connector = 'and') {
4039
+ const array = toArray(input);
4040
+ connector = stringify(connector);
4041
+ let outputSize = connector.length + array.length * 2;
4042
+ for (let i = 0; i < array.length; i++)
4043
+ outputSize += stringify(array[i]).length;
4044
+ this.context.memoryLimit.use(outputSize);
3712
4045
  switch (array.length) {
3713
4046
  case 0:
3714
4047
  return '';
@@ -3735,6 +4068,7 @@ var stringFilters = /*#__PURE__*/Object.freeze({
3735
4068
  split: split,
3736
4069
  strip: strip,
3737
4070
  strip_newlines: strip_newlines,
4071
+ squish: squish,
3738
4072
  capitalize: capitalize,
3739
4073
  replace: replace,
3740
4074
  replace_first: replace_first,
@@ -3746,7 +4080,77 @@ var stringFilters = /*#__PURE__*/Object.freeze({
3746
4080
  array_to_sentence_string: array_to_sentence_string
3747
4081
  });
3748
4082
 
3749
- const filters = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, htmlFilters), mathFilters), urlFilters), arrayFilters), dateFilters), stringFilters), misc);
4083
+ function base64Encode(str) {
4084
+ return Buffer.from(str, 'utf8').toString('base64');
4085
+ }
4086
+ function base64Decode(str) {
4087
+ return Buffer.from(str, 'base64').toString('utf8');
4088
+ }
4089
+
4090
+ /**
4091
+ * Base64 related filters
4092
+ *
4093
+ * Implements base64_encode and base64_decode filters for Shopify compatibility
4094
+ */
4095
+ function base64_encode(value) {
4096
+ if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) {
4097
+ this.context.memoryLimit.use(value.byteLength);
4098
+ return value.toString('base64');
4099
+ }
4100
+ const str = stringify(value);
4101
+ this.context.memoryLimit.use(str.length);
4102
+ return base64Encode(str);
4103
+ }
4104
+ function base64_decode(value) {
4105
+ const str = stringify(value);
4106
+ this.context.memoryLimit.use(str.length);
4107
+ assert(isBase64(str), 'invalid base64 provided to base64_decode');
4108
+ return base64Decode(str);
4109
+ }
4110
+ function isBase64(str) {
4111
+ str = str.replace(/[\t\n\f\r ]/g, '');
4112
+ if (str.length % 4 === 0)
4113
+ str = str.replace(/==?$/, '');
4114
+ return str.length % 4 !== 1 && /^[A-Za-z0-9+/]*$/.test(str);
4115
+ }
4116
+
4117
+ var base64Filters = /*#__PURE__*/Object.freeze({
4118
+ __proto__: null,
4119
+ base64_encode: base64_encode,
4120
+ base64_decode: base64_decode
4121
+ });
4122
+
4123
+ function sha256(str) {
4124
+ return createHash('sha256').update(str, 'utf8').digest('hex');
4125
+ }
4126
+ function hmacSha256(str, key) {
4127
+ return createHmac('sha256', key).update(str, 'utf8').digest('hex');
4128
+ }
4129
+
4130
+ /**
4131
+ * Crypto related filters
4132
+ *
4133
+ * Implements sha256 and hmac_sha256 filters for Shopify compatibility
4134
+ */
4135
+ function sha256$1(value) {
4136
+ const str = stringify(value);
4137
+ this.context.memoryLimit.use(str.length);
4138
+ return sha256(str);
4139
+ }
4140
+ function hmac_sha256(value, key) {
4141
+ const str = stringify(value);
4142
+ const keyStr = stringify(key);
4143
+ this.context.memoryLimit.use(str.length + keyStr.length);
4144
+ return hmacSha256(str, keyStr);
4145
+ }
4146
+
4147
+ var cryptoFilters = /*#__PURE__*/Object.freeze({
4148
+ __proto__: null,
4149
+ sha256: sha256$1,
4150
+ hmac_sha256: hmac_sha256
4151
+ });
4152
+
4153
+ const filters = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, htmlFilters), mathFilters), urlFilters), arrayFilters), dateFilters), stringFilters), base64Filters), cryptoFilters), misc);
3750
4154
 
3751
4155
  class AssignTag extends Tag {
3752
4156
  constructor(token, remainTokens, liquid) {
@@ -3802,8 +4206,8 @@ class ForTag extends Tag {
3802
4206
  return;
3803
4207
  }
3804
4208
  const continueKey = 'continue-' + this.variable + '-' + this.collection.getText();
3805
- ctx.push({ continue: ctx.getRegister(continueKey) });
3806
- const hash = yield this.hash.render(ctx);
4209
+ ctx.push(createScope({ continue: ctx.getRegister(continueKey, {}) }));
4210
+ const hash = (yield this.hash.render(ctx));
3807
4211
  ctx.pop();
3808
4212
  const modifiers = this.liquid.options.orderedFilterParameters
3809
4213
  ? Object.keys(hash).filter(x => MODIFIERS.includes(x))
@@ -3816,7 +4220,7 @@ class ForTag extends Tag {
3816
4220
  return reversed(collection);
3817
4221
  }, collection);
3818
4222
  ctx.setRegister(continueKey, (hash['offset'] || 0) + collection.length);
3819
- const scope = { forloop: new ForloopDrop(collection.length, this.collection.getText(), this.variable) };
4223
+ const scope = createScope({ forloop: new ForloopDrop(collection.length, this.collection.getText(), this.variable) });
3820
4224
  ctx.push(scope);
3821
4225
  for (const item of collection) {
3822
4226
  scope[this.variable] = item;
@@ -3942,12 +4346,12 @@ class CaseTag extends Tag {
3942
4346
  }
3943
4347
  *render(ctx, emitter) {
3944
4348
  const r = this.liquid.renderer;
3945
- const target = toValue(yield this.value.value(ctx, ctx.opts.lenientIf));
4349
+ const target = yield toValue(yield this.value.value(ctx, ctx.opts.lenientIf));
3946
4350
  let branchHit = false;
3947
4351
  for (const branch of this.branches) {
3948
4352
  for (const valueToken of branch.values) {
3949
4353
  const value = yield evalToken(valueToken, ctx, ctx.opts.lenientIf);
3950
- if (equals(target, value)) {
4354
+ if (yield equals(target, value)) {
3951
4355
  yield r.renderTemplates(branch.templates, ctx, emitter);
3952
4356
  branchHit = true;
3953
4357
  break;
@@ -4008,7 +4412,11 @@ class RenderTag extends Tag {
4008
4412
  alias = tokenizer.readIdentifier();
4009
4413
  else
4010
4414
  tokenizer.p = beforeAs;
4011
- this[keyword.content] = { value, alias: alias && alias.content };
4415
+ const binding = { value, alias: alias && alias.content };
4416
+ if (keyword.content === 'with')
4417
+ this.with = binding;
4418
+ else
4419
+ this.forBinding = binding;
4012
4420
  tokenizer.skipBlank();
4013
4421
  if (tokenizer.peek() === ',')
4014
4422
  tokenizer.advance();
@@ -4026,42 +4434,42 @@ class RenderTag extends Tag {
4026
4434
  }
4027
4435
  *render(ctx, emitter) {
4028
4436
  const { liquid, hash } = this;
4029
- const filepath = (yield renderFilePath(this['file'], ctx, liquid));
4437
+ const filepath = (yield renderFilePath(this.file, ctx, liquid));
4030
4438
  assert(filepath, () => `illegal file path "${filepath}"`);
4031
4439
  const childCtx = ctx.spawn();
4032
4440
  const scope = childCtx.bottom();
4033
4441
  __assign(scope, yield hash.render(ctx));
4034
- if (this['with']) {
4035
- const { value, alias } = this['with'];
4442
+ if (this.with) {
4443
+ const { value, alias } = this.with;
4036
4444
  scope[alias || filepath] = yield evalToken(value, ctx);
4037
4445
  }
4038
- if (this['for']) {
4039
- const { value, alias } = this['for'];
4446
+ if (this.forBinding) {
4447
+ const { value, alias } = this.forBinding;
4040
4448
  const collection = toEnumerable(yield evalToken(value, ctx));
4041
4449
  scope['forloop'] = new ForloopDrop(collection.length, value.getText(), alias);
4042
4450
  for (const item of collection) {
4043
4451
  scope[alias] = item;
4044
- const templates = (yield liquid._parsePartialFile(filepath, childCtx.sync, this['currentFile']));
4452
+ const templates = (yield liquid._parsePartialFile(filepath, childCtx.sync, this.currentFile));
4045
4453
  yield liquid.renderer.renderTemplates(templates, childCtx, emitter);
4046
4454
  scope['forloop'].next();
4047
4455
  }
4048
4456
  }
4049
4457
  else {
4050
- const templates = (yield liquid._parsePartialFile(filepath, childCtx.sync, this['currentFile']));
4458
+ const templates = (yield liquid._parsePartialFile(filepath, childCtx.sync, this.currentFile));
4051
4459
  yield liquid.renderer.renderTemplates(templates, childCtx, emitter);
4052
4460
  }
4053
4461
  }
4054
4462
  *children(partials, sync) {
4055
- if (partials && isString(this['file'])) {
4056
- return (yield this.liquid._parsePartialFile(this['file'], sync, this['currentFile']));
4463
+ if (partials && isString(this.file)) {
4464
+ return (yield this.liquid._parsePartialFile(this.file, sync, this.currentFile));
4057
4465
  }
4058
4466
  return [];
4059
4467
  }
4060
4468
  partialScope() {
4061
- if (isString(this['file'])) {
4469
+ if (isString(this.file)) {
4062
4470
  const names = Object.keys(this.hash.hash);
4063
- if (this['with']) {
4064
- const { value, alias } = this['with'];
4471
+ if (this.with) {
4472
+ const { value, alias } = this.with;
4065
4473
  if (isString(alias)) {
4066
4474
  names.push([alias, value]);
4067
4475
  }
@@ -4069,8 +4477,8 @@ class RenderTag extends Tag {
4069
4477
  names.push([this.file, value]);
4070
4478
  }
4071
4479
  }
4072
- if (this['for']) {
4073
- const { value, alias } = this['for'];
4480
+ if (this.forBinding) {
4481
+ const { value, alias } = this.forBinding;
4074
4482
  if (isString(alias)) {
4075
4483
  names.push([alias, value]);
4076
4484
  }
@@ -4078,7 +4486,7 @@ class RenderTag extends Tag {
4078
4486
  names.push([this.file, value]);
4079
4487
  }
4080
4488
  }
4081
- return { name: this['file'], isolated: true, scope: names };
4489
+ return { name: this.file, isolated: true, scope: names };
4082
4490
  }
4083
4491
  }
4084
4492
  *arguments() {
@@ -4087,14 +4495,14 @@ class RenderTag extends Tag {
4087
4495
  yield v;
4088
4496
  }
4089
4497
  }
4090
- if (this['with']) {
4091
- const { value } = this['with'];
4498
+ if (this.with) {
4499
+ const { value } = this.with;
4092
4500
  if (isValueToken(value)) {
4093
4501
  yield value;
4094
4502
  }
4095
4503
  }
4096
- if (this['for']) {
4097
- const { value } = this['for'];
4504
+ if (this.forBinding) {
4505
+ const { value } = this.forBinding;
4098
4506
  if (isValueToken(value)) {
4099
4507
  yield value;
4100
4508
  }
@@ -4142,8 +4550,8 @@ class IncludeTag extends Tag {
4142
4550
  constructor(token, remainTokens, liquid, parser) {
4143
4551
  super(token, remainTokens, liquid);
4144
4552
  const { tokenizer } = token;
4145
- this['file'] = parseFilePath(tokenizer, this.liquid, parser);
4146
- this['currentFile'] = token.file;
4553
+ this.file = parseFilePath(tokenizer, this.liquid, parser);
4554
+ this.currentFile = token.file;
4147
4555
  const begin = tokenizer.p;
4148
4556
  const withStr = tokenizer.readIdentifier();
4149
4557
  if (withStr.content === 'with') {
@@ -4161,28 +4569,28 @@ class IncludeTag extends Tag {
4161
4569
  *render(ctx, emitter) {
4162
4570
  const { liquid, hash, withVar } = this;
4163
4571
  const { renderer } = liquid;
4164
- const filepath = (yield renderFilePath(this['file'], ctx, liquid));
4572
+ const filepath = (yield renderFilePath(this.file, ctx, liquid));
4165
4573
  assert(filepath, () => `illegal file path "${filepath}"`);
4166
4574
  const saved = ctx.saveRegister('blocks', 'blockMode');
4167
4575
  ctx.setRegister('blocks', {});
4168
4576
  ctx.setRegister('blockMode', BlockMode.OUTPUT);
4169
- const scope = (yield hash.render(ctx));
4577
+ const scope = createScope((yield hash.render(ctx)));
4170
4578
  if (withVar)
4171
4579
  scope[filepath] = yield evalToken(withVar, ctx);
4172
- const templates = (yield liquid._parsePartialFile(filepath, ctx.sync, this['currentFile']));
4173
- ctx.push(ctx.opts.jekyllInclude ? { include: scope } : scope);
4580
+ const templates = (yield liquid._parsePartialFile(filepath, ctx.sync, this.currentFile));
4581
+ ctx.push(ctx.opts.jekyllInclude ? createScope({ include: scope }) : scope);
4174
4582
  yield renderer.renderTemplates(templates, ctx, emitter);
4175
4583
  ctx.pop();
4176
4584
  ctx.restoreRegister(saved);
4177
4585
  }
4178
4586
  *children(partials, sync) {
4179
- if (partials && isString(this['file'])) {
4180
- return (yield this.liquid._parsePartialFile(this['file'], sync, this['currentFile']));
4587
+ if (partials && isString(this.file)) {
4588
+ return (yield this.liquid._parsePartialFile(this.file, sync, this.currentFile));
4181
4589
  }
4182
4590
  return [];
4183
4591
  }
4184
4592
  partialScope() {
4185
- if (isString(this['file'])) {
4593
+ if (isString(this.file)) {
4186
4594
  let names;
4187
4595
  if (this.liquid.options.jekyllInclude) {
4188
4596
  names = ['include'];
@@ -4190,16 +4598,16 @@ class IncludeTag extends Tag {
4190
4598
  else {
4191
4599
  names = Object.keys(this.hash.hash);
4192
4600
  if (this.withVar) {
4193
- names.push([this['file'], this.withVar]);
4601
+ names.push([this.file, this.withVar]);
4194
4602
  }
4195
4603
  }
4196
- return { name: this['file'], isolated: false, scope: names };
4604
+ return { name: this.file, isolated: false, scope: names };
4197
4605
  }
4198
4606
  }
4199
4607
  *arguments() {
4200
4608
  yield* Object.values(this.hash.hash).filter(isValueToken);
4201
- if (isValueToken(this['file'])) {
4202
- yield this['file'];
4609
+ if (isValueToken(this.file)) {
4610
+ yield this.file;
4203
4611
  }
4204
4612
  if (isValueToken(this.withVar)) {
4205
4613
  yield this.withVar;
@@ -4229,6 +4637,7 @@ class CycleTag extends Tag {
4229
4637
  constructor(token, remainTokens, liquid) {
4230
4638
  super(token, remainTokens, liquid);
4231
4639
  this.candidates = [];
4640
+ const begin = this.tokenizer.p;
4232
4641
  const group = this.tokenizer.readValue();
4233
4642
  this.tokenizer.skipBlank();
4234
4643
  if (group) {
@@ -4237,28 +4646,33 @@ class CycleTag extends Tag {
4237
4646
  this.tokenizer.advance();
4238
4647
  }
4239
4648
  else
4240
- this.candidates.push(group);
4649
+ this.tokenizer.p = begin;
4241
4650
  }
4242
4651
  while (!this.tokenizer.end()) {
4243
- const value = this.tokenizer.readValue();
4244
- if (value)
4245
- this.candidates.push(value);
4652
+ const value = this.tokenizer.readFilteredValue();
4653
+ this.candidates.push(value);
4246
4654
  this.tokenizer.readTo(',');
4247
4655
  }
4248
4656
  this.tokenizer.assert(this.candidates.length, () => `empty candidates: "${token.getText()}"`);
4249
4657
  }
4250
4658
  *render(ctx, emitter) {
4659
+ var _a;
4251
4660
  const group = (yield evalToken(this.group, ctx));
4252
- const fingerprint = `cycle:${group}:` + this.candidates.join(',');
4253
- const groups = ctx.getRegister('cycle');
4661
+ const fingerprint = `cycle:${group}:` + this.candidates.map(c => c.getText()).join(',');
4662
+ const groups = ctx.getRegister('cycle', {});
4254
4663
  let idx = groups[fingerprint];
4255
4664
  if (idx === undefined) {
4256
4665
  idx = groups[fingerprint] = 0;
4257
4666
  }
4258
- const candidate = this.candidates[idx];
4667
+ const candidate = new Value(this.candidates[idx], this.liquid);
4259
4668
  idx = (idx + 1) % this.candidates.length;
4260
4669
  groups[fingerprint] = idx;
4261
- return yield evalToken(candidate, ctx);
4670
+ let val = yield toValue(yield candidate.value(ctx, false));
4671
+ const outputEscape = this.liquid.options.outputEscape;
4672
+ if (outputEscape && !((_a = candidate.filters[candidate.filters.length - 1]) === null || _a === void 0 ? void 0 : _a.raw)) {
4673
+ val = yield outputEscape.call({ context: ctx, token: this.token, liquid: this.liquid }, val);
4674
+ }
4675
+ return val;
4262
4676
  }
4263
4677
  *arguments() {
4264
4678
  yield* this.candidates;
@@ -4298,7 +4712,7 @@ class IfTag extends Tag {
4298
4712
  *render(ctx, emitter) {
4299
4713
  const r = this.liquid.renderer;
4300
4714
  for (const { value, templates } of this.branches) {
4301
- const v = yield value.value(ctx, ctx.opts.lenientIf);
4715
+ const v = yield toValue(yield value.value(ctx, ctx.opts.lenientIf));
4302
4716
  if (isTruthy(v, ctx)) {
4303
4717
  yield r.renderTemplates(templates, ctx, emitter);
4304
4718
  return;
@@ -4342,7 +4756,7 @@ class LayoutTag extends Tag {
4342
4756
  constructor(token, remainTokens, liquid, parser) {
4343
4757
  super(token, remainTokens, liquid);
4344
4758
  this.file = parseFilePath(this.tokenizer, this.liquid, parser);
4345
- this['currentFile'] = token.file;
4759
+ this.currentFile = token.file;
4346
4760
  this.args = new Hash(this.tokenizer, liquid.options.keyValueSeparator);
4347
4761
  this.templates = parser.parseTokens(remainTokens);
4348
4762
  }
@@ -4356,24 +4770,24 @@ class LayoutTag extends Tag {
4356
4770
  }
4357
4771
  const filepath = (yield renderFilePath(this.file, ctx, liquid));
4358
4772
  assert(filepath, () => `illegal file path "${filepath}"`);
4359
- const templates = (yield liquid._parseLayoutFile(filepath, ctx.sync, this['currentFile']));
4773
+ const templates = (yield liquid._parseLayoutFile(filepath, ctx.sync, this.currentFile));
4360
4774
  // render remaining contents and store rendered results
4361
4775
  ctx.setRegister('blockMode', BlockMode.STORE);
4362
4776
  const html = yield renderer.renderTemplates(this.templates, ctx);
4363
- const blocks = ctx.getRegister('blocks');
4777
+ const blocks = ctx.getRegister('blocks', {});
4364
4778
  // set whole content to anonymous block if anonymous doesn't specified
4365
4779
  if (blocks[''] === undefined)
4366
4780
  blocks[''] = (parent, emitter) => emitter.write(html);
4367
4781
  ctx.setRegister('blockMode', BlockMode.OUTPUT);
4368
4782
  // render the layout file use stored blocks
4369
- ctx.push((yield args.render(ctx)));
4783
+ ctx.push(createScope((yield args.render(ctx))));
4370
4784
  yield renderer.renderTemplates(templates, ctx, emitter);
4371
4785
  ctx.pop();
4372
4786
  }
4373
4787
  *children(partials) {
4374
4788
  const templates = this.templates.slice();
4375
4789
  if (partials && isString(this.file)) {
4376
- templates.push(...(yield this.liquid._parsePartialFile(this.file, true, this['currentFile'])));
4790
+ templates.push(...(yield this.liquid._parsePartialFile(this.file, true, this.currentFile)));
4377
4791
  }
4378
4792
  return templates;
4379
4793
  }
@@ -4412,23 +4826,28 @@ class BlockTag extends Tag {
4412
4826
  *render(ctx, emitter) {
4413
4827
  const blockRender = this.getBlockRender(ctx);
4414
4828
  if (ctx.getRegister('blockMode') === BlockMode.STORE) {
4415
- ctx.getRegister('blocks')[this.block] = blockRender;
4829
+ ctx.getRegister('blocks', {})[this.block] = blockRender;
4416
4830
  }
4417
4831
  else {
4418
4832
  yield blockRender(new BlockDrop(), emitter);
4419
4833
  }
4420
4834
  }
4421
4835
  getBlockRender(ctx) {
4836
+ const self = this;
4422
4837
  const { liquid, templates } = this;
4423
- const renderChild = ctx.getRegister('blocks')[this.block];
4838
+ const renderChild = ctx.getRegister('blocks', {})[this.block];
4424
4839
  const renderCurrent = function* (superBlock, emitter) {
4425
- // add {{ block.super }} support when rendering
4426
- ctx.push({ block: superBlock });
4840
+ const stack = ctx.getRegister('blockStack', []);
4841
+ if (stack.includes(self))
4842
+ throw new Error('block tag cannot be nested');
4843
+ stack.push(self);
4844
+ ctx.push(createScope({ block: superBlock }));
4427
4845
  yield liquid.renderer.renderTemplates(templates, ctx, emitter);
4428
4846
  ctx.pop();
4847
+ stack.pop();
4429
4848
  };
4430
4849
  return renderChild
4431
- ? (superBlock, emitter) => renderChild(new BlockDrop(() => renderCurrent(superBlock, emitter)), emitter)
4850
+ ? (superBlock, emitter) => renderChild(new BlockDrop((emitter) => renderCurrent(superBlock, emitter)), emitter)
4432
4851
  : renderCurrent;
4433
4852
  }
4434
4853
  *children() {
@@ -4512,7 +4931,7 @@ class TablerowTag extends Tag {
4512
4931
  const cols = args.cols || collection.length;
4513
4932
  const r = this.liquid.renderer;
4514
4933
  const tablerowloop = new TablerowloopDrop(collection.length, cols, this.collection.getText(), this.variable);
4515
- const scope = { tablerowloop };
4934
+ const scope = createScope({ tablerowloop });
4516
4935
  ctx.push(scope);
4517
4936
  for (let idx = 0; idx < collection.length; idx++, tablerowloop.next()) {
4518
4937
  scope[this.variable] = collection[idx];
@@ -4585,7 +5004,7 @@ class UnlessTag extends Tag {
4585
5004
  *render(ctx, emitter) {
4586
5005
  const r = this.liquid.renderer;
4587
5006
  for (const { value, test, templates } of this.branches) {
4588
- const v = yield value.value(ctx, ctx.opts.lenientIf);
5007
+ const v = yield toValue(yield value.value(ctx, ctx.opts.lenientIf));
4589
5008
  if (test(v, ctx)) {
4590
5009
  yield r.renderTemplates(templates, ctx, emitter);
4591
5010
  return;
@@ -4626,9 +5045,14 @@ class EchoTag extends Tag {
4626
5045
  }
4627
5046
  }
4628
5047
  *render(ctx, emitter) {
5048
+ var _a;
4629
5049
  if (!this.value)
4630
5050
  return;
4631
- const val = yield this.value.value(ctx, false);
5051
+ let val = yield toValue(yield this.value.value(ctx, false));
5052
+ const outputEscape = this.liquid.options.outputEscape;
5053
+ if (outputEscape && !((_a = this.value.filters[this.value.filters.length - 1]) === null || _a === void 0 ? void 0 : _a.raw)) {
5054
+ val = yield outputEscape.call({ context: ctx, token: this.token, liquid: this.liquid }, val);
5055
+ }
4632
5056
  emitter.write(val);
4633
5057
  }
4634
5058
  *arguments() {
@@ -4689,8 +5113,8 @@ const tags = {
4689
5113
  class Liquid {
4690
5114
  constructor(opts = {}) {
4691
5115
  this.renderer = new Render();
4692
- this.filters = {};
4693
- this.tags = {};
5116
+ this.filters = Object.create(null);
5117
+ this.tags = Object.create(null);
4694
5118
  this.options = normalize(opts);
4695
5119
  // eslint-disable-next-line deprecation/deprecation
4696
5120
  this.parser = new Parser(this);
@@ -4702,7 +5126,7 @@ class Liquid {
4702
5126
  return parser.parse(html, filepath);
4703
5127
  }
4704
5128
  _render(tpl, scope, renderOptions) {
4705
- const ctx = scope instanceof Context ? scope : new Context(scope, this.options, renderOptions);
5129
+ const ctx = scope instanceof Context ? scope : new Context(scope, this.options, renderOptions, { liquid: this });
4706
5130
  return this.renderer.renderTemplates(tpl, ctx);
4707
5131
  }
4708
5132
  render(tpl, scope, renderOptions) {
@@ -4714,7 +5138,7 @@ class Liquid {
4714
5138
  return toValueSync(this._render(tpl, scope, Object.assign(Object.assign({}, renderOptions), { sync: true })));
4715
5139
  }
4716
5140
  renderToNodeStream(tpl, scope, renderOptions = {}) {
4717
- const ctx = new Context(scope, this.options, renderOptions);
5141
+ const ctx = new Context(scope, this.options, renderOptions, { liquid: this });
4718
5142
  return this.renderer.renderTemplatesToNodeStream(tpl, ctx);
4719
5143
  }
4720
5144
  _parseAndRender(html, scope, renderOptions) {
@@ -4766,7 +5190,7 @@ class Liquid {
4766
5190
  }
4767
5191
  _evalValue(str, scope) {
4768
5192
  const value = new Value(str, this);
4769
- const ctx = scope instanceof Context ? scope : new Context(scope, this.options);
5193
+ const ctx = scope instanceof Context ? scope : new Context(scope, this.options, {}, { liquid: this });
4770
5194
  return value.value(ctx);
4771
5195
  }
4772
5196
  evalValue(str, scope) {
@@ -4780,6 +5204,9 @@ class Liquid {
4780
5204
  registerFilter(name, filter) {
4781
5205
  this.filters[name] = filter;
4782
5206
  }
5207
+ unregisterFilter(name) {
5208
+ delete this.filters[name];
5209
+ }
4783
5210
  registerTag(name, tag) {
4784
5211
  this.tags[name] = isFunction(tag) ? tag : createTagClass(tag);
4785
5212
  }
@@ -4893,4 +5320,4 @@ class Liquid {
4893
5320
  /* istanbul ignore file */
4894
5321
  const version = '[VI]{version}[/VI]';
4895
5322
 
4896
- export { AssertionError, AssignTag, BlockTag, BreakTag, CaptureTag, CaseTag, CommentTag, Context, ContinueTag, CycleTag, DecrementTag, Drop, EchoTag, Expression, Filter, ForTag, Hash, IfTag, IncludeTag, IncrementTag, InlineCommentTag, LayoutTag, Liquid, LiquidError, LiquidTag, Output, ParseError, ParseStream, Parser, RawTag, RenderError, RenderTag, TablerowTag, Tag, TagToken, Token, TokenKind, TokenizationError, Tokenizer, typeGuards as TypeGuards, UndefinedVariableError, UnlessTag, Value, Variable, analyze, analyzeSync, assert, createTrie, defaultOperators, defaultOptions, evalQuotedToken, evalToken, filters, isFalsy, isTruthy, tags, toPromise, toValue, toValueSync, version };
5323
+ export { AssertionError, AssignTag, BlockTag, BreakTag, CaptureTag, CaseTag, CommentTag, Context, ContinueTag, CycleTag, DecrementTag, Drop, EchoTag, Expression, Filter, ForTag, Hash, IfTag, IncludeTag, IncrementTag, InlineCommentTag, LayoutTag, Liquid, LiquidError, LiquidTag, LookupType, Output, ParseError, ParseStream, Parser, RawTag, RenderError, RenderTag, TablerowTag, Tag, TagToken, Token, TokenKind, TokenizationError, Tokenizer, typeGuards as TypeGuards, UndefinedVariableError, UnlessTag, Value, Variable, analyze, analyzeSync, assert, createTrie, defaultOperators, defaultOptions, evalQuotedToken, evalToken, evalTokenValue, filters, isFalsy, isTruthy, tags, toPromise, toValue, toValueSync, version };