@skeptools/skep-organization-project 0.0.15 → 0.0.17

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 (75) hide show
  1. package/.jsii +138 -3
  2. package/API.md +137 -44
  3. package/lib/skep-organization-project.js +1 -1
  4. package/node_modules/liquidjs/README.md +29 -17
  5. package/node_modules/liquidjs/dist/context/context.d.ts +13 -4
  6. package/node_modules/liquidjs/dist/drop/blank-drop.d.ts +1 -0
  7. package/node_modules/liquidjs/dist/drop/drop.d.ts +1 -1
  8. package/node_modules/liquidjs/dist/drop/empty-drop.d.ts +1 -0
  9. package/node_modules/liquidjs/dist/filters/array.d.ts +14 -14
  10. package/node_modules/liquidjs/dist/filters/html.d.ts +6 -5
  11. package/node_modules/liquidjs/dist/filters/math.d.ts +9 -9
  12. package/node_modules/liquidjs/dist/filters/string.d.ts +22 -21
  13. package/node_modules/liquidjs/dist/fs/map-fs.d.ts +13 -0
  14. package/node_modules/liquidjs/dist/fs/node-require.d.ts +1 -2
  15. package/node_modules/liquidjs/dist/index.d.ts +3 -3
  16. package/node_modules/liquidjs/dist/liquid-options.d.ts +30 -2
  17. package/node_modules/liquidjs/dist/liquid.browser.min.js +1 -1
  18. package/node_modules/liquidjs/dist/liquid.browser.min.js.map +1 -1
  19. package/node_modules/liquidjs/dist/{liquid.browser.esm.js → liquid.browser.mjs} +1147 -264
  20. package/node_modules/liquidjs/dist/liquid.browser.umd.js +2072 -478
  21. package/node_modules/liquidjs/dist/liquid.browser.umd.js.map +1 -1
  22. package/node_modules/liquidjs/dist/liquid.d.ts +32 -1
  23. package/node_modules/liquidjs/dist/{liquid.node.cjs.js → liquid.node.js} +1123 -264
  24. package/node_modules/liquidjs/dist/{liquid.node.esm.js → liquid.node.mjs} +1152 -265
  25. package/node_modules/liquidjs/dist/parser/parser.d.ts +1 -0
  26. package/node_modules/liquidjs/dist/parser/parser.spec.d.ts +1 -0
  27. package/node_modules/liquidjs/dist/parser/tokenizer.d.ts +4 -3
  28. package/node_modules/liquidjs/dist/render/expression.d.ts +1 -1
  29. package/node_modules/liquidjs/dist/render/operator.d.ts +1 -0
  30. package/node_modules/liquidjs/dist/tags/assign.d.ts +5 -0
  31. package/node_modules/liquidjs/dist/tags/block.d.ts +4 -1
  32. package/node_modules/liquidjs/dist/tags/break.d.ts +1 -1
  33. package/node_modules/liquidjs/dist/tags/capture.d.ts +7 -2
  34. package/node_modules/liquidjs/dist/tags/case.d.ts +5 -1
  35. package/node_modules/liquidjs/dist/tags/continue.d.ts +1 -1
  36. package/node_modules/liquidjs/dist/tags/cycle.d.ts +2 -0
  37. package/node_modules/liquidjs/dist/tags/decrement.d.ts +3 -0
  38. package/node_modules/liquidjs/dist/tags/echo.d.ts +2 -0
  39. package/node_modules/liquidjs/dist/tags/for.d.ts +6 -1
  40. package/node_modules/liquidjs/dist/tags/if.d.ts +6 -2
  41. package/node_modules/liquidjs/dist/tags/include.d.ts +7 -2
  42. package/node_modules/liquidjs/dist/tags/increment.d.ts +3 -0
  43. package/node_modules/liquidjs/dist/tags/layout.d.ts +6 -1
  44. package/node_modules/liquidjs/dist/tags/liquid.d.ts +3 -1
  45. package/node_modules/liquidjs/dist/tags/render.d.ts +7 -2
  46. package/node_modules/liquidjs/dist/tags/tablerow.d.ts +6 -1
  47. package/node_modules/liquidjs/dist/tags/unless.d.ts +5 -1
  48. package/node_modules/liquidjs/dist/template/analysis.d.ts +85 -0
  49. package/node_modules/liquidjs/dist/template/analysis.spec.d.ts +1 -0
  50. package/node_modules/liquidjs/dist/template/filter-impl-options.d.ts +2 -0
  51. package/node_modules/liquidjs/dist/template/filter.d.ts +3 -1
  52. package/node_modules/liquidjs/dist/template/hash.d.ts +2 -1
  53. package/node_modules/liquidjs/dist/template/index.d.ts +1 -0
  54. package/node_modules/liquidjs/dist/template/output.d.ts +2 -1
  55. package/node_modules/liquidjs/dist/template/tag.d.ts +2 -2
  56. package/node_modules/liquidjs/dist/template/template.d.ts +29 -0
  57. package/node_modules/liquidjs/dist/tokens/identifier-token.d.ts +0 -1
  58. package/node_modules/liquidjs/dist/tokens/liquid-tag-token.d.ts +1 -1
  59. package/node_modules/liquidjs/dist/util/assert.d.ts +1 -0
  60. package/node_modules/liquidjs/dist/util/error.d.ts +7 -2
  61. package/node_modules/liquidjs/dist/util/error.spec.d.ts +1 -0
  62. package/node_modules/liquidjs/dist/util/index.d.ts +2 -1
  63. package/node_modules/liquidjs/dist/util/intl.d.ts +6 -0
  64. package/node_modules/liquidjs/dist/util/limiter.d.ts +8 -0
  65. package/node_modules/liquidjs/dist/util/liquid-date.d.ts +39 -7
  66. package/node_modules/liquidjs/dist/util/liquid-date.spec.d.ts +1 -0
  67. package/node_modules/liquidjs/dist/util/performance.d.ts +5 -0
  68. package/node_modules/liquidjs/dist/util/performance.spec.d.ts +1 -0
  69. package/node_modules/liquidjs/dist/util/type-guards.d.ts +2 -1
  70. package/node_modules/liquidjs/dist/util/type-guards.spec.d.ts +1 -0
  71. package/node_modules/liquidjs/dist/util/underscore.d.ts +5 -1
  72. package/node_modules/liquidjs/package.json +11 -7
  73. package/package.json +17 -13
  74. package/node_modules/liquidjs/dist/util/timezone-date.d.ts +0 -44
  75. /package/node_modules/liquidjs/dist/{util/timezone-date.spec.d.ts → fs/map-fs.spec.d.ts} +0 -0
@@ -1,6 +1,6 @@
1
1
  /*
2
- * liquidjs@10.13.0, https://github.com/harttle/liquidjs
3
- * (c) 2016-2024 harttle
2
+ * liquidjs@10.20.1, https://github.com/harttle/liquidjs
3
+ * (c) 2016-2025 harttle
4
4
  * Released under the MIT License.
5
5
  */
6
6
  class Token {
@@ -89,6 +89,10 @@ function toArray(val) {
89
89
  function toValue(value) {
90
90
  return (value instanceof Drop && isFunction(value.valueOf)) ? value.valueOf() : value;
91
91
  }
92
+ function toNumber(value) {
93
+ value = Number(value);
94
+ return isNaN(value) ? 0 : value;
95
+ }
92
96
  function isNumber(value) {
93
97
  return typeof value === 'number';
94
98
  }
@@ -107,6 +111,9 @@ function isArray(value) {
107
111
  // be compatible with IE 8
108
112
  return toString$1.call(value) === '[object Array]';
109
113
  }
114
+ function isArrayLike(value) {
115
+ return value && isNumber(value.length);
116
+ }
110
117
  function isIterable(value) {
111
118
  return isObject(value) && Symbol.iterator in value;
112
119
  }
@@ -188,14 +195,25 @@ function caseInsensitiveCompare(a, b) {
188
195
  return 0;
189
196
  }
190
197
  function argumentsToValue(fn) {
191
- return (...args) => fn(...args.map(toValue));
198
+ return function (...args) { return fn.call(this, ...args.map(toValue)); };
192
199
  }
193
200
  function escapeRegExp(text) {
194
201
  return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
202
+ }
203
+ /** Return an array containing unique elements from _array_. Works with nested arrays and objects. */
204
+ function* strictUniq(array) {
205
+ const seen = new Set();
206
+ for (const element of array) {
207
+ const key = JSON.stringify(element);
208
+ if (!seen.has(key)) {
209
+ seen.add(key);
210
+ yield element;
211
+ }
212
+ }
195
213
  }
196
214
 
197
215
  /**
198
- * targeting ES5, extends Error won't create a proper prototype chain, need a trait to kee track of classes
216
+ * targeting ES5, extends Error won't create a proper prototype chain, need a trait to keep track of classes
199
217
  */
200
218
  const TRAIT = '__liquidClass__';
201
219
  class LiquidError extends Error {
@@ -249,6 +267,19 @@ class RenderError extends LiquidError {
249
267
  return obj.name === 'RenderError';
250
268
  }
251
269
  }
270
+ class LiquidErrors extends LiquidError {
271
+ constructor(errors) {
272
+ super(errors[0], errors[0].token);
273
+ this.errors = errors;
274
+ this.name = 'LiquidErrors';
275
+ const s = errors.length > 1 ? 's' : '';
276
+ this.message = `${errors.length} error${s} found`;
277
+ super.update();
278
+ }
279
+ static is(obj) {
280
+ return obj.name === 'LiquidErrors';
281
+ }
282
+ }
252
283
  class UndefinedVariableError extends LiquidError {
253
284
  constructor(err, token) {
254
285
  super(err, token);
@@ -327,6 +358,9 @@ function assert(predicate, message) {
327
358
  : (message || `expect ${predicate} to be true`);
328
359
  throw new AssertionError(msg);
329
360
  }
361
+ }
362
+ function assertEmpty(predicate, message = `unexpected ${JSON.stringify(predicate)}`) {
363
+ assert(!predicate, message);
330
364
  }
331
365
 
332
366
  class NullDrop extends Drop {
@@ -376,6 +410,9 @@ class EmptyDrop extends Drop {
376
410
  valueOf() {
377
411
  return '';
378
412
  }
413
+ static is(value) {
414
+ return value instanceof EmptyDrop;
415
+ }
379
416
  }
380
417
 
381
418
  class BlankDrop extends EmptyDrop {
@@ -388,6 +425,9 @@ class BlankDrop extends EmptyDrop {
388
425
  return /^\s*$/.test(value);
389
426
  return super.equals(value);
390
427
  }
428
+ static is(value) {
429
+ return value instanceof BlankDrop;
430
+ }
391
431
  }
392
432
 
393
433
  class ForloopDrop extends Drop {
@@ -440,7 +480,12 @@ class BlockDrop extends Drop {
440
480
  }
441
481
 
442
482
  function isComparable(arg) {
443
- return arg && isFunction(arg.equals);
483
+ return (arg &&
484
+ isFunction(arg.equals) &&
485
+ isFunction(arg.gt) &&
486
+ isFunction(arg.geq) &&
487
+ isFunction(arg.lt) &&
488
+ isFunction(arg.leq));
444
489
  }
445
490
 
446
491
  const nil = new NullDrop();
@@ -560,18 +605,6 @@ function toValueSync(val) {
560
605
  }
561
606
 
562
607
  const rFormat = /%([-_0^#:]+)?(\d+)?([EO])?(.)/;
563
- const monthNames = [
564
- 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August',
565
- 'September', 'October', 'November', 'December'
566
- ];
567
- const dayNames = [
568
- 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'
569
- ];
570
- const monthNamesShort = monthNames.map(abbr);
571
- const dayNamesShort = dayNames.map(abbr);
572
- function abbr(str) {
573
- return str.slice(0, 3);
574
- }
575
608
  // prototype extensions
576
609
  function daysInMonth(d) {
577
610
  const feb = isLeapYear(d) ? 29 : 28;
@@ -626,19 +659,7 @@ const padWidths = {
626
659
  U: 2,
627
660
  W: 2
628
661
  };
629
- // default to '0'
630
- const padChars = {
631
- a: ' ',
632
- A: ' ',
633
- b: ' ',
634
- B: ' ',
635
- c: ' ',
636
- e: ' ',
637
- k: ' ',
638
- l: ' ',
639
- p: ' ',
640
- P: ' '
641
- };
662
+ const padSpaceChars = new Set('aAbBceklpP');
642
663
  function getTimezoneOffset(d, opts) {
643
664
  const nOffset = Math.abs(d.getTimezoneOffset());
644
665
  const h = Math.floor(nOffset / 60);
@@ -649,10 +670,10 @@ function getTimezoneOffset(d, opts) {
649
670
  padStart(m, 2, '0');
650
671
  }
651
672
  const formatCodes = {
652
- a: (d) => dayNamesShort[d.getDay()],
653
- A: (d) => dayNames[d.getDay()],
654
- b: (d) => monthNamesShort[d.getMonth()],
655
- B: (d) => monthNames[d.getMonth()],
673
+ a: (d) => d.getShortWeekdayName(),
674
+ A: (d) => d.getLongWeekdayName(),
675
+ b: (d) => d.getShortMonthName(),
676
+ B: (d) => d.getLongMonthName(),
656
677
  c: (d) => d.toLocaleString(),
657
678
  C: (d) => century(d),
658
679
  d: (d) => d.getDate(),
@@ -684,12 +705,7 @@ const formatCodes = {
684
705
  y: (d) => d.getFullYear().toString().slice(2, 4),
685
706
  Y: (d) => d.getFullYear(),
686
707
  z: getTimezoneOffset,
687
- Z: (d, opts) => {
688
- if (d.getTimezoneName) {
689
- return d.getTimezoneName() || getTimezoneOffset(d, opts);
690
- }
691
- return (typeof Intl !== 'undefined' ? Intl.DateTimeFormat().resolvedOptions().timeZone : '');
692
- },
708
+ Z: (d, opts) => d.getTimeZoneName() || getTimezoneOffset(d, opts),
693
709
  't': () => '\t',
694
710
  'n': () => '\n',
695
711
  '%': () => '%'
@@ -715,7 +731,7 @@ function format(d, match) {
715
731
  for (const flag of flagStr)
716
732
  flags[flag] = true;
717
733
  let ret = String(convert(d, { flags, width, modifier }));
718
- let padChar = padChars[conversion] || '0';
734
+ let padChar = padSpaceChars.has(conversion) ? ' ' : '0';
719
735
  let padWidth = width || padWidths[conversion] || 0;
720
736
  if (flags['^'])
721
737
  ret = ret.toUpperCase();
@@ -730,9 +746,26 @@ function format(d, match) {
730
746
  return padStart(ret, padWidth, padChar);
731
747
  }
732
748
 
749
+ function getDateTimeFormat() {
750
+ return (typeof Intl !== 'undefined' ? Intl.DateTimeFormat : undefined);
751
+ }
752
+
733
753
  // one minute in milliseconds
734
754
  const OneMinute = 60000;
735
- const ISO8601_TIMEZONE_PATTERN = /([zZ]|([+-])(\d{2}):(\d{2}))$/;
755
+ /**
756
+ * Need support both ISO8601 and RFC2822 as in major browsers & NodeJS
757
+ * RFC2822: https://datatracker.ietf.org/doc/html/rfc2822#section-3.3
758
+ */
759
+ const TIMEZONE_PATTERN = /([zZ]|([+-])(\d{2}):?(\d{2}))$/;
760
+ const monthNames = [
761
+ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August',
762
+ 'September', 'October', 'November', 'December'
763
+ ];
764
+ const monthNamesShort = monthNames.map(name => name.slice(0, 3));
765
+ const dayNames = [
766
+ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'
767
+ ];
768
+ const dayNamesShort = dayNames.map(name => name.slice(0, 3));
736
769
  /**
737
770
  * A date implementation with timezone info, just like Ruby date
738
771
  *
@@ -740,12 +773,16 @@ const ISO8601_TIMEZONE_PATTERN = /([zZ]|([+-])(\d{2}):(\d{2}))$/;
740
773
  * - create a Date offset by it's timezone difference, avoiding overriding a bunch of methods
741
774
  * - rewrite getTimezoneOffset() to trick strftime
742
775
  */
743
- class TimezoneDate {
744
- constructor(init, timezone) {
745
- this.date = init instanceof TimezoneDate
746
- ? init.date
747
- : new Date(init);
748
- this.timezoneOffset = isString(timezone) ? TimezoneDate.getTimezoneOffset(timezone, this.date) : timezone;
776
+ class LiquidDate {
777
+ constructor(init, locale, timezone) {
778
+ this.locale = locale;
779
+ this.DateTimeFormat = getDateTimeFormat();
780
+ this.date = new Date(init);
781
+ this.timezoneFixed = timezone !== undefined;
782
+ if (timezone === undefined) {
783
+ timezone = this.date.getTimezoneOffset();
784
+ }
785
+ this.timezoneOffset = isString(timezone) ? LiquidDate.getTimezoneOffset(timezone, this.date) : timezone;
749
786
  this.timezoneName = isString(timezone) ? timezone : '';
750
787
  const diff = (this.date.getTimezoneOffset() - this.timezoneOffset) * OneMinute;
751
788
  const time = this.date.getTime() + diff;
@@ -793,8 +830,34 @@ class TimezoneDate {
793
830
  getTimezoneOffset() {
794
831
  return this.timezoneOffset;
795
832
  }
796
- getTimezoneName() {
797
- return this.timezoneName;
833
+ getTimeZoneName() {
834
+ if (this.timezoneFixed)
835
+ return this.timezoneName;
836
+ if (!this.DateTimeFormat)
837
+ return;
838
+ return this.DateTimeFormat().resolvedOptions().timeZone;
839
+ }
840
+ getLongMonthName() {
841
+ var _a;
842
+ return (_a = this.format({ month: 'long' })) !== null && _a !== void 0 ? _a : monthNames[this.getMonth()];
843
+ }
844
+ getShortMonthName() {
845
+ var _a;
846
+ return (_a = this.format({ month: 'short' })) !== null && _a !== void 0 ? _a : monthNamesShort[this.getMonth()];
847
+ }
848
+ getLongWeekdayName() {
849
+ var _a;
850
+ return (_a = this.format({ weekday: 'long' })) !== null && _a !== void 0 ? _a : dayNames[this.displayDate.getDay()];
851
+ }
852
+ getShortWeekdayName() {
853
+ var _a;
854
+ return (_a = this.format({ weekday: 'short' })) !== null && _a !== void 0 ? _a : dayNamesShort[this.displayDate.getDay()];
855
+ }
856
+ valid() {
857
+ return !isNaN(this.getTime());
858
+ }
859
+ format(options) {
860
+ return this.DateTimeFormat && this.DateTimeFormat(this.locale, options).format(this.displayDate);
798
861
  }
799
862
  /**
800
863
  * Create a Date object fixed to it's declared Timezone. Both
@@ -809,21 +872,21 @@ class TimezoneDate {
809
872
  * we create a different Date to trick strftime, it's both simpler and more performant.
810
873
  * Given that a template is expected to be parsed fewer times than rendered.
811
874
  */
812
- static createDateFixedToTimezone(dateString) {
813
- const m = dateString.match(ISO8601_TIMEZONE_PATTERN);
875
+ static createDateFixedToTimezone(dateString, locale) {
876
+ const m = dateString.match(TIMEZONE_PATTERN);
814
877
  // representing a UTC timestamp
815
878
  if (m && m[1] === 'Z') {
816
- return new TimezoneDate(+new Date(dateString), 0);
879
+ return new LiquidDate(+new Date(dateString), locale, 0);
817
880
  }
818
881
  // has a timezone specified
819
882
  if (m && m[2] && m[3] && m[4]) {
820
883
  const [, , sign, hours, minutes] = m;
821
884
  const offset = (sign === '+' ? -1 : 1) * (parseInt(hours, 10) * 60 + parseInt(minutes, 10));
822
- return new TimezoneDate(+new Date(dateString), offset);
885
+ return new LiquidDate(+new Date(dateString), locale, offset);
823
886
  }
824
- return new Date(dateString);
887
+ return new LiquidDate(dateString, locale);
825
888
  }
826
- static getTimezoneOffset(timezoneName, date = new Date()) {
889
+ static getTimezoneOffset(timezoneName, date) {
827
890
  const localDateString = date.toLocaleString('en-US', { timeZone: timezoneName });
828
891
  const utcDateString = date.toLocaleString('en-US', { timeZone: 'UTC' });
829
892
  const localDate = new Date(localDateString);
@@ -832,6 +895,23 @@ class TimezoneDate {
832
895
  }
833
896
  }
834
897
 
898
+ class Limiter {
899
+ constructor(resource, limit) {
900
+ this.base = 0;
901
+ this.message = `${resource} limit exceeded`;
902
+ this.limit = limit;
903
+ }
904
+ use(count) {
905
+ count = toNumber(count);
906
+ assert(this.base + count <= this.limit, this.message);
907
+ this.base += count;
908
+ }
909
+ check(count) {
910
+ count = toNumber(count);
911
+ assert(count <= this.limit, this.message);
912
+ }
913
+ }
914
+
835
915
  class DelimitedToken extends Token {
836
916
  constructor(kind, [contentBegin, contentEnd], input, begin, end, trimLeft, trimRight, file) {
837
917
  super(kind, input, begin, end, file);
@@ -912,16 +992,6 @@ class IdentifierToken extends Token {
912
992
  this.file = file;
913
993
  this.content = this.getText();
914
994
  }
915
- isNumber(allowSign = false) {
916
- const begin = allowSign && TYPES[this.input.charCodeAt(this.begin)] & SIGN
917
- ? this.begin + 1
918
- : this.begin;
919
- for (let i = begin; i < this.end; i++) {
920
- if (!(TYPES[this.input.charCodeAt(i)] & NUMBER))
921
- return false;
922
- }
923
- return true;
924
- }
925
995
  }
926
996
 
927
997
  class LiteralToken extends Token {
@@ -1089,7 +1159,9 @@ class LiquidTagToken extends DelimitedToken {
1089
1159
  this.name = this.tokenizer.readTagName();
1090
1160
  this.tokenizer.assert(this.name, 'illegal liquid tag syntax');
1091
1161
  this.tokenizer.skipBlank();
1092
- this.args = this.tokenizer.remaining();
1162
+ }
1163
+ get args() {
1164
+ return this.tokenizer.input.slice(this.tokenizer.p, this.contentRange[1]);
1093
1165
  }
1094
1166
  }
1095
1167
 
@@ -1110,6 +1182,15 @@ class FilteredValueToken extends Token {
1110
1182
  }
1111
1183
  }
1112
1184
 
1185
+ const polyfill = {
1186
+ now: () => Date.now()
1187
+ };
1188
+ function getPerformance() {
1189
+ return (typeof global === 'object' && global.performance) ||
1190
+ (typeof window === 'object' && window.performance) ||
1191
+ polyfill;
1192
+ }
1193
+
1113
1194
  class SimpleEmitter {
1114
1195
  constructor() {
1115
1196
  this.buffer = '';
@@ -1157,20 +1238,28 @@ class Render {
1157
1238
  if (!emitter) {
1158
1239
  emitter = ctx.opts.keepOutputType ? new KeepingTypeEmitter() : new SimpleEmitter();
1159
1240
  }
1241
+ const errors = [];
1160
1242
  for (const tpl of templates) {
1243
+ ctx.renderLimit.check(getPerformance().now());
1161
1244
  try {
1162
1245
  // if tpl.render supports emitter, it'll return empty `html`
1163
1246
  const html = yield tpl.render(ctx, emitter);
1164
1247
  // if not, it'll return an `html`, write to the emitter for it
1165
1248
  html && emitter.write(html);
1166
- if (emitter['break'] || emitter['continue'])
1249
+ if (ctx.breakCalled || ctx.continueCalled)
1167
1250
  break;
1168
1251
  }
1169
1252
  catch (e) {
1170
- const err = RenderError.is(e) ? e : new RenderError(e, tpl);
1171
- throw err;
1253
+ const err = LiquidError.is(e) ? e : new RenderError(e, tpl);
1254
+ if (ctx.opts.catchAllErrors)
1255
+ errors.push(err);
1256
+ else
1257
+ throw err;
1172
1258
  }
1173
1259
  }
1260
+ if (errors.length) {
1261
+ throw new LiquidErrors(errors);
1262
+ }
1174
1263
  return emitter.buffer;
1175
1264
  }
1176
1265
  }
@@ -1241,6 +1330,7 @@ function evalQuotedToken(token) {
1241
1330
  function* evalRangeToken(token, ctx) {
1242
1331
  const low = yield evalToken(token.lhs, ctx);
1243
1332
  const high = yield evalToken(token.rhs, ctx);
1333
+ ctx.memoryLimit.use(high - low + 1);
1244
1334
  return range(+low, +high + 1);
1245
1335
  }
1246
1336
  function* toPostfix(tokens) {
@@ -1264,6 +1354,7 @@ function isTruthy(val, ctx) {
1264
1354
  return !isFalsy(val, ctx);
1265
1355
  }
1266
1356
  function isFalsy(val, ctx) {
1357
+ val = toValue(val);
1267
1358
  if (ctx.opts.jsTruthy) {
1268
1359
  return !val;
1269
1360
  }
@@ -1331,6 +1422,9 @@ function arrayEquals(lhs, rhs) {
1331
1422
  if (lhs.length !== rhs.length)
1332
1423
  return false;
1333
1424
  return !lhs.some((value, i) => !equals(value, rhs[i]));
1425
+ }
1426
+ function arrayIncludes(arr, item) {
1427
+ return arr.some(value => equals(value, item));
1334
1428
  }
1335
1429
 
1336
1430
  class Node {
@@ -1522,22 +1616,30 @@ const unescapeMap = {
1522
1616
  '&#39;': "'"
1523
1617
  };
1524
1618
  function escape(str) {
1525
- return stringify(str).replace(/&|<|>|"|'/g, m => escapeMap[m]);
1619
+ str = stringify(str);
1620
+ this.context.memoryLimit.use(str.length);
1621
+ return str.replace(/&|<|>|"|'/g, m => escapeMap[m]);
1526
1622
  }
1527
1623
  function xml_escape(str) {
1528
- return escape(str);
1624
+ return escape.call(this, str);
1529
1625
  }
1530
1626
  function unescape(str) {
1531
- return stringify(str).replace(/&(amp|lt|gt|#34|#39);/g, m => unescapeMap[m]);
1627
+ str = stringify(str);
1628
+ this.context.memoryLimit.use(str.length);
1629
+ return str.replace(/&(amp|lt|gt|#34|#39);/g, m => unescapeMap[m]);
1532
1630
  }
1533
1631
  function escape_once(str) {
1534
- return escape(unescape(stringify(str)));
1632
+ return escape.call(this, unescape.call(this, str));
1535
1633
  }
1536
1634
  function newline_to_br(v) {
1537
- return stringify(v).replace(/\r?\n/gm, '<br />\n');
1635
+ const str = stringify(v);
1636
+ this.context.memoryLimit.use(str.length);
1637
+ return str.replace(/\r?\n/gm, '<br />\n');
1538
1638
  }
1539
1639
  function strip_html(v) {
1540
- return stringify(v).replace(/<script[\s\S]*?<\/script>|<style[\s\S]*?<\/style>|<.*?>|<!--[\s\S]*?-->/g, '');
1640
+ const str = stringify(v);
1641
+ this.context.memoryLimit.use(str.length);
1642
+ return str.replace(/<script[\s\S]*?<\/script>|<style[\s\S]*?<\/style>|<.*?>|<!--[\s\S]*?-->/g, '');
1541
1643
  }
1542
1644
 
1543
1645
  var htmlFilters = /*#__PURE__*/Object.freeze({
@@ -1549,18 +1651,68 @@ var htmlFilters = /*#__PURE__*/Object.freeze({
1549
1651
  strip_html: strip_html
1550
1652
  });
1551
1653
 
1654
+ class MapFS {
1655
+ constructor(mapping) {
1656
+ this.mapping = mapping;
1657
+ this.sep = '/';
1658
+ }
1659
+ exists(filepath) {
1660
+ return __awaiter(this, void 0, void 0, function* () {
1661
+ return this.existsSync(filepath);
1662
+ });
1663
+ }
1664
+ existsSync(filepath) {
1665
+ return !isNil(this.mapping[filepath]);
1666
+ }
1667
+ readFile(filepath) {
1668
+ return __awaiter(this, void 0, void 0, function* () {
1669
+ return this.readFileSync(filepath);
1670
+ });
1671
+ }
1672
+ readFileSync(filepath) {
1673
+ const content = this.mapping[filepath];
1674
+ if (isNil(content))
1675
+ throw new Error(`ENOENT: ${filepath}`);
1676
+ return content;
1677
+ }
1678
+ dirname(filepath) {
1679
+ const segments = filepath.split(this.sep);
1680
+ segments.pop();
1681
+ return segments.join(this.sep);
1682
+ }
1683
+ resolve(dir, file, ext) {
1684
+ file += ext;
1685
+ if (dir === '.')
1686
+ return file;
1687
+ const segments = dir.split(/\/+/);
1688
+ for (const segment of file.split(this.sep)) {
1689
+ if (segment === '.' || segment === '')
1690
+ continue;
1691
+ else if (segment === '..') {
1692
+ if (segments.length > 1 || segments[0] !== '')
1693
+ segments.pop();
1694
+ }
1695
+ else
1696
+ segments.push(segment);
1697
+ }
1698
+ return segments.join(this.sep);
1699
+ }
1700
+ }
1701
+
1552
1702
  const defaultOptions = {
1553
1703
  root: ['.'],
1554
1704
  layouts: ['.'],
1555
1705
  partials: ['.'],
1556
1706
  relativeReference: true,
1557
1707
  jekyllInclude: false,
1708
+ keyValueSeparator: ':',
1558
1709
  cache: undefined,
1559
1710
  extname: '',
1560
1711
  fs: fs,
1561
1712
  dynamicPartials: true,
1562
1713
  jsTruthy: false,
1563
1714
  dateFormat: '%A, %B %-e, %Y at %-l:%M %P %z',
1715
+ locale: '',
1564
1716
  trimTagRight: false,
1565
1717
  trimTagLeft: false,
1566
1718
  trimOutputRight: false,
@@ -1577,9 +1729,13 @@ const defaultOptions = {
1577
1729
  lenientIf: false,
1578
1730
  globals: {},
1579
1731
  keepOutputType: false,
1580
- operators: defaultOperators
1732
+ operators: defaultOperators,
1733
+ memoryLimit: Infinity,
1734
+ parseLimit: Infinity,
1735
+ renderLimit: Infinity
1581
1736
  };
1582
1737
  function normalize(options) {
1738
+ var _a, _b;
1583
1739
  if (options.hasOwnProperty('root')) {
1584
1740
  if (!options.hasOwnProperty('partials'))
1585
1741
  options.partials = options.root;
@@ -1605,6 +1761,14 @@ function normalize(options) {
1605
1761
  options.partials = normalizeDirectoryList(options.partials);
1606
1762
  options.layouts = normalizeDirectoryList(options.layouts);
1607
1763
  options.outputEscape = options.outputEscape && getOutputEscapeFunction(options.outputEscape);
1764
+ if (!options.locale) {
1765
+ options.locale = (_b = (_a = getDateTimeFormat()) === null || _a === void 0 ? void 0 : _a().resolvedOptions().locale) !== null && _b !== void 0 ? _b : 'en-US';
1766
+ }
1767
+ if (options.templates) {
1768
+ options.fs = new MapFS(options.templates);
1769
+ options.relativeReference = true;
1770
+ options.root = options.partials = options.layouts = '.';
1771
+ }
1608
1772
  return options;
1609
1773
  }
1610
1774
  function getOutputEscapeFunction(nameOrFunction) {
@@ -1731,9 +1895,7 @@ class Tokenizer {
1731
1895
  this.skipBlank();
1732
1896
  if (this.end())
1733
1897
  return null;
1734
- this.assert(this.peek() === '|', `expected "|" before filter`);
1735
- this.p++;
1736
- const begin = this.p;
1898
+ this.assert(this.read() === '|', `expected "|" before filter`);
1737
1899
  const name = this.readIdentifier();
1738
1900
  if (!name.size()) {
1739
1901
  this.assert(this.end(), `expected filter name`);
@@ -1754,7 +1916,7 @@ class Tokenizer {
1754
1916
  else {
1755
1917
  throw this.error('expected ":" after filter name');
1756
1918
  }
1757
- return new FilterToken(name.getText(), args, this.input, begin, this.p, this.file);
1919
+ return new FilterToken(name.getText(), args, this.input, name.begin, this.p, this.file);
1758
1920
  }
1759
1921
  readFilterArg() {
1760
1922
  const key = this.readValue();
@@ -1795,7 +1957,7 @@ class Tokenizer {
1795
1957
  }
1796
1958
  return new HTMLToken(this.input, begin, this.p, this.file);
1797
1959
  }
1798
- readTagToken(options = defaultOptions) {
1960
+ readTagToken(options) {
1799
1961
  const { file, input } = this;
1800
1962
  const begin = this.p;
1801
1963
  if (this.readToDelimiter(options.tagDelimiterRight) === -1) {
@@ -1926,7 +2088,7 @@ class Tokenizer {
1926
2088
  return;
1927
2089
  let value;
1928
2090
  this.skipBlank();
1929
- const sep = jekyllStyle ? '=' : ':';
2091
+ const sep = isString(jekyllStyle) ? jekyllStyle : (jekyllStyle ? '=' : ':');
1930
2092
  if (this.peek() === sep) {
1931
2093
  ++this.p;
1932
2094
  value = this.readValue();
@@ -1942,6 +2104,9 @@ class Tokenizer {
1942
2104
  end() {
1943
2105
  return this.p >= this.N;
1944
2106
  }
2107
+ read() {
2108
+ return this.input[this.p++];
2109
+ }
1945
2110
  readTo(end) {
1946
2111
  while (this.p < this.N) {
1947
2112
  ++this.p;
@@ -2165,9 +2330,9 @@ class Tag extends TemplateImpl {
2165
2330
  * hash['reversed'] === undefined
2166
2331
  */
2167
2332
  class Hash {
2168
- constructor(markup, jekyllStyle) {
2333
+ constructor(input, jekyllStyle) {
2169
2334
  this.hash = {};
2170
- const tokenizer = new Tokenizer(markup, {});
2335
+ const tokenizer = input instanceof Tokenizer ? input : new Tokenizer(input, {});
2171
2336
  for (const hash of tokenizer.readHashes(jekyllStyle)) {
2172
2337
  this.hash[hash.name.content] = hash.value;
2173
2338
  }
@@ -2190,7 +2355,7 @@ function createTagClass(options) {
2190
2355
  }
2191
2356
  }
2192
2357
  *render(ctx, emitter) {
2193
- const hash = (yield new Hash(this.token.args).render(ctx));
2358
+ const hash = (yield new Hash(this.token.args, ctx.opts.keyValueSeparator).render(ctx));
2194
2359
  return yield options.render.call(this, ctx, emitter, hash);
2195
2360
  }
2196
2361
  };
@@ -2201,13 +2366,14 @@ function isKeyValuePair(arr) {
2201
2366
  }
2202
2367
 
2203
2368
  class Filter {
2204
- constructor(name, options, args, liquid) {
2205
- this.name = name;
2369
+ constructor(token, options, liquid) {
2370
+ this.token = token;
2371
+ this.name = token.name;
2206
2372
  this.handler = isFunction(options)
2207
2373
  ? options
2208
2374
  : (isFunction(options === null || options === void 0 ? void 0 : options.handler) ? options.handler : identify);
2209
2375
  this.raw = !isFunction(options) && !!(options === null || options === void 0 ? void 0 : options.raw);
2210
- this.args = args;
2376
+ this.args = token.args;
2211
2377
  this.liquid = liquid;
2212
2378
  }
2213
2379
  *render(value, context) {
@@ -2218,7 +2384,7 @@ class Filter {
2218
2384
  else
2219
2385
  argv.push(yield evalToken(arg, context));
2220
2386
  }
2221
- return yield this.handler.apply({ context, liquid: this.liquid }, [value, ...argv]);
2387
+ return yield this.handler.apply({ context, token: this.token, liquid: this.liquid }, [value, ...argv]);
2222
2388
  }
2223
2389
  }
2224
2390
 
@@ -2232,7 +2398,7 @@ class Value {
2232
2398
  ? new Tokenizer(input, liquid.options.operators).readFilteredValue()
2233
2399
  : input;
2234
2400
  this.initial = token.initial;
2235
- this.filters = token.filters.map(({ name, args }) => new Filter(name, this.getFilter(liquid, name), args, liquid));
2401
+ this.filters = token.filters.map(token => new Filter(token, this.getFilter(liquid, token.name), liquid));
2236
2402
  }
2237
2403
  *value(ctx, lenient) {
2238
2404
  lenient = lenient || (ctx.opts.lenientIf && this.filters.length > 0 && this.filters[0].name === 'default');
@@ -2258,13 +2424,17 @@ class Output extends TemplateImpl {
2258
2424
  const filters = this.value.filters;
2259
2425
  const outputEscape = liquid.options.outputEscape;
2260
2426
  if (!((_a = filters[filters.length - 1]) === null || _a === void 0 ? void 0 : _a.raw) && outputEscape) {
2261
- filters.push(new Filter(toString.call(outputEscape), outputEscape, [], liquid));
2427
+ const token = new FilterToken(toString.call(outputEscape), [], '', 0, 0);
2428
+ filters.push(new Filter(token, outputEscape, liquid));
2262
2429
  }
2263
2430
  }
2264
2431
  *render(ctx, emitter) {
2265
2432
  const val = yield this.value.value(ctx, false);
2266
2433
  emitter.write(val);
2267
2434
  }
2435
+ *arguments() {
2436
+ yield this.value;
2437
+ }
2268
2438
  }
2269
2439
 
2270
2440
  class HTML extends TemplateImpl {
@@ -2277,6 +2447,336 @@ class HTML extends TemplateImpl {
2277
2447
  }
2278
2448
  }
2279
2449
 
2450
+ /**
2451
+ * A variable's segments and location, which can be coerced to a string.
2452
+ */
2453
+ class Variable {
2454
+ constructor(segments, location) {
2455
+ this.segments = segments;
2456
+ this.location = location;
2457
+ }
2458
+ toString() {
2459
+ return segmentsString(this.segments, true);
2460
+ }
2461
+ /** Return this variable's segments as an array, possibly with nested arrays for nested paths. */
2462
+ toArray() {
2463
+ function* _visit(...segments) {
2464
+ for (const segment of segments) {
2465
+ if (segment instanceof Variable) {
2466
+ yield Array.from(_visit(...segment.segments));
2467
+ }
2468
+ else {
2469
+ yield segment;
2470
+ }
2471
+ }
2472
+ }
2473
+ return Array.from(_visit(...this.segments));
2474
+ }
2475
+ }
2476
+ /**
2477
+ * Group variables by the string representation of their root segment.
2478
+ */
2479
+ class VariableMap {
2480
+ constructor() {
2481
+ this.map = new Map();
2482
+ }
2483
+ get(key) {
2484
+ const k = segmentsString([key.segments[0]]);
2485
+ if (!this.map.has(k)) {
2486
+ this.map.set(k, []);
2487
+ }
2488
+ return this.map.get(k);
2489
+ }
2490
+ has(key) {
2491
+ return this.map.has(segmentsString([key.segments[0]]));
2492
+ }
2493
+ push(variable) {
2494
+ this.get(variable).push(variable);
2495
+ }
2496
+ asObject() {
2497
+ return Object.fromEntries(this.map);
2498
+ }
2499
+ }
2500
+ const defaultStaticAnalysisOptions = {
2501
+ partials: true
2502
+ };
2503
+ function* _analyze(templates, partials, sync) {
2504
+ const variables = new VariableMap();
2505
+ const globals = new VariableMap();
2506
+ const locals = new VariableMap();
2507
+ const rootScope = new DummyScope(new Set());
2508
+ // Names of partial templates that we've already analyzed.
2509
+ const seen = new Set();
2510
+ function updateVariables(variable, scope) {
2511
+ variables.push(variable);
2512
+ const aliased = scope.alias(variable);
2513
+ if (aliased !== undefined) {
2514
+ const root = aliased.segments[0];
2515
+ // TODO: What if a a template renders a rendered template? Do we need scope.parent?
2516
+ if (isString(root) && !rootScope.has(root)) {
2517
+ globals.push(aliased);
2518
+ }
2519
+ }
2520
+ else {
2521
+ const root = variable.segments[0];
2522
+ if (isString(root) && !scope.has(root)) {
2523
+ globals.push(variable);
2524
+ }
2525
+ }
2526
+ // Recurse for nested Variables
2527
+ for (const segment of variable.segments) {
2528
+ if (segment instanceof Variable) {
2529
+ updateVariables(segment, scope);
2530
+ }
2531
+ }
2532
+ }
2533
+ function* visit(template, scope) {
2534
+ if (template.arguments) {
2535
+ for (const arg of template.arguments()) {
2536
+ for (const variable of extractVariables(arg)) {
2537
+ updateVariables(variable, scope);
2538
+ }
2539
+ }
2540
+ }
2541
+ if (template.localScope) {
2542
+ for (const ident of template.localScope()) {
2543
+ scope.add(ident.content);
2544
+ scope.deleteAlias(ident.content);
2545
+ const [row, col] = ident.getPosition();
2546
+ locals.push(new Variable([ident.content], { row, col, file: ident.file }));
2547
+ }
2548
+ }
2549
+ if (template.children) {
2550
+ if (template.partialScope) {
2551
+ const partial = template.partialScope();
2552
+ if (partial === undefined) {
2553
+ // Layouts, for example, can have children that are not partials.
2554
+ for (const child of (yield template.children(partials, sync))) {
2555
+ yield visit(child, scope);
2556
+ }
2557
+ return;
2558
+ }
2559
+ if (seen.has(partial.name))
2560
+ return;
2561
+ const partialScopeNames = new Set();
2562
+ const partialScope = partial.isolated
2563
+ ? new DummyScope(partialScopeNames)
2564
+ : scope.push(partialScopeNames);
2565
+ for (const name of partial.scope) {
2566
+ if (isString(name)) {
2567
+ partialScopeNames.add(name);
2568
+ }
2569
+ else {
2570
+ const [alias, argument] = name;
2571
+ partialScopeNames.add(alias);
2572
+ const variables = Array.from(extractVariables(argument));
2573
+ if (variables.length) {
2574
+ partialScope.setAlias(alias, variables[0].segments);
2575
+ }
2576
+ }
2577
+ }
2578
+ for (const child of (yield template.children(partials, sync))) {
2579
+ yield visit(child, partialScope);
2580
+ seen.add(partial.name);
2581
+ }
2582
+ partialScope.pop();
2583
+ }
2584
+ else {
2585
+ if (template.blockScope) {
2586
+ scope.push(new Set(template.blockScope()));
2587
+ }
2588
+ for (const child of (yield template.children(partials, sync))) {
2589
+ yield visit(child, scope);
2590
+ }
2591
+ if (template.blockScope) {
2592
+ scope.pop();
2593
+ }
2594
+ }
2595
+ }
2596
+ }
2597
+ for (const template of templates) {
2598
+ yield visit(template, rootScope);
2599
+ }
2600
+ return {
2601
+ variables: variables.asObject(),
2602
+ globals: globals.asObject(),
2603
+ locals: locals.asObject()
2604
+ };
2605
+ }
2606
+ /**
2607
+ * Statically analyze a template and report variable usage.
2608
+ */
2609
+ function analyze(template, options = {}) {
2610
+ const opts = Object.assign(Object.assign({}, defaultStaticAnalysisOptions), options);
2611
+ return toPromise(_analyze(template, opts.partials, false));
2612
+ }
2613
+ /**
2614
+ * Statically analyze a template and report variable usage.
2615
+ */
2616
+ function analyzeSync(template, options = {}) {
2617
+ const opts = Object.assign(Object.assign({}, defaultStaticAnalysisOptions), options);
2618
+ return toValueSync(_analyze(template, opts.partials, true));
2619
+ }
2620
+ /**
2621
+ * A stack to manage scopes while traversing templates during static analysis.
2622
+ */
2623
+ class DummyScope {
2624
+ constructor(globals) {
2625
+ this.stack = [{ names: globals, aliases: new Map() }];
2626
+ }
2627
+ /** Return true if `name` is in scope. */
2628
+ has(name) {
2629
+ for (const scope of this.stack) {
2630
+ if (scope.names.has(name)) {
2631
+ return true;
2632
+ }
2633
+ }
2634
+ return false;
2635
+ }
2636
+ push(scope) {
2637
+ this.stack.push({ names: scope, aliases: new Map() });
2638
+ return this;
2639
+ }
2640
+ pop() {
2641
+ var _a;
2642
+ return (_a = this.stack.pop()) === null || _a === void 0 ? void 0 : _a.names;
2643
+ }
2644
+ // Add a name to the template scope.
2645
+ add(name) {
2646
+ this.stack[0].names.add(name);
2647
+ }
2648
+ /** Return the variable that `variable` aliases, or `variable` if it doesn't alias anything. */
2649
+ alias(variable) {
2650
+ const root = variable.segments[0];
2651
+ if (!isString(root))
2652
+ return undefined;
2653
+ const alias = this.getAlias(root);
2654
+ if (alias === undefined)
2655
+ return undefined;
2656
+ return new Variable([...alias, ...variable.segments.slice(1)], variable.location);
2657
+ }
2658
+ // TODO: `from` could be a path with multiple segments, like `include.x`.
2659
+ setAlias(from, to) {
2660
+ this.stack[this.stack.length - 1].aliases.set(from, to);
2661
+ }
2662
+ deleteAlias(name) {
2663
+ this.stack[this.stack.length - 1].aliases.delete(name);
2664
+ }
2665
+ getAlias(name) {
2666
+ for (const scope of this.stack) {
2667
+ if (scope.aliases.has(name)) {
2668
+ return scope.aliases.get(name);
2669
+ }
2670
+ // If a scope has defined `name`, then it masks aliases in parent scopes.
2671
+ if (scope.names.has(name)) {
2672
+ return undefined;
2673
+ }
2674
+ }
2675
+ return undefined;
2676
+ }
2677
+ }
2678
+ function* extractVariables(value) {
2679
+ if (isValueToken(value)) {
2680
+ yield* extractValueTokenVariables(value);
2681
+ }
2682
+ else if (value instanceof Value) {
2683
+ yield* extractFilteredValueVariables(value);
2684
+ }
2685
+ }
2686
+ function* extractFilteredValueVariables(value) {
2687
+ for (const token of value.initial.postfix) {
2688
+ if (isValueToken(token)) {
2689
+ yield* extractValueTokenVariables(token);
2690
+ }
2691
+ }
2692
+ for (const filter of value.filters) {
2693
+ for (const arg of filter.args) {
2694
+ if (isKeyValuePair(arg) && arg[1]) {
2695
+ yield* extractValueTokenVariables(arg[1]);
2696
+ }
2697
+ else if (isValueToken(arg)) {
2698
+ yield* extractValueTokenVariables(arg);
2699
+ }
2700
+ }
2701
+ }
2702
+ }
2703
+ function* extractValueTokenVariables(token) {
2704
+ if (isRangeToken(token)) {
2705
+ yield* extractValueTokenVariables(token.lhs);
2706
+ yield* extractValueTokenVariables(token.rhs);
2707
+ }
2708
+ else if (isPropertyAccessToken(token)) {
2709
+ yield extractPropertyAccessVariable(token);
2710
+ }
2711
+ }
2712
+ function extractPropertyAccessVariable(token) {
2713
+ const segments = [];
2714
+ // token is not guaranteed to have `file` set. We'll try to get it from a prop if not.
2715
+ let file = token.file;
2716
+ // Here we're flattening the first segment of a path if it is a nested path.
2717
+ const root = token.props[0];
2718
+ file = file || root.file;
2719
+ if (isQuotedToken(root) || isNumberToken(root) || isWordToken(root)) {
2720
+ segments.push(root.content);
2721
+ }
2722
+ else if (isPropertyAccessToken(root)) {
2723
+ // Flatten paths that start with a nested path.
2724
+ segments.push(...extractPropertyAccessVariable(root).segments);
2725
+ }
2726
+ for (const prop of token.props.slice(1)) {
2727
+ file = file || prop.file;
2728
+ if (isQuotedToken(prop) || isNumberToken(prop) || isWordToken(prop)) {
2729
+ segments.push(prop.content);
2730
+ }
2731
+ else if (isPropertyAccessToken(prop)) {
2732
+ segments.push(extractPropertyAccessVariable(prop));
2733
+ }
2734
+ }
2735
+ const [row, col] = token.getPosition();
2736
+ return new Variable(segments, {
2737
+ row,
2738
+ col,
2739
+ file
2740
+ });
2741
+ }
2742
+ // This is used to detect segments that can be represented with dot notation
2743
+ // when creating a string representation of VariableSegments.
2744
+ const RE_PROPERTY = /^[\u0080-\uFFFFa-zA-Z_][\u0080-\uFFFFa-zA-Z0-9_-]*$/;
2745
+ /**
2746
+ * Return a string representation of segments using dot notation where possible.
2747
+ * @param segments - The property names and array indices that make up a path to a variable.
2748
+ * @param bracketedRoot - If false (the default), don't surround the root segment with square brackets.
2749
+ */
2750
+ function segmentsString(segments, bracketedRoot = false) {
2751
+ const buf = [];
2752
+ const root = segments[0];
2753
+ if (isString(root)) {
2754
+ if (!bracketedRoot || root.match(RE_PROPERTY)) {
2755
+ buf.push(`${root}`);
2756
+ }
2757
+ else {
2758
+ buf.push(`['${root}']`);
2759
+ }
2760
+ }
2761
+ for (const segment of segments.slice(1)) {
2762
+ if (segment instanceof Variable) {
2763
+ buf.push(`[${segmentsString(segment.segments)}]`);
2764
+ }
2765
+ else if (isString(segment)) {
2766
+ if (segment.match(RE_PROPERTY)) {
2767
+ buf.push(`.${segment}`);
2768
+ }
2769
+ else {
2770
+ buf.push(`['${segment}']`);
2771
+ }
2772
+ }
2773
+ else {
2774
+ buf.push(`[${segment}]`);
2775
+ }
2776
+ }
2777
+ return buf.join('');
2778
+ }
2779
+
2280
2780
  var LookupType;
2281
2781
  (function (LookupType) {
2282
2782
  LookupType["Partials"] = "partials";
@@ -2293,7 +2793,7 @@ class Loader {
2293
2793
  this.shouldLoadRelative = (referencedFile) => rRelativePath.test(referencedFile);
2294
2794
  }
2295
2795
  else {
2296
- this.shouldLoadRelative = (referencedFile) => false;
2796
+ this.shouldLoadRelative = (_referencedFile) => false;
2297
2797
  }
2298
2798
  this.contains = this.options.fs.contains || (() => true);
2299
2799
  }
@@ -2350,8 +2850,11 @@ class Parser {
2350
2850
  this.fs = this.liquid.options.fs;
2351
2851
  this.parseFile = this.cache ? this._parseFileCached : this._parseFile;
2352
2852
  this.loader = new Loader(this.liquid.options);
2853
+ this.parseLimit = new Limiter('parse length', liquid.options.parseLimit);
2353
2854
  }
2354
2855
  parse(html, filepath) {
2856
+ html = String(html);
2857
+ this.parseLimit.use(html.length);
2355
2858
  const tokenizer = new Tokenizer(html, this.liquid.options.operators, filepath);
2356
2859
  const tokens = tokenizer.readTopLevelTokens(this.liquid.options);
2357
2860
  return this.parseTokens(tokens);
@@ -2359,9 +2862,20 @@ class Parser {
2359
2862
  parseTokens(tokens) {
2360
2863
  let token;
2361
2864
  const templates = [];
2865
+ const errors = [];
2362
2866
  while ((token = tokens.shift())) {
2363
- templates.push(this.parseToken(token, tokens));
2867
+ try {
2868
+ templates.push(this.parseToken(token, tokens));
2869
+ }
2870
+ catch (err) {
2871
+ if (this.liquid.options.catchAllErrors)
2872
+ errors.push(err);
2873
+ else
2874
+ throw err;
2875
+ }
2364
2876
  }
2877
+ if (errors.length)
2878
+ throw new LiquidErrors(errors);
2365
2879
  return templates;
2366
2880
  }
2367
2881
  parseToken(token, remainTokens) {
@@ -2369,7 +2883,7 @@ class Parser {
2369
2883
  if (isTagToken(token)) {
2370
2884
  const TagClass = this.liquid.tags[token.name];
2371
2885
  assert(TagClass, `tag "${token.name}" not found`);
2372
- return new TagClass(token, remainTokens, this.liquid);
2886
+ return new TagClass(token, remainTokens, this.liquid, this);
2373
2887
  }
2374
2888
  if (isOutputToken(token)) {
2375
2889
  return new Output(token, this.liquid);
@@ -2407,7 +2921,7 @@ class Parser {
2407
2921
  }
2408
2922
  *_parseFile(file, sync, type = LookupType.Root, currentFile) {
2409
2923
  const filepath = yield this.loader.lookup(file, type, sync, currentFile);
2410
- return this.liquid.parse(sync ? this.fs.readFileSync(filepath) : yield this.fs.readFile(filepath), filepath);
2924
+ return this.parse(sync ? this.fs.readFileSync(filepath) : yield this.fs.readFile(filepath), filepath);
2411
2925
  }
2412
2926
  }
2413
2927
 
@@ -2462,6 +2976,10 @@ function isWordToken(val) {
2462
2976
  function isRangeToken(val) {
2463
2977
  return getKind(val) === TokenKind.Range;
2464
2978
  }
2979
+ function isValueToken(val) {
2980
+ // valueTokenBitMask = TokenKind.Number | TokenKind.Literal | TokenKind.Quoted | TokenKind.PropertyAccess | TokenKind.Range
2981
+ return (getKind(val) & 1667) > 0;
2982
+ }
2465
2983
  function getKind(val) {
2466
2984
  return val ? val.kind : -1;
2467
2985
  }
@@ -2478,24 +2996,29 @@ var typeGuards = /*#__PURE__*/Object.freeze({
2478
2996
  isNumberToken: isNumberToken,
2479
2997
  isPropertyAccessToken: isPropertyAccessToken,
2480
2998
  isWordToken: isWordToken,
2481
- isRangeToken: isRangeToken
2999
+ isRangeToken: isRangeToken,
3000
+ isValueToken: isValueToken
2482
3001
  });
2483
3002
 
2484
3003
  class Context {
2485
- constructor(env = {}, opts = defaultOptions, renderOptions = {}) {
2486
- var _a, _b, _c;
3004
+ constructor(env = {}, opts = defaultOptions, renderOptions = {}, { memoryLimit, renderLimit } = {}) {
3005
+ var _a, _b, _c, _d, _e;
2487
3006
  /**
2488
3007
  * insert a Context-level empty scope,
2489
3008
  * for tags like `{% capture %}` `{% assign %}` to operate
2490
3009
  */
2491
3010
  this.scopes = [{}];
2492
3011
  this.registers = {};
3012
+ this.breakCalled = false;
3013
+ this.continueCalled = false;
2493
3014
  this.sync = !!renderOptions.sync;
2494
3015
  this.opts = opts;
2495
3016
  this.globals = (_a = renderOptions.globals) !== null && _a !== void 0 ? _a : opts.globals;
2496
3017
  this.environments = isObject(env) ? env : Object(env);
2497
3018
  this.strictVariables = (_b = renderOptions.strictVariables) !== null && _b !== void 0 ? _b : this.opts.strictVariables;
2498
3019
  this.ownPropertyOnly = (_c = renderOptions.ownPropertyOnly) !== null && _c !== void 0 ? _c : opts.ownPropertyOnly;
3020
+ this.memoryLimit = memoryLimit !== null && memoryLimit !== void 0 ? memoryLimit : new Limiter('memory alloc', (_d = renderOptions.memoryLimit) !== null && _d !== void 0 ? _d : opts.memoryLimit);
3021
+ this.renderLimit = renderLimit !== null && renderLimit !== void 0 ? renderLimit : new Limiter('template render', getPerformance().now() + ((_e = renderOptions.renderLimit) !== null && _e !== void 0 ? _e : opts.renderLimit));
2499
3022
  }
2500
3023
  getRegister(key) {
2501
3024
  return (this.registers[key] = this.registers[key] || {});
@@ -2523,7 +3046,7 @@ class Context {
2523
3046
  return toValueSync(this._get(paths));
2524
3047
  }
2525
3048
  *_get(paths) {
2526
- const scope = this.findScope(paths[0]);
3049
+ const scope = this.findScope(paths[0]); // first prop should always be a string
2527
3050
  return yield this._getFromScope(scope, paths);
2528
3051
  }
2529
3052
  /**
@@ -2552,6 +3075,16 @@ class Context {
2552
3075
  bottom() {
2553
3076
  return this.scopes[0];
2554
3077
  }
3078
+ spawn(scope = {}) {
3079
+ return new Context(scope, this.opts, {
3080
+ sync: this.sync,
3081
+ globals: this.globals,
3082
+ strictVariables: this.strictVariables
3083
+ }, {
3084
+ renderLimit: this.renderLimit,
3085
+ memoryLimit: this.memoryLimit
3086
+ });
3087
+ }
2555
3088
  findScope(key) {
2556
3089
  for (let i = this.scopes.length - 1; i >= 0; i--) {
2557
3090
  const candidate = this.scopes[i];
@@ -2565,6 +3098,7 @@ class Context {
2565
3098
  }
2566
3099
  function readProperty(obj, key, ownPropertyOnly) {
2567
3100
  obj = toLiquid(obj);
3101
+ key = toValue(key);
2568
3102
  if (isNil(obj))
2569
3103
  return obj;
2570
3104
  if (isArray(obj) && key < 0)
@@ -2583,7 +3117,7 @@ function readProperty(obj, key, ownPropertyOnly) {
2583
3117
  return value;
2584
3118
  }
2585
3119
  function readJSProperty(obj, key, ownPropertyOnly) {
2586
- if (ownPropertyOnly && !Object.hasOwnProperty.call(obj, key) && !(obj instanceof Drop))
3120
+ if (ownPropertyOnly && !hasOwnProperty.call(obj, key) && !(obj instanceof Drop))
2587
3121
  return undefined;
2588
3122
  return obj[key];
2589
3123
  }
@@ -2598,7 +3132,7 @@ function readLast(obj) {
2598
3132
  return obj['last'];
2599
3133
  }
2600
3134
  function readSize(obj) {
2601
- if (obj.hasOwnProperty('size') || obj['size'] !== undefined)
3135
+ if (hasOwnProperty.call(obj, 'size') || obj['size'] !== undefined)
2602
3136
  return obj['size'];
2603
3137
  if (isArray(obj) || isString(obj))
2604
3138
  return obj.length;
@@ -2705,13 +3239,25 @@ var urlFilters = /*#__PURE__*/Object.freeze({
2705
3239
  slugify: slugify
2706
3240
  });
2707
3241
 
2708
- const join = argumentsToValue((v, arg) => toArray(v).join(arg === undefined ? ' ' : arg));
2709
- const last$1 = argumentsToValue((v) => isArray(v) ? last(v) : '');
2710
- const first = argumentsToValue((v) => isArray(v) ? v[0] : '');
2711
- const reverse = argumentsToValue((v) => [...toArray(v)].reverse());
3242
+ const join = argumentsToValue(function (v, arg) {
3243
+ const array = toArray(v);
3244
+ const sep = isNil(arg) ? ' ' : stringify(arg);
3245
+ const complexity = array.length * (1 + sep.length);
3246
+ this.context.memoryLimit.use(complexity);
3247
+ return array.join(sep);
3248
+ });
3249
+ const last$1 = argumentsToValue((v) => isArrayLike(v) ? last(v) : '');
3250
+ const first = argumentsToValue((v) => isArrayLike(v) ? v[0] : '');
3251
+ const reverse = argumentsToValue(function (v) {
3252
+ const array = toArray(v);
3253
+ this.context.memoryLimit.use(array.length);
3254
+ return [...array].reverse();
3255
+ });
2712
3256
  function* sort(arr, property) {
2713
3257
  const values = [];
2714
- for (const item of toArray(arr)) {
3258
+ const array = toArray(arr);
3259
+ this.context.memoryLimit.use(array.length);
3260
+ for (const item of array) {
2715
3261
  values.push([
2716
3262
  item,
2717
3263
  property ? yield this.context._getFromScope(item, stringify(property).split('.'), false) : item
@@ -2728,35 +3274,47 @@ function sort_natural(input, property) {
2728
3274
  const compare = property === undefined
2729
3275
  ? caseInsensitiveCompare
2730
3276
  : (lhs, rhs) => caseInsensitiveCompare(lhs[propertyString], rhs[propertyString]);
2731
- return [...toArray(input)].sort(compare);
3277
+ const array = toArray(input);
3278
+ this.context.memoryLimit.use(array.length);
3279
+ return [...array].sort(compare);
2732
3280
  }
2733
3281
  const size = (v) => (v && v.length) || 0;
2734
3282
  function* map(arr, property) {
2735
3283
  const results = [];
2736
- for (const item of toArray(arr)) {
3284
+ const array = toArray(arr);
3285
+ this.context.memoryLimit.use(array.length);
3286
+ for (const item of array) {
2737
3287
  results.push(yield this.context._getFromScope(item, stringify(property), false));
2738
3288
  }
2739
3289
  return results;
2740
3290
  }
2741
3291
  function* sum(arr, property) {
2742
3292
  let sum = 0;
2743
- for (const item of toArray(arr)) {
3293
+ const array = toArray(arr);
3294
+ for (const item of array) {
2744
3295
  const data = Number(property ? yield this.context._getFromScope(item, stringify(property), false) : item);
2745
3296
  sum += Number.isNaN(data) ? 0 : data;
2746
3297
  }
2747
3298
  return sum;
2748
3299
  }
2749
3300
  function compact(arr) {
2750
- return toArray(arr).filter(x => !isNil(toValue(x)));
3301
+ const array = toArray(arr);
3302
+ this.context.memoryLimit.use(array.length);
3303
+ return array.filter(x => !isNil(toValue(x)));
2751
3304
  }
2752
3305
  function concat(v, arg = []) {
2753
- return toArray(v).concat(toArray(arg));
3306
+ const lhs = toArray(v);
3307
+ const rhs = toArray(arg);
3308
+ this.context.memoryLimit.use(lhs.length + rhs.length);
3309
+ return lhs.concat(rhs);
2754
3310
  }
2755
3311
  function push(v, arg) {
2756
- return concat(v, [arg]);
3312
+ return concat.call(this, v, [arg]);
2757
3313
  }
2758
3314
  function unshift(v, arg) {
2759
- const clone = [...toArray(v)];
3315
+ const array = toArray(v);
3316
+ this.context.memoryLimit.use(array.length);
3317
+ const clone = [...array];
2760
3318
  clone.unshift(arg);
2761
3319
  return clone;
2762
3320
  }
@@ -2766,7 +3324,9 @@ function pop(v) {
2766
3324
  return clone;
2767
3325
  }
2768
3326
  function shift(v) {
2769
- const clone = [...toArray(v)];
3327
+ const array = toArray(v);
3328
+ this.context.memoryLimit.use(array.length);
3329
+ const clone = [...array];
2770
3330
  clone.shift();
2771
3331
  return clone;
2772
3332
  }
@@ -2777,26 +3337,33 @@ function slice(v, begin, length = 1) {
2777
3337
  if (!isArray(v))
2778
3338
  v = stringify(v);
2779
3339
  begin = begin < 0 ? v.length + begin : begin;
3340
+ this.context.memoryLimit.use(length);
2780
3341
  return v.slice(begin, begin + length);
2781
3342
  }
2782
3343
  function* where(arr, property, expected) {
2783
3344
  const values = [];
2784
3345
  arr = toArray(arr);
3346
+ this.context.memoryLimit.use(arr.length);
2785
3347
  const token = new Tokenizer(stringify(property)).readScopeValue();
2786
3348
  for (const item of arr) {
2787
- values.push(yield evalToken(token, new Context(item)));
3349
+ values.push(yield evalToken(token, this.context.spawn(item)));
2788
3350
  }
3351
+ const matcher = this.context.opts.jekyllWhere
3352
+ ? (v) => EmptyDrop.is(expected) ? equals(v, expected) : (isArray(v) ? arrayIncludes(v, expected) : equals(v, expected))
3353
+ : (v) => equals(v, expected);
2789
3354
  return arr.filter((_, i) => {
2790
3355
  if (expected === undefined)
2791
3356
  return isTruthy(values[i], this.context);
2792
- return equals(values[i], expected);
3357
+ return matcher(values[i]);
2793
3358
  });
2794
3359
  }
2795
3360
  function* where_exp(arr, itemName, exp) {
2796
3361
  const filtered = [];
2797
3362
  const keyTemplate = new Value(stringify(exp), this.liquid);
2798
- for (const item of toArray(arr)) {
2799
- const value = yield keyTemplate.value(new Context({ [itemName]: item }));
3363
+ const array = toArray(arr);
3364
+ this.context.memoryLimit.use(array.length);
3365
+ for (const item of array) {
3366
+ const value = yield keyTemplate.value(this.context.spawn({ [itemName]: item }));
2800
3367
  if (value)
2801
3368
  filtered.push(item);
2802
3369
  }
@@ -2804,10 +3371,11 @@ function* where_exp(arr, itemName, exp) {
2804
3371
  }
2805
3372
  function* group_by(arr, property) {
2806
3373
  const map = new Map();
2807
- arr = toArray(arr);
3374
+ arr = toEnumerable(arr);
2808
3375
  const token = new Tokenizer(stringify(property)).readScopeValue();
3376
+ this.context.memoryLimit.use(arr.length);
2809
3377
  for (const item of arr) {
2810
- const key = yield evalToken(token, new Context(item));
3378
+ const key = yield evalToken(token, this.context.spawn(item));
2811
3379
  if (!map.has(key))
2812
3380
  map.set(key, []);
2813
3381
  map.get(key).push(item);
@@ -2817,8 +3385,10 @@ function* group_by(arr, property) {
2817
3385
  function* group_by_exp(arr, itemName, exp) {
2818
3386
  const map = new Map();
2819
3387
  const keyTemplate = new Value(stringify(exp), this.liquid);
2820
- for (const item of toArray(arr)) {
2821
- const key = yield keyTemplate.value(new Context({ [itemName]: item }));
3388
+ arr = toEnumerable(arr);
3389
+ this.context.memoryLimit.use(arr.length);
3390
+ for (const item of arr) {
3391
+ const key = yield keyTemplate.value(this.context.spawn({ [itemName]: item }));
2822
3392
  if (!map.has(key))
2823
3393
  map.set(key, []);
2824
3394
  map.get(key).push(item);
@@ -2827,31 +3397,26 @@ function* group_by_exp(arr, itemName, exp) {
2827
3397
  }
2828
3398
  function* find(arr, property, expected) {
2829
3399
  const token = new Tokenizer(stringify(property)).readScopeValue();
2830
- for (const item of toArray(arr)) {
2831
- const value = yield evalToken(token, new Context(item));
3400
+ const array = toArray(arr);
3401
+ for (const item of array) {
3402
+ const value = yield evalToken(token, this.context.spawn(item));
2832
3403
  if (equals(value, expected))
2833
3404
  return item;
2834
3405
  }
2835
- return null;
2836
3406
  }
2837
3407
  function* find_exp(arr, itemName, exp) {
2838
3408
  const predicate = new Value(stringify(exp), this.liquid);
2839
- for (const item of toArray(arr)) {
2840
- const value = yield predicate.value(new Context({ [itemName]: item }));
3409
+ const array = toArray(arr);
3410
+ for (const item of array) {
3411
+ const value = yield predicate.value(this.context.spawn({ [itemName]: item }));
2841
3412
  if (value)
2842
3413
  return item;
2843
3414
  }
2844
- return null;
2845
3415
  }
2846
3416
  function uniq(arr) {
2847
- arr = toValue(arr);
2848
- const u = {};
2849
- return (arr || []).filter(val => {
2850
- if (hasOwnProperty.call(u, String(val)))
2851
- return false;
2852
- u[String(val)] = true;
2853
- return true;
2854
- });
3417
+ arr = toArray(arr);
3418
+ this.context.memoryLimit.use(arr.length);
3419
+ return [...new Set(arr)];
2855
3420
  }
2856
3421
  function sample(v, count = 1) {
2857
3422
  v = toValue(v);
@@ -2859,6 +3424,7 @@ function sample(v, count = 1) {
2859
3424
  return [];
2860
3425
  if (!isArray(v))
2861
3426
  v = stringify(v);
3427
+ this.context.memoryLimit.use(count);
2862
3428
  const shuffled = [...v].sort(() => Math.random() - 0.5);
2863
3429
  if (count === 1)
2864
3430
  return shuffled[0];
@@ -2894,6 +3460,9 @@ var arrayFilters = /*#__PURE__*/Object.freeze({
2894
3460
  });
2895
3461
 
2896
3462
  function date(v, format, timezoneOffset) {
3463
+ var _a, _b, _c;
3464
+ 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);
3465
+ this.context.memoryLimit.use(size);
2897
3466
  const date = parseDate(v, this.context.opts, timezoneOffset);
2898
3467
  if (!date)
2899
3468
  return v;
@@ -2927,39 +3496,30 @@ function stringify_date(v, month_type, type, style) {
2927
3496
  }
2928
3497
  function parseDate(v, opts, timezoneOffset) {
2929
3498
  let date;
3499
+ const defaultTimezoneOffset = timezoneOffset !== null && timezoneOffset !== void 0 ? timezoneOffset : opts.timezoneOffset;
3500
+ const locale = opts.locale;
2930
3501
  v = toValue(v);
2931
3502
  if (v === 'now' || v === 'today') {
2932
- date = new Date();
3503
+ date = new LiquidDate(Date.now(), locale, defaultTimezoneOffset);
2933
3504
  }
2934
3505
  else if (isNumber(v)) {
2935
- date = new Date(v * 1000);
3506
+ date = new LiquidDate(v * 1000, locale, defaultTimezoneOffset);
2936
3507
  }
2937
3508
  else if (isString(v)) {
2938
3509
  if (/^\d+$/.test(v)) {
2939
- date = new Date(+v * 1000);
3510
+ date = new LiquidDate(+v * 1000, locale, defaultTimezoneOffset);
2940
3511
  }
2941
- else if (opts.preserveTimezones) {
2942
- date = TimezoneDate.createDateFixedToTimezone(v);
3512
+ else if (opts.preserveTimezones && timezoneOffset === undefined) {
3513
+ date = LiquidDate.createDateFixedToTimezone(v, locale);
2943
3514
  }
2944
3515
  else {
2945
- date = new Date(v);
3516
+ date = new LiquidDate(v, locale, defaultTimezoneOffset);
2946
3517
  }
2947
3518
  }
2948
3519
  else {
2949
- date = v;
2950
- }
2951
- if (!isValidDate(date))
2952
- return;
2953
- if (timezoneOffset !== undefined) {
2954
- date = new TimezoneDate(date, timezoneOffset);
2955
- }
2956
- else if (!(date instanceof TimezoneDate) && opts.timezoneOffset !== undefined) {
2957
- date = new TimezoneDate(date, opts.timezoneOffset);
3520
+ date = new LiquidDate(v, locale, defaultTimezoneOffset);
2958
3521
  }
2959
- return date;
2960
- }
2961
- function isValidDate(date) {
2962
- return (date instanceof Date || date instanceof TimezoneDate) && !isNaN(date.getTime());
3522
+ return date.valid() ? date : undefined;
2963
3523
  }
2964
3524
 
2965
3525
  var dateFilters = /*#__PURE__*/Object.freeze({
@@ -2981,33 +3541,50 @@ const rCJKWord = /[\u4E00-\u9FFF\uF900-\uFAFF\u3400-\u4DBF\u3040-\u309F\u30A0-\u
2981
3541
  const rNonCJKWord = /[^\u4E00-\u9FFF\uF900-\uFAFF\u3400-\u4DBF\u3040-\u309F\u30A0-\u30FF\uAC00-\uD7AF\s]+/gu;
2982
3542
  function append(v, arg) {
2983
3543
  assert(arguments.length === 2, 'append expect 2 arguments');
2984
- return stringify(v) + stringify(arg);
3544
+ const lhs = stringify(v);
3545
+ const rhs = stringify(arg);
3546
+ this.context.memoryLimit.use(lhs.length + rhs.length);
3547
+ return lhs + rhs;
2985
3548
  }
2986
3549
  function prepend(v, arg) {
2987
3550
  assert(arguments.length === 2, 'prepend expect 2 arguments');
2988
- return stringify(arg) + stringify(v);
3551
+ const lhs = stringify(v);
3552
+ const rhs = stringify(arg);
3553
+ this.context.memoryLimit.use(lhs.length + rhs.length);
3554
+ return rhs + lhs;
2989
3555
  }
2990
3556
  function lstrip(v, chars) {
3557
+ const str = stringify(v);
3558
+ this.context.memoryLimit.use(str.length);
2991
3559
  if (chars) {
2992
3560
  chars = escapeRegExp(stringify(chars));
2993
- return stringify(v).replace(new RegExp(`^[${chars}]+`, 'g'), '');
3561
+ return str.replace(new RegExp(`^[${chars}]+`, 'g'), '');
2994
3562
  }
2995
- return stringify(v).replace(/^\s+/, '');
3563
+ return str.replace(/^\s+/, '');
2996
3564
  }
2997
3565
  function downcase(v) {
2998
- return stringify(v).toLowerCase();
3566
+ const str = stringify(v);
3567
+ this.context.memoryLimit.use(str.length);
3568
+ return str.toLowerCase();
2999
3569
  }
3000
- function upcase(str) {
3570
+ function upcase(v) {
3571
+ const str = stringify(v);
3572
+ this.context.memoryLimit.use(str.length);
3001
3573
  return stringify(str).toUpperCase();
3002
3574
  }
3003
3575
  function remove(v, arg) {
3004
- return stringify(v).split(stringify(arg)).join('');
3576
+ const str = stringify(v);
3577
+ this.context.memoryLimit.use(str.length);
3578
+ return str.split(stringify(arg)).join('');
3005
3579
  }
3006
3580
  function remove_first(v, l) {
3007
- return stringify(v).replace(stringify(l), '');
3581
+ const str = stringify(v);
3582
+ this.context.memoryLimit.use(str.length);
3583
+ return str.replace(stringify(l), '');
3008
3584
  }
3009
3585
  function remove_last(v, l) {
3010
3586
  const str = stringify(v);
3587
+ this.context.memoryLimit.use(str.length);
3011
3588
  const pattern = stringify(l);
3012
3589
  const index = str.lastIndexOf(pattern);
3013
3590
  if (index === -1)
@@ -3015,14 +3592,18 @@ function remove_last(v, l) {
3015
3592
  return str.substring(0, index) + str.substring(index + pattern.length);
3016
3593
  }
3017
3594
  function rstrip(str, chars) {
3595
+ str = stringify(str);
3596
+ this.context.memoryLimit.use(str.length);
3018
3597
  if (chars) {
3019
3598
  chars = escapeRegExp(stringify(chars));
3020
- return stringify(str).replace(new RegExp(`[${chars}]+$`, 'g'), '');
3599
+ return str.replace(new RegExp(`[${chars}]+$`, 'g'), '');
3021
3600
  }
3022
- return stringify(str).replace(/\s+$/, '');
3601
+ return str.replace(/\s+$/, '');
3023
3602
  }
3024
3603
  function split(v, arg) {
3025
- const arr = stringify(v).split(stringify(arg));
3604
+ const str = stringify(v);
3605
+ this.context.memoryLimit.use(str.length);
3606
+ const arr = str.split(stringify(arg));
3026
3607
  // align to ruby split, which is the behavior of shopify/liquid
3027
3608
  // see: https://ruby-doc.org/core-2.4.0/String.html#method-i-split
3028
3609
  while (arr.length && arr[arr.length - 1] === '')
@@ -3030,29 +3611,39 @@ function split(v, arg) {
3030
3611
  return arr;
3031
3612
  }
3032
3613
  function strip(v, chars) {
3614
+ const str = stringify(v);
3615
+ this.context.memoryLimit.use(str.length);
3033
3616
  if (chars) {
3034
3617
  chars = escapeRegExp(stringify(chars));
3035
- return stringify(v)
3618
+ return str
3036
3619
  .replace(new RegExp(`^[${chars}]+`, 'g'), '')
3037
3620
  .replace(new RegExp(`[${chars}]+$`, 'g'), '');
3038
3621
  }
3039
- return stringify(v).trim();
3622
+ return str.trim();
3040
3623
  }
3041
3624
  function strip_newlines(v) {
3042
- return stringify(v).replace(/\r?\n/gm, '');
3625
+ const str = stringify(v);
3626
+ this.context.memoryLimit.use(str.length);
3627
+ return str.replace(/\r?\n/gm, '');
3043
3628
  }
3044
3629
  function capitalize(str) {
3045
3630
  str = stringify(str);
3631
+ this.context.memoryLimit.use(str.length);
3046
3632
  return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
3047
3633
  }
3048
3634
  function replace(v, pattern, replacement) {
3049
- return stringify(v).split(stringify(pattern)).join(replacement);
3635
+ const str = stringify(v);
3636
+ this.context.memoryLimit.use(str.length);
3637
+ return str.split(stringify(pattern)).join(replacement);
3050
3638
  }
3051
3639
  function replace_first(v, arg1, arg2) {
3052
- return stringify(v).replace(stringify(arg1), arg2);
3640
+ const str = stringify(v);
3641
+ this.context.memoryLimit.use(str.length);
3642
+ return str.replace(stringify(arg1), arg2);
3053
3643
  }
3054
3644
  function replace_last(v, arg1, arg2) {
3055
3645
  const str = stringify(v);
3646
+ this.context.memoryLimit.use(str.length);
3056
3647
  const pattern = stringify(arg1);
3057
3648
  const index = str.lastIndexOf(pattern);
3058
3649
  if (index === -1)
@@ -3061,13 +3652,16 @@ function replace_last(v, arg1, arg2) {
3061
3652
  return str.substring(0, index) + replacement + str.substring(index + pattern.length);
3062
3653
  }
3063
3654
  function truncate(v, l = 50, o = '...') {
3064
- v = stringify(v);
3065
- if (v.length <= l)
3655
+ const str = stringify(v);
3656
+ this.context.memoryLimit.use(str.length);
3657
+ if (str.length <= l)
3066
3658
  return v;
3067
- return v.substring(0, l - o.length) + o;
3659
+ return str.substring(0, l - o.length) + o;
3068
3660
  }
3069
3661
  function truncatewords(v, words = 15, o = '...') {
3070
- const arr = stringify(v).split(/\s+/);
3662
+ const str = stringify(v);
3663
+ this.context.memoryLimit.use(str.length);
3664
+ const arr = str.split(/\s+/);
3071
3665
  if (words <= 0)
3072
3666
  words = 1;
3073
3667
  let ret = arr.slice(0, words).join(' ');
@@ -3076,11 +3670,14 @@ function truncatewords(v, words = 15, o = '...') {
3076
3670
  return ret;
3077
3671
  }
3078
3672
  function normalize_whitespace(v) {
3079
- v = stringify(v);
3080
- return v.replace(/\s+/g, ' ');
3673
+ const str = stringify(v);
3674
+ this.context.memoryLimit.use(str.length);
3675
+ return str.replace(/\s+/g, ' ');
3081
3676
  }
3082
3677
  function number_of_words(input, mode) {
3083
- input = stringify(input).trim();
3678
+ const str = stringify(input);
3679
+ this.context.memoryLimit.use(str.length);
3680
+ input = str.trim();
3084
3681
  if (!input)
3085
3682
  return 0;
3086
3683
  switch (mode) {
@@ -3098,6 +3695,7 @@ function number_of_words(input, mode) {
3098
3695
  }
3099
3696
  }
3100
3697
  function array_to_sentence_string(array, connector = 'and') {
3698
+ this.context.memoryLimit.use(array.length);
3101
3699
  switch (array.length) {
3102
3700
  case 0:
3103
3701
  return '';
@@ -3140,7 +3738,8 @@ const filters = Object.assign(Object.assign(Object.assign(Object.assign(Object.a
3140
3738
  class AssignTag extends Tag {
3141
3739
  constructor(token, remainTokens, liquid) {
3142
3740
  super(token, remainTokens, liquid);
3143
- this.key = this.tokenizer.readIdentifier().content;
3741
+ this.identifier = this.tokenizer.readIdentifier();
3742
+ this.key = this.identifier.content;
3144
3743
  this.tokenizer.assert(this.key, 'expected variable name');
3145
3744
  this.tokenizer.skipBlank();
3146
3745
  this.tokenizer.assert(this.tokenizer.peek() === '=', 'expected "="');
@@ -3150,11 +3749,17 @@ class AssignTag extends Tag {
3150
3749
  *render(ctx) {
3151
3750
  ctx.bottom()[this.key] = yield this.value.value(ctx, this.liquid.options.lenientIf);
3152
3751
  }
3752
+ *arguments() {
3753
+ yield this.value;
3754
+ }
3755
+ *localScope() {
3756
+ yield this.identifier;
3757
+ }
3153
3758
  }
3154
3759
 
3155
3760
  const MODIFIERS = ['offset', 'limit', 'reversed'];
3156
3761
  class ForTag extends Tag {
3157
- constructor(token, remainTokens, liquid) {
3762
+ constructor(token, remainTokens, liquid, parser) {
3158
3763
  super(token, remainTokens, liquid);
3159
3764
  const variable = this.tokenizer.readIdentifier();
3160
3765
  const inStr = this.tokenizer.readIdentifier();
@@ -3164,18 +3769,16 @@ class ForTag extends Tag {
3164
3769
  }
3165
3770
  this.variable = variable.content;
3166
3771
  this.collection = collection;
3167
- this.hash = new Hash(this.tokenizer.remaining());
3772
+ this.hash = new Hash(this.tokenizer, liquid.options.keyValueSeparator);
3168
3773
  this.templates = [];
3169
3774
  this.elseTemplates = [];
3170
3775
  let p;
3171
- const stream = this.liquid.parser.parseStream(remainTokens)
3776
+ const stream = parser.parseStream(remainTokens)
3172
3777
  .on('start', () => (p = this.templates))
3173
- .on('tag:else', () => (p = this.elseTemplates))
3174
- .on('tag:endfor', () => stream.stop())
3778
+ .on('tag:else', tag => { assertEmpty(tag.args); p = this.elseTemplates; })
3779
+ .on('tag:endfor', tag => { assertEmpty(tag.args); stream.stop(); })
3175
3780
  .on('template', (tpl) => p.push(tpl))
3176
- .on('end', () => {
3177
- throw new Error(`tag ${token.getText()} not closed`);
3178
- });
3781
+ .on('end', () => { throw new Error(`tag ${token.getText()} not closed`); });
3179
3782
  stream.start();
3180
3783
  }
3181
3784
  *render(ctx, emitter) {
@@ -3204,16 +3807,33 @@ class ForTag extends Tag {
3204
3807
  ctx.push(scope);
3205
3808
  for (const item of collection) {
3206
3809
  scope[this.variable] = item;
3810
+ ctx.continueCalled = ctx.breakCalled = false;
3207
3811
  yield r.renderTemplates(this.templates, ctx, emitter);
3208
- if (emitter['break']) {
3209
- emitter['break'] = false;
3812
+ if (ctx.breakCalled)
3210
3813
  break;
3211
- }
3212
- emitter['continue'] = false;
3213
3814
  scope.forloop.next();
3214
3815
  }
3816
+ ctx.continueCalled = ctx.breakCalled = false;
3215
3817
  ctx.pop();
3216
3818
  }
3819
+ *children() {
3820
+ const templates = this.templates.slice();
3821
+ if (this.elseTemplates) {
3822
+ templates.push(...this.elseTemplates);
3823
+ }
3824
+ return templates;
3825
+ }
3826
+ *arguments() {
3827
+ yield this.collection;
3828
+ for (const v of Object.values(this.hash.hash)) {
3829
+ if (isValueToken(v)) {
3830
+ yield v;
3831
+ }
3832
+ }
3833
+ }
3834
+ blockScope() {
3835
+ return [this.variable, 'forloop'];
3836
+ }
3217
3837
  }
3218
3838
  function reversed(arr) {
3219
3839
  return [...arr].reverse();
@@ -3226,36 +3846,43 @@ function limit(arr, count) {
3226
3846
  }
3227
3847
 
3228
3848
  class CaptureTag extends Tag {
3229
- constructor(tagToken, remainTokens, liquid) {
3849
+ constructor(tagToken, remainTokens, liquid, parser) {
3230
3850
  super(tagToken, remainTokens, liquid);
3231
3851
  this.templates = [];
3232
- this.variable = this.readVariableName();
3852
+ this.identifier = this.readVariable();
3853
+ this.variable = this.identifier.content;
3233
3854
  while (remainTokens.length) {
3234
3855
  const token = remainTokens.shift();
3235
3856
  if (isTagToken(token) && token.name === 'endcapture')
3236
3857
  return;
3237
- this.templates.push(liquid.parser.parseToken(token, remainTokens));
3858
+ this.templates.push(parser.parseToken(token, remainTokens));
3238
3859
  }
3239
3860
  throw new Error(`tag ${tagToken.getText()} not closed`);
3240
3861
  }
3862
+ readVariable() {
3863
+ let ident = this.tokenizer.readIdentifier();
3864
+ if (ident.content)
3865
+ return ident;
3866
+ ident = this.tokenizer.readQuoted();
3867
+ if (ident)
3868
+ return ident;
3869
+ throw this.tokenizer.error('invalid capture name');
3870
+ }
3241
3871
  *render(ctx) {
3242
3872
  const r = this.liquid.renderer;
3243
3873
  const html = yield r.renderTemplates(this.templates, ctx);
3244
3874
  ctx.bottom()[this.variable] = html;
3245
3875
  }
3246
- readVariableName() {
3247
- const word = this.tokenizer.readIdentifier().content;
3248
- if (word)
3249
- return word;
3250
- const quoted = this.tokenizer.readQuoted();
3251
- if (quoted)
3252
- return evalQuotedToken(quoted);
3253
- throw this.tokenizer.error('invalid capture name');
3876
+ *children() {
3877
+ return this.templates;
3878
+ }
3879
+ *localScope() {
3880
+ yield this.identifier;
3254
3881
  }
3255
3882
  }
3256
3883
 
3257
3884
  class CaseTag extends Tag {
3258
- constructor(tagToken, remainTokens, liquid) {
3885
+ constructor(tagToken, remainTokens, liquid, parser) {
3259
3886
  super(tagToken, remainTokens, liquid);
3260
3887
  this.branches = [];
3261
3888
  this.elseTemplates = [];
@@ -3263,7 +3890,7 @@ class CaseTag extends Tag {
3263
3890
  this.elseTemplates = [];
3264
3891
  let p = [];
3265
3892
  let elseCount = 0;
3266
- const stream = this.liquid.parser.parseStream(remainTokens)
3893
+ const stream = parser.parseStream(remainTokens)
3267
3894
  .on('tag:when', (token) => {
3268
3895
  if (elseCount > 0) {
3269
3896
  return;
@@ -3318,6 +3945,17 @@ class CaseTag extends Tag {
3318
3945
  yield r.renderTemplates(this.elseTemplates, ctx, emitter);
3319
3946
  }
3320
3947
  }
3948
+ *arguments() {
3949
+ yield this.value;
3950
+ yield* this.branches.flatMap(b => b.values);
3951
+ }
3952
+ *children() {
3953
+ const templates = this.branches.flatMap(b => b.templates);
3954
+ if (this.elseTemplates) {
3955
+ templates.push(...this.elseTemplates);
3956
+ }
3957
+ return templates;
3958
+ }
3321
3959
  }
3322
3960
 
3323
3961
  class CommentTag extends Tag {
@@ -3334,10 +3972,10 @@ class CommentTag extends Tag {
3334
3972
  }
3335
3973
 
3336
3974
  class RenderTag extends Tag {
3337
- constructor(token, remainTokens, liquid) {
3975
+ constructor(token, remainTokens, liquid, parser) {
3338
3976
  super(token, remainTokens, liquid);
3339
3977
  const tokenizer = this.tokenizer;
3340
- this.file = parseFilePath(tokenizer, this.liquid);
3978
+ this.file = parseFilePath(tokenizer, this.liquid, parser);
3341
3979
  this.currentFile = token.file;
3342
3980
  while (!tokenizer.end()) {
3343
3981
  tokenizer.skipBlank();
@@ -3371,13 +4009,13 @@ class RenderTag extends Tag {
3371
4009
  tokenizer.p = begin;
3372
4010
  break;
3373
4011
  }
3374
- this.hash = new Hash(tokenizer.remaining());
4012
+ this.hash = new Hash(tokenizer, liquid.options.keyValueSeparator);
3375
4013
  }
3376
4014
  *render(ctx, emitter) {
3377
4015
  const { liquid, hash } = this;
3378
4016
  const filepath = (yield renderFilePath(this['file'], ctx, liquid));
3379
4017
  assert(filepath, () => `illegal file path "${filepath}"`);
3380
- const childCtx = new Context({}, ctx.opts, { sync: ctx.sync, globals: ctx.globals, strictVariables: ctx.strictVariables });
4018
+ const childCtx = ctx.spawn();
3381
4019
  const scope = childCtx.bottom();
3382
4020
  __assign(scope, yield hash.render(ctx));
3383
4021
  if (this['with']) {
@@ -3400,6 +4038,55 @@ class RenderTag extends Tag {
3400
4038
  yield liquid.renderer.renderTemplates(templates, childCtx, emitter);
3401
4039
  }
3402
4040
  }
4041
+ *children(partials, sync) {
4042
+ if (partials && isString(this['file'])) {
4043
+ return (yield this.liquid._parsePartialFile(this['file'], sync, this['currentFile']));
4044
+ }
4045
+ return [];
4046
+ }
4047
+ partialScope() {
4048
+ if (isString(this['file'])) {
4049
+ const names = Object.keys(this.hash.hash);
4050
+ if (this['with']) {
4051
+ const { value, alias } = this['with'];
4052
+ if (isString(alias)) {
4053
+ names.push([alias, value]);
4054
+ }
4055
+ else if (isString(this.file)) {
4056
+ names.push([this.file, value]);
4057
+ }
4058
+ }
4059
+ if (this['for']) {
4060
+ const { value, alias } = this['for'];
4061
+ if (isString(alias)) {
4062
+ names.push([alias, value]);
4063
+ }
4064
+ else if (isString(this.file)) {
4065
+ names.push([this.file, value]);
4066
+ }
4067
+ }
4068
+ return { name: this['file'], isolated: true, scope: names };
4069
+ }
4070
+ }
4071
+ *arguments() {
4072
+ for (const v of Object.values(this.hash.hash)) {
4073
+ if (isValueToken(v)) {
4074
+ yield v;
4075
+ }
4076
+ }
4077
+ if (this['with']) {
4078
+ const { value } = this['with'];
4079
+ if (isValueToken(value)) {
4080
+ yield value;
4081
+ }
4082
+ }
4083
+ if (this['for']) {
4084
+ const { value } = this['for'];
4085
+ if (isValueToken(value)) {
4086
+ yield value;
4087
+ }
4088
+ }
4089
+ }
3403
4090
  }
3404
4091
  /**
3405
4092
  * @return null for "none",
@@ -3407,7 +4094,7 @@ class RenderTag extends Tag {
3407
4094
  * @return Token for expression (not quoted)
3408
4095
  * @throws TypeError if cannot read next token
3409
4096
  */
3410
- function parseFilePath(tokenizer, liquid) {
4097
+ function parseFilePath(tokenizer, liquid, parser) {
3411
4098
  if (liquid.options.dynamicPartials) {
3412
4099
  const file = tokenizer.readValue();
3413
4100
  tokenizer.assert(file, 'illegal file path');
@@ -3415,13 +4102,13 @@ function parseFilePath(tokenizer, liquid) {
3415
4102
  return;
3416
4103
  if (isQuotedToken(file)) {
3417
4104
  // for filenames like "files/{{file}}", eval as liquid template
3418
- const templates = liquid.parse(evalQuotedToken(file));
4105
+ const templates = parser.parse(evalQuotedToken(file));
3419
4106
  return optimize(templates);
3420
4107
  }
3421
4108
  return file;
3422
4109
  }
3423
4110
  const tokens = [...tokenizer.readFileNameTemplate(liquid.options)];
3424
- const templates = optimize(liquid.parser.parseTokens(tokens));
4111
+ const templates = optimize(parser.parseTokens(tokens));
3425
4112
  return templates === 'none' ? undefined : templates;
3426
4113
  }
3427
4114
  function optimize(templates) {
@@ -3439,10 +4126,10 @@ function* renderFilePath(file, ctx, liquid) {
3439
4126
  }
3440
4127
 
3441
4128
  class IncludeTag extends Tag {
3442
- constructor(token, remainTokens, liquid) {
4129
+ constructor(token, remainTokens, liquid, parser) {
3443
4130
  super(token, remainTokens, liquid);
3444
4131
  const { tokenizer } = token;
3445
- this['file'] = parseFilePath(tokenizer, this.liquid);
4132
+ this['file'] = parseFilePath(tokenizer, this.liquid, parser);
3446
4133
  this['currentFile'] = token.file;
3447
4134
  const begin = tokenizer.p;
3448
4135
  const withStr = tokenizer.readIdentifier();
@@ -3456,7 +4143,7 @@ class IncludeTag extends Tag {
3456
4143
  }
3457
4144
  else
3458
4145
  tokenizer.p = begin;
3459
- this.hash = new Hash(tokenizer.remaining(), this.liquid.options.jekyllInclude);
4146
+ this.hash = new Hash(tokenizer, liquid.options.jekyllInclude || liquid.options.keyValueSeparator);
3460
4147
  }
3461
4148
  *render(ctx, emitter) {
3462
4149
  const { liquid, hash, withVar } = this;
@@ -3475,12 +4162,43 @@ class IncludeTag extends Tag {
3475
4162
  ctx.pop();
3476
4163
  ctx.restoreRegister(saved);
3477
4164
  }
4165
+ *children(partials, sync) {
4166
+ if (partials && isString(this['file'])) {
4167
+ return (yield this.liquid._parsePartialFile(this['file'], sync, this['currentFile']));
4168
+ }
4169
+ return [];
4170
+ }
4171
+ partialScope() {
4172
+ if (isString(this['file'])) {
4173
+ let names;
4174
+ if (this.liquid.options.jekyllInclude) {
4175
+ names = ['include'];
4176
+ }
4177
+ else {
4178
+ names = Object.keys(this.hash.hash);
4179
+ if (this.withVar) {
4180
+ names.push([this['file'], this.withVar]);
4181
+ }
4182
+ }
4183
+ return { name: this['file'], isolated: false, scope: names };
4184
+ }
4185
+ }
4186
+ *arguments() {
4187
+ yield* Object.values(this.hash.hash).filter(isValueToken);
4188
+ if (isValueToken(this['file'])) {
4189
+ yield this['file'];
4190
+ }
4191
+ if (isValueToken(this.withVar)) {
4192
+ yield this.withVar;
4193
+ }
4194
+ }
3478
4195
  }
3479
4196
 
3480
4197
  class DecrementTag extends Tag {
3481
4198
  constructor(token, remainTokens, liquid) {
3482
4199
  super(token, remainTokens, liquid);
3483
- this.variable = this.tokenizer.readIdentifier().content;
4200
+ this.identifier = this.tokenizer.readIdentifier();
4201
+ this.variable = this.identifier.content;
3484
4202
  }
3485
4203
  render(context, emitter) {
3486
4204
  const scope = context.environments;
@@ -3489,6 +4207,9 @@ class DecrementTag extends Tag {
3489
4207
  }
3490
4208
  emitter.write(stringify(--scope[this.variable]));
3491
4209
  }
4210
+ *localScope() {
4211
+ yield this.identifier;
4212
+ }
3492
4213
  }
3493
4214
 
3494
4215
  class CycleTag extends Tag {
@@ -3526,40 +4247,38 @@ class CycleTag extends Tag {
3526
4247
  groups[fingerprint] = idx;
3527
4248
  return yield evalToken(candidate, ctx);
3528
4249
  }
4250
+ *arguments() {
4251
+ yield* this.candidates;
4252
+ if (this.group) {
4253
+ yield this.group;
4254
+ }
4255
+ }
3529
4256
  }
3530
4257
 
3531
4258
  class IfTag extends Tag {
3532
- constructor(tagToken, remainTokens, liquid) {
4259
+ constructor(tagToken, remainTokens, liquid, parser) {
3533
4260
  super(tagToken, remainTokens, liquid);
3534
4261
  this.branches = [];
3535
- this.elseTemplates = [];
3536
4262
  let p = [];
3537
- let elseCount = 0;
3538
- liquid.parser.parseStream(remainTokens)
4263
+ parser.parseStream(remainTokens)
3539
4264
  .on('start', () => this.branches.push({
3540
- value: new Value(tagToken.args, this.liquid),
4265
+ value: new Value(tagToken.tokenizer.readFilteredValue(), this.liquid),
3541
4266
  templates: (p = [])
3542
4267
  }))
3543
4268
  .on('tag:elsif', (token) => {
3544
- if (elseCount > 0) {
3545
- p = [];
3546
- return;
3547
- }
4269
+ assert(!this.elseTemplates, 'unexpected elsif after else');
3548
4270
  this.branches.push({
3549
- value: new Value(token.args, this.liquid),
4271
+ value: new Value(token.tokenizer.readFilteredValue(), this.liquid),
3550
4272
  templates: (p = [])
3551
4273
  });
3552
4274
  })
3553
- .on('tag:else', () => {
3554
- elseCount++;
3555
- p = this.elseTemplates;
3556
- })
3557
- .on('tag:endif', function () { this.stop(); })
3558
- .on('template', (tpl) => {
3559
- if (p !== this.elseTemplates || elseCount === 1) {
3560
- p.push(tpl);
3561
- }
4275
+ .on('tag:else', tag => {
4276
+ assertEmpty(tag.args);
4277
+ assert(!this.elseTemplates, 'duplicated else');
4278
+ p = this.elseTemplates = [];
3562
4279
  })
4280
+ .on('tag:endif', function (tag) { assertEmpty(tag.args); this.stop(); })
4281
+ .on('template', (tpl) => p.push(tpl))
3563
4282
  .on('end', () => { throw new Error(`tag ${tagToken.getText()} not closed`); })
3564
4283
  .start();
3565
4284
  }
@@ -3572,14 +4291,25 @@ class IfTag extends Tag {
3572
4291
  return;
3573
4292
  }
3574
4293
  }
3575
- yield r.renderTemplates(this.elseTemplates, ctx, emitter);
4294
+ yield r.renderTemplates(this.elseTemplates || [], ctx, emitter);
4295
+ }
4296
+ *children() {
4297
+ const templates = this.branches.flatMap(b => b.templates);
4298
+ if (this.elseTemplates) {
4299
+ templates.push(...this.elseTemplates);
4300
+ }
4301
+ return templates;
4302
+ }
4303
+ arguments() {
4304
+ return this.branches.map(b => b.value);
3576
4305
  }
3577
4306
  }
3578
4307
 
3579
4308
  class IncrementTag extends Tag {
3580
4309
  constructor(token, remainTokens, liquid) {
3581
4310
  super(token, remainTokens, liquid);
3582
- this.variable = this.tokenizer.readIdentifier().content;
4311
+ this.identifier = this.tokenizer.readIdentifier();
4312
+ this.variable = this.identifier.content;
3583
4313
  }
3584
4314
  render(context, emitter) {
3585
4315
  const scope = context.environments;
@@ -3590,15 +4320,18 @@ class IncrementTag extends Tag {
3590
4320
  scope[this.variable]++;
3591
4321
  emitter.write(stringify(val));
3592
4322
  }
4323
+ *localScope() {
4324
+ yield this.identifier;
4325
+ }
3593
4326
  }
3594
4327
 
3595
4328
  class LayoutTag extends Tag {
3596
- constructor(token, remainTokens, liquid) {
4329
+ constructor(token, remainTokens, liquid, parser) {
3597
4330
  super(token, remainTokens, liquid);
3598
- this.file = parseFilePath(this.tokenizer, this.liquid);
4331
+ this.file = parseFilePath(this.tokenizer, this.liquid, parser);
3599
4332
  this['currentFile'] = token.file;
3600
- this.args = new Hash(this.tokenizer.remaining());
3601
- this.templates = this.liquid.parser.parseTokens(remainTokens);
4333
+ this.args = new Hash(this.tokenizer, liquid.options.keyValueSeparator);
4334
+ this.templates = parser.parseTokens(remainTokens);
3602
4335
  }
3603
4336
  *render(ctx, emitter) {
3604
4337
  const { liquid, args, file } = this;
@@ -3624,10 +4357,32 @@ class LayoutTag extends Tag {
3624
4357
  yield renderer.renderTemplates(templates, ctx, emitter);
3625
4358
  ctx.pop();
3626
4359
  }
4360
+ *children(partials) {
4361
+ const templates = this.templates.slice();
4362
+ if (partials && isString(this.file)) {
4363
+ templates.push(...(yield this.liquid._parsePartialFile(this.file, true, this['currentFile'])));
4364
+ }
4365
+ return templates;
4366
+ }
4367
+ *arguments() {
4368
+ for (const v of Object.values(this.args.hash)) {
4369
+ if (isValueToken(v)) {
4370
+ yield v;
4371
+ }
4372
+ }
4373
+ if (isValueToken(this.file)) {
4374
+ yield this.file;
4375
+ }
4376
+ }
4377
+ partialScope() {
4378
+ if (isString(this.file)) {
4379
+ return { name: this.file, isolated: false, scope: Object.keys(this.args.hash) };
4380
+ }
4381
+ }
3627
4382
  }
3628
4383
 
3629
4384
  class BlockTag extends Tag {
3630
- constructor(token, remainTokens, liquid) {
4385
+ constructor(token, remainTokens, liquid, parser) {
3631
4386
  super(token, remainTokens, liquid);
3632
4387
  this.templates = [];
3633
4388
  const match = /\w+/.exec(token.args);
@@ -3636,7 +4391,7 @@ class BlockTag extends Tag {
3636
4391
  const token = remainTokens.shift();
3637
4392
  if (isTagToken(token) && token.name === 'endblock')
3638
4393
  return;
3639
- const template = liquid.parser.parseToken(token, remainTokens);
4394
+ const template = parser.parseToken(token, remainTokens);
3640
4395
  this.templates.push(template);
3641
4396
  }
3642
4397
  throw new Error(`tag ${token.getText()} not closed`);
@@ -3663,6 +4418,12 @@ class BlockTag extends Tag {
3663
4418
  ? (superBlock, emitter) => renderChild(new BlockDrop(() => renderCurrent(superBlock, emitter)), emitter)
3664
4419
  : renderCurrent;
3665
4420
  }
4421
+ *children() {
4422
+ return this.templates;
4423
+ }
4424
+ blockScope() {
4425
+ return ['block'];
4426
+ }
3666
4427
  }
3667
4428
 
3668
4429
  class RawTag extends Tag {
@@ -3706,7 +4467,7 @@ class TablerowloopDrop extends ForloopDrop {
3706
4467
  }
3707
4468
 
3708
4469
  class TablerowTag extends Tag {
3709
- constructor(tagToken, remainTokens, liquid) {
4470
+ constructor(tagToken, remainTokens, liquid, parser) {
3710
4471
  super(tagToken, remainTokens, liquid);
3711
4472
  const variable = this.tokenizer.readIdentifier();
3712
4473
  this.tokenizer.skipBlank();
@@ -3717,10 +4478,10 @@ class TablerowTag extends Tag {
3717
4478
  }
3718
4479
  this.variable = variable.content;
3719
4480
  this.collection = collectionToken;
3720
- this.args = new Hash(this.tokenizer.remaining());
4481
+ this.args = new Hash(this.tokenizer, liquid.options.keyValueSeparator);
3721
4482
  this.templates = [];
3722
4483
  let p;
3723
- const stream = this.liquid.parser.parseStream(remainTokens)
4484
+ const stream = parser.parseStream(remainTokens)
3724
4485
  .on('start', () => (p = this.templates))
3725
4486
  .on('tag:endtablerow', () => stream.stop())
3726
4487
  .on('template', (tpl) => p.push(tpl))
@@ -3755,18 +4516,32 @@ class TablerowTag extends Tag {
3755
4516
  emitter.write('</tr>');
3756
4517
  ctx.pop();
3757
4518
  }
4519
+ *children() {
4520
+ return this.templates;
4521
+ }
4522
+ *arguments() {
4523
+ yield this.collection;
4524
+ for (const v of Object.values(this.args.hash)) {
4525
+ if (isValueToken(v)) {
4526
+ yield v;
4527
+ }
4528
+ }
4529
+ }
4530
+ blockScope() {
4531
+ return [this.variable, 'tablerowloop'];
4532
+ }
3758
4533
  }
3759
4534
 
3760
4535
  class UnlessTag extends Tag {
3761
- constructor(tagToken, remainTokens, liquid) {
4536
+ constructor(tagToken, remainTokens, liquid, parser) {
3762
4537
  super(tagToken, remainTokens, liquid);
3763
4538
  this.branches = [];
3764
4539
  this.elseTemplates = [];
3765
4540
  let p = [];
3766
4541
  let elseCount = 0;
3767
- this.liquid.parser.parseStream(remainTokens)
4542
+ parser.parseStream(remainTokens)
3768
4543
  .on('start', () => this.branches.push({
3769
- value: new Value(tagToken.args, this.liquid),
4544
+ value: new Value(tagToken.tokenizer.readFilteredValue(), this.liquid),
3770
4545
  test: isFalsy,
3771
4546
  templates: (p = [])
3772
4547
  }))
@@ -3776,7 +4551,7 @@ class UnlessTag extends Tag {
3776
4551
  return;
3777
4552
  }
3778
4553
  this.branches.push({
3779
- value: new Value(token.args, this.liquid),
4554
+ value: new Value(token.tokenizer.readFilteredValue(), this.liquid),
3780
4555
  test: isTruthy,
3781
4556
  templates: (p = [])
3782
4557
  });
@@ -3805,17 +4580,27 @@ class UnlessTag extends Tag {
3805
4580
  }
3806
4581
  yield r.renderTemplates(this.elseTemplates, ctx, emitter);
3807
4582
  }
4583
+ *children() {
4584
+ const children = this.branches.flatMap(b => b.templates);
4585
+ if (this.elseTemplates) {
4586
+ children.push(...this.elseTemplates);
4587
+ }
4588
+ return children;
4589
+ }
4590
+ arguments() {
4591
+ return this.branches.map(b => b.value);
4592
+ }
3808
4593
  }
3809
4594
 
3810
4595
  class BreakTag extends Tag {
3811
- render(ctx, emitter) {
3812
- emitter['break'] = true;
4596
+ render(ctx, _emitter) {
4597
+ ctx.breakCalled = true;
3813
4598
  }
3814
4599
  }
3815
4600
 
3816
4601
  class ContinueTag extends Tag {
3817
- render(ctx, emitter) {
3818
- emitter['continue'] = true;
4602
+ render(ctx, _emitter) {
4603
+ ctx.continueCalled = true;
3819
4604
  }
3820
4605
  }
3821
4606
 
@@ -3833,17 +4618,25 @@ class EchoTag extends Tag {
3833
4618
  const val = yield this.value.value(ctx, false);
3834
4619
  emitter.write(val);
3835
4620
  }
4621
+ *arguments() {
4622
+ if (this.value) {
4623
+ yield this.value;
4624
+ }
4625
+ }
3836
4626
  }
3837
4627
 
3838
4628
  class LiquidTag extends Tag {
3839
- constructor(token, remainTokens, liquid) {
4629
+ constructor(token, remainTokens, liquid, parser) {
3840
4630
  super(token, remainTokens, liquid);
3841
4631
  const tokens = this.tokenizer.readLiquidTagTokens(this.liquid.options);
3842
- this.templates = this.liquid.parser.parseTokens(tokens);
4632
+ this.templates = parser.parseTokens(tokens);
3843
4633
  }
3844
4634
  *render(ctx, emitter) {
3845
4635
  yield this.liquid.renderer.renderTemplates(this.templates, ctx, emitter);
3846
4636
  }
4637
+ *children() {
4638
+ return this.templates;
4639
+ }
3847
4640
  }
3848
4641
 
3849
4642
  class InlineCommentTag extends Tag {
@@ -3886,12 +4679,14 @@ class Liquid {
3886
4679
  this.filters = {};
3887
4680
  this.tags = {};
3888
4681
  this.options = normalize(opts);
4682
+ // eslint-disable-next-line deprecation/deprecation
3889
4683
  this.parser = new Parser(this);
3890
4684
  forOwn(tags, (conf, name) => this.registerTag(name, conf));
3891
4685
  forOwn(filters, (handler, name) => this.registerFilter(name, handler));
3892
4686
  }
3893
4687
  parse(html, filepath) {
3894
- return this.parser.parse(html, filepath);
4688
+ const parser = new Parser(this);
4689
+ return parser.parse(html, filepath);
3895
4690
  }
3896
4691
  _render(tpl, scope, renderOptions) {
3897
4692
  const ctx = scope instanceof Context ? scope : new Context(scope, this.options, renderOptions);
@@ -3922,21 +4717,21 @@ class Liquid {
3922
4717
  return toValueSync(this._parseAndRender(html, scope, Object.assign(Object.assign({}, renderOptions), { sync: true })));
3923
4718
  }
3924
4719
  _parsePartialFile(file, sync, currentFile) {
3925
- return this.parser.parseFile(file, sync, LookupType.Partials, currentFile);
4720
+ return new Parser(this).parseFile(file, sync, LookupType.Partials, currentFile);
3926
4721
  }
3927
4722
  _parseLayoutFile(file, sync, currentFile) {
3928
- return this.parser.parseFile(file, sync, LookupType.Layouts, currentFile);
4723
+ return new Parser(this).parseFile(file, sync, LookupType.Layouts, currentFile);
3929
4724
  }
3930
4725
  _parseFile(file, sync, lookupType, currentFile) {
3931
- return this.parser.parseFile(file, sync, lookupType, currentFile);
4726
+ return new Parser(this).parseFile(file, sync, lookupType, currentFile);
3932
4727
  }
3933
4728
  parseFile(file, lookupType) {
3934
4729
  return __awaiter(this, void 0, void 0, function* () {
3935
- return toPromise(this.parser.parseFile(file, false, lookupType));
4730
+ return toPromise(new Parser(this).parseFile(file, false, lookupType));
3936
4731
  });
3937
4732
  }
3938
4733
  parseFileSync(file, lookupType) {
3939
- return toValueSync(this.parser.parseFile(file, true, lookupType));
4734
+ return toValueSync(new Parser(this).parseFile(file, true, lookupType));
3940
4735
  }
3941
4736
  *_renderFile(file, ctx, renderFileOptions) {
3942
4737
  const templates = (yield this._parseFile(file, renderFileOptions.sync, renderFileOptions.lookupType));
@@ -3992,9 +4787,97 @@ class Liquid {
3992
4787
  self.renderFile(filePath, ctx).then(html => callback(null, html), callback);
3993
4788
  };
3994
4789
  }
4790
+ analyze(template, options = {}) {
4791
+ return __awaiter(this, void 0, void 0, function* () {
4792
+ return analyze(template, options);
4793
+ });
4794
+ }
4795
+ analyzeSync(template, options = {}) {
4796
+ return analyzeSync(template, options);
4797
+ }
4798
+ parseAndAnalyze(html, filename, options = {}) {
4799
+ return __awaiter(this, void 0, void 0, function* () {
4800
+ return analyze(this.parse(html, filename), options);
4801
+ });
4802
+ }
4803
+ parseAndAnalyzeSync(html, filename, options = {}) {
4804
+ return analyzeSync(this.parse(html, filename), options);
4805
+ }
4806
+ /** Return an array of all variables without their properties. */
4807
+ variables(template, options = {}) {
4808
+ return __awaiter(this, void 0, void 0, function* () {
4809
+ const analysis = yield analyze(isString(template) ? this.parse(template) : template, options);
4810
+ return Object.keys(analysis.variables);
4811
+ });
4812
+ }
4813
+ /** Return an array of all variables without their properties. */
4814
+ variablesSync(template, options = {}) {
4815
+ const analysis = analyzeSync(isString(template) ? this.parse(template) : template, options);
4816
+ return Object.keys(analysis.variables);
4817
+ }
4818
+ /** Return an array of all variables including their properties/paths. */
4819
+ fullVariables(template, options = {}) {
4820
+ return __awaiter(this, void 0, void 0, function* () {
4821
+ const analysis = yield analyze(isString(template) ? this.parse(template) : template, options);
4822
+ return Array.from(new Set(Object.values(analysis.variables).flatMap((a) => a.map((v) => String(v)))));
4823
+ });
4824
+ }
4825
+ /** Return an array of all variables including their properties/paths. */
4826
+ fullVariablesSync(template, options = {}) {
4827
+ const analysis = analyzeSync(isString(template) ? this.parse(template) : template, options);
4828
+ return Array.from(new Set(Object.values(analysis.variables).flatMap((a) => a.map((v) => String(v)))));
4829
+ }
4830
+ /** Return an array of all variables, each as an array of properties/segments. */
4831
+ variableSegments(template, options = {}) {
4832
+ return __awaiter(this, void 0, void 0, function* () {
4833
+ const analysis = yield analyze(isString(template) ? this.parse(template) : template, options);
4834
+ return Array.from(strictUniq(Object.values(analysis.variables).flatMap((a) => a.map((v) => v.toArray()))));
4835
+ });
4836
+ }
4837
+ /** Return an array of all variables, each as an array of properties/segments. */
4838
+ variableSegmentsSync(template, options = {}) {
4839
+ const analysis = analyzeSync(isString(template) ? this.parse(template) : template, options);
4840
+ return Array.from(strictUniq(Object.values(analysis.variables).flatMap((a) => a.map((v) => v.toArray()))));
4841
+ }
4842
+ /** Return an array of all expected context variables without their properties. */
4843
+ globalVariables(template, options = {}) {
4844
+ return __awaiter(this, void 0, void 0, function* () {
4845
+ const analysis = yield analyze(isString(template) ? this.parse(template) : template, options);
4846
+ return Object.keys(analysis.globals);
4847
+ });
4848
+ }
4849
+ /** Return an array of all expected context variables without their properties. */
4850
+ globalVariablesSync(template, options = {}) {
4851
+ const analysis = analyzeSync(isString(template) ? this.parse(template) : template, options);
4852
+ return Object.keys(analysis.globals);
4853
+ }
4854
+ /** Return an array of all expected context variables including their properties/paths. */
4855
+ globalFullVariables(template, options = {}) {
4856
+ return __awaiter(this, void 0, void 0, function* () {
4857
+ const analysis = yield analyze(isString(template) ? this.parse(template) : template, options);
4858
+ return Array.from(new Set(Object.values(analysis.globals).flatMap((a) => a.map((v) => String(v)))));
4859
+ });
4860
+ }
4861
+ /** Return an array of all expected context variables including their properties/paths. */
4862
+ globalFullVariablesSync(template, options = {}) {
4863
+ const analysis = analyzeSync(isString(template) ? this.parse(template) : template, options);
4864
+ return Array.from(new Set(Object.values(analysis.globals).flatMap((a) => a.map((v) => String(v)))));
4865
+ }
4866
+ /** Return an array of all expected context variables, each as an array of properties/segments. */
4867
+ globalVariableSegments(template, options = {}) {
4868
+ return __awaiter(this, void 0, void 0, function* () {
4869
+ const analysis = yield analyze(isString(template) ? this.parse(template) : template, options);
4870
+ return Array.from(strictUniq(Object.values(analysis.globals).flatMap((a) => a.map((v) => v.toArray()))));
4871
+ });
4872
+ }
4873
+ /** Return an array of all expected context variables, each as an array of properties/segments. */
4874
+ globalVariableSegmentsSync(template, options = {}) {
4875
+ const analysis = analyzeSync(isString(template) ? this.parse(template) : template, options);
4876
+ return Array.from(strictUniq(Object.values(analysis.globals).flatMap((a) => a.map((v) => v.toArray()))));
4877
+ }
3995
4878
  }
3996
4879
 
3997
4880
  /* istanbul ignore file */
3998
- const version = '10.13.0';
4881
+ const version = '[VI]{version}[/VI]';
3999
4882
 
4000
- 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, RawTag, RenderError, RenderTag, TablerowTag, Tag, TagToken, TimezoneDate, Token, TokenKind, TokenizationError, Tokenizer, typeGuards as TypeGuards, UndefinedVariableError, UnlessTag, Value, assert, createTrie, defaultOperators, defaultOptions, evalQuotedToken, evalToken, filters, isFalsy, isTruthy, tags, toPromise, toValue, toValueSync, version };
4883
+ 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 };