@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,11 +1,12 @@
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
  import { PassThrough } from 'stream';
7
7
  import { sep, extname, resolve as resolve$1, dirname as dirname$1 } from 'path';
8
8
  import { statSync, readFileSync as readFileSync$1, stat, readFile as readFile$1 } from 'fs';
9
+ import { createRequire } from 'module';
9
10
 
10
11
  class Token {
11
12
  constructor(kind, input, begin, end, file) {
@@ -102,6 +103,10 @@ function toArray(val) {
102
103
  function toValue(value) {
103
104
  return (value instanceof Drop && isFunction(value.valueOf)) ? value.valueOf() : value;
104
105
  }
106
+ function toNumber(value) {
107
+ value = Number(value);
108
+ return isNaN(value) ? 0 : value;
109
+ }
105
110
  function isNumber(value) {
106
111
  return typeof value === 'number';
107
112
  }
@@ -120,6 +125,9 @@ function isArray(value) {
120
125
  // be compatible with IE 8
121
126
  return toString$1.call(value) === '[object Array]';
122
127
  }
128
+ function isArrayLike(value) {
129
+ return value && isNumber(value.length);
130
+ }
123
131
  function isIterable(value) {
124
132
  return isObject(value) && Symbol.iterator in value;
125
133
  }
@@ -201,14 +209,25 @@ function caseInsensitiveCompare(a, b) {
201
209
  return 0;
202
210
  }
203
211
  function argumentsToValue(fn) {
204
- return (...args) => fn(...args.map(toValue));
212
+ return function (...args) { return fn.call(this, ...args.map(toValue)); };
205
213
  }
206
214
  function escapeRegExp(text) {
207
215
  return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
216
+ }
217
+ /** Return an array containing unique elements from _array_. Works with nested arrays and objects. */
218
+ function* strictUniq(array) {
219
+ const seen = new Set();
220
+ for (const element of array) {
221
+ const key = JSON.stringify(element);
222
+ if (!seen.has(key)) {
223
+ seen.add(key);
224
+ yield element;
225
+ }
226
+ }
208
227
  }
209
228
 
210
229
  /**
211
- * targeting ES5, extends Error won't create a proper prototype chain, need a trait to kee track of classes
230
+ * targeting ES5, extends Error won't create a proper prototype chain, need a trait to keep track of classes
212
231
  */
213
232
  const TRAIT = '__liquidClass__';
214
233
  class LiquidError extends Error {
@@ -262,6 +281,19 @@ class RenderError extends LiquidError {
262
281
  return obj.name === 'RenderError';
263
282
  }
264
283
  }
284
+ class LiquidErrors extends LiquidError {
285
+ constructor(errors) {
286
+ super(errors[0], errors[0].token);
287
+ this.errors = errors;
288
+ this.name = 'LiquidErrors';
289
+ const s = errors.length > 1 ? 's' : '';
290
+ this.message = `${errors.length} error${s} found`;
291
+ super.update();
292
+ }
293
+ static is(obj) {
294
+ return obj.name === 'LiquidErrors';
295
+ }
296
+ }
265
297
  class UndefinedVariableError extends LiquidError {
266
298
  constructor(err, token) {
267
299
  super(err, token);
@@ -340,6 +372,9 @@ function assert(predicate, message) {
340
372
  : (message || `expect ${predicate} to be true`);
341
373
  throw new AssertionError(msg);
342
374
  }
375
+ }
376
+ function assertEmpty(predicate, message = `unexpected ${JSON.stringify(predicate)}`) {
377
+ assert(!predicate, message);
343
378
  }
344
379
 
345
380
  class NullDrop extends Drop {
@@ -389,6 +424,9 @@ class EmptyDrop extends Drop {
389
424
  valueOf() {
390
425
  return '';
391
426
  }
427
+ static is(value) {
428
+ return value instanceof EmptyDrop;
429
+ }
392
430
  }
393
431
 
394
432
  class BlankDrop extends EmptyDrop {
@@ -401,6 +439,9 @@ class BlankDrop extends EmptyDrop {
401
439
  return /^\s*$/.test(value);
402
440
  return super.equals(value);
403
441
  }
442
+ static is(value) {
443
+ return value instanceof BlankDrop;
444
+ }
404
445
  }
405
446
 
406
447
  class ForloopDrop extends Drop {
@@ -453,7 +494,12 @@ class BlockDrop extends Drop {
453
494
  }
454
495
 
455
496
  function isComparable(arg) {
456
- return arg && isFunction(arg.equals);
497
+ return (arg &&
498
+ isFunction(arg.equals) &&
499
+ isFunction(arg.gt) &&
500
+ isFunction(arg.geq) &&
501
+ isFunction(arg.lt) &&
502
+ isFunction(arg.leq));
457
503
  }
458
504
 
459
505
  const nil = new NullDrop();
@@ -573,18 +619,6 @@ function toValueSync(val) {
573
619
  }
574
620
 
575
621
  const rFormat = /%([-_0^#:]+)?(\d+)?([EO])?(.)/;
576
- const monthNames = [
577
- 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August',
578
- 'September', 'October', 'November', 'December'
579
- ];
580
- const dayNames = [
581
- 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'
582
- ];
583
- const monthNamesShort = monthNames.map(abbr);
584
- const dayNamesShort = dayNames.map(abbr);
585
- function abbr(str) {
586
- return str.slice(0, 3);
587
- }
588
622
  // prototype extensions
589
623
  function daysInMonth(d) {
590
624
  const feb = isLeapYear(d) ? 29 : 28;
@@ -639,19 +673,7 @@ const padWidths = {
639
673
  U: 2,
640
674
  W: 2
641
675
  };
642
- // default to '0'
643
- const padChars = {
644
- a: ' ',
645
- A: ' ',
646
- b: ' ',
647
- B: ' ',
648
- c: ' ',
649
- e: ' ',
650
- k: ' ',
651
- l: ' ',
652
- p: ' ',
653
- P: ' '
654
- };
676
+ const padSpaceChars = new Set('aAbBceklpP');
655
677
  function getTimezoneOffset(d, opts) {
656
678
  const nOffset = Math.abs(d.getTimezoneOffset());
657
679
  const h = Math.floor(nOffset / 60);
@@ -662,10 +684,10 @@ function getTimezoneOffset(d, opts) {
662
684
  padStart(m, 2, '0');
663
685
  }
664
686
  const formatCodes = {
665
- a: (d) => dayNamesShort[d.getDay()],
666
- A: (d) => dayNames[d.getDay()],
667
- b: (d) => monthNamesShort[d.getMonth()],
668
- B: (d) => monthNames[d.getMonth()],
687
+ a: (d) => d.getShortWeekdayName(),
688
+ A: (d) => d.getLongWeekdayName(),
689
+ b: (d) => d.getShortMonthName(),
690
+ B: (d) => d.getLongMonthName(),
669
691
  c: (d) => d.toLocaleString(),
670
692
  C: (d) => century(d),
671
693
  d: (d) => d.getDate(),
@@ -697,12 +719,7 @@ const formatCodes = {
697
719
  y: (d) => d.getFullYear().toString().slice(2, 4),
698
720
  Y: (d) => d.getFullYear(),
699
721
  z: getTimezoneOffset,
700
- Z: (d, opts) => {
701
- if (d.getTimezoneName) {
702
- return d.getTimezoneName() || getTimezoneOffset(d, opts);
703
- }
704
- return (typeof Intl !== 'undefined' ? Intl.DateTimeFormat().resolvedOptions().timeZone : '');
705
- },
722
+ Z: (d, opts) => d.getTimeZoneName() || getTimezoneOffset(d, opts),
706
723
  't': () => '\t',
707
724
  'n': () => '\n',
708
725
  '%': () => '%'
@@ -728,7 +745,7 @@ function format(d, match) {
728
745
  for (const flag of flagStr)
729
746
  flags[flag] = true;
730
747
  let ret = String(convert(d, { flags, width, modifier }));
731
- let padChar = padChars[conversion] || '0';
748
+ let padChar = padSpaceChars.has(conversion) ? ' ' : '0';
732
749
  let padWidth = width || padWidths[conversion] || 0;
733
750
  if (flags['^'])
734
751
  ret = ret.toUpperCase();
@@ -743,9 +760,26 @@ function format(d, match) {
743
760
  return padStart(ret, padWidth, padChar);
744
761
  }
745
762
 
763
+ function getDateTimeFormat() {
764
+ return (typeof Intl !== 'undefined' ? Intl.DateTimeFormat : undefined);
765
+ }
766
+
746
767
  // one minute in milliseconds
747
768
  const OneMinute = 60000;
748
- const ISO8601_TIMEZONE_PATTERN = /([zZ]|([+-])(\d{2}):(\d{2}))$/;
769
+ /**
770
+ * Need support both ISO8601 and RFC2822 as in major browsers & NodeJS
771
+ * RFC2822: https://datatracker.ietf.org/doc/html/rfc2822#section-3.3
772
+ */
773
+ const TIMEZONE_PATTERN = /([zZ]|([+-])(\d{2}):?(\d{2}))$/;
774
+ const monthNames = [
775
+ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August',
776
+ 'September', 'October', 'November', 'December'
777
+ ];
778
+ const monthNamesShort = monthNames.map(name => name.slice(0, 3));
779
+ const dayNames = [
780
+ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'
781
+ ];
782
+ const dayNamesShort = dayNames.map(name => name.slice(0, 3));
749
783
  /**
750
784
  * A date implementation with timezone info, just like Ruby date
751
785
  *
@@ -753,12 +787,16 @@ const ISO8601_TIMEZONE_PATTERN = /([zZ]|([+-])(\d{2}):(\d{2}))$/;
753
787
  * - create a Date offset by it's timezone difference, avoiding overriding a bunch of methods
754
788
  * - rewrite getTimezoneOffset() to trick strftime
755
789
  */
756
- class TimezoneDate {
757
- constructor(init, timezone) {
758
- this.date = init instanceof TimezoneDate
759
- ? init.date
760
- : new Date(init);
761
- this.timezoneOffset = isString(timezone) ? TimezoneDate.getTimezoneOffset(timezone, this.date) : timezone;
790
+ class LiquidDate {
791
+ constructor(init, locale, timezone) {
792
+ this.locale = locale;
793
+ this.DateTimeFormat = getDateTimeFormat();
794
+ this.date = new Date(init);
795
+ this.timezoneFixed = timezone !== undefined;
796
+ if (timezone === undefined) {
797
+ timezone = this.date.getTimezoneOffset();
798
+ }
799
+ this.timezoneOffset = isString(timezone) ? LiquidDate.getTimezoneOffset(timezone, this.date) : timezone;
762
800
  this.timezoneName = isString(timezone) ? timezone : '';
763
801
  const diff = (this.date.getTimezoneOffset() - this.timezoneOffset) * OneMinute;
764
802
  const time = this.date.getTime() + diff;
@@ -806,8 +844,34 @@ class TimezoneDate {
806
844
  getTimezoneOffset() {
807
845
  return this.timezoneOffset;
808
846
  }
809
- getTimezoneName() {
810
- return this.timezoneName;
847
+ getTimeZoneName() {
848
+ if (this.timezoneFixed)
849
+ return this.timezoneName;
850
+ if (!this.DateTimeFormat)
851
+ return;
852
+ return this.DateTimeFormat().resolvedOptions().timeZone;
853
+ }
854
+ getLongMonthName() {
855
+ var _a;
856
+ return (_a = this.format({ month: 'long' })) !== null && _a !== void 0 ? _a : monthNames[this.getMonth()];
857
+ }
858
+ getShortMonthName() {
859
+ var _a;
860
+ return (_a = this.format({ month: 'short' })) !== null && _a !== void 0 ? _a : monthNamesShort[this.getMonth()];
861
+ }
862
+ getLongWeekdayName() {
863
+ var _a;
864
+ return (_a = this.format({ weekday: 'long' })) !== null && _a !== void 0 ? _a : dayNames[this.displayDate.getDay()];
865
+ }
866
+ getShortWeekdayName() {
867
+ var _a;
868
+ return (_a = this.format({ weekday: 'short' })) !== null && _a !== void 0 ? _a : dayNamesShort[this.displayDate.getDay()];
869
+ }
870
+ valid() {
871
+ return !isNaN(this.getTime());
872
+ }
873
+ format(options) {
874
+ return this.DateTimeFormat && this.DateTimeFormat(this.locale, options).format(this.displayDate);
811
875
  }
812
876
  /**
813
877
  * Create a Date object fixed to it's declared Timezone. Both
@@ -822,21 +886,21 @@ class TimezoneDate {
822
886
  * we create a different Date to trick strftime, it's both simpler and more performant.
823
887
  * Given that a template is expected to be parsed fewer times than rendered.
824
888
  */
825
- static createDateFixedToTimezone(dateString) {
826
- const m = dateString.match(ISO8601_TIMEZONE_PATTERN);
889
+ static createDateFixedToTimezone(dateString, locale) {
890
+ const m = dateString.match(TIMEZONE_PATTERN);
827
891
  // representing a UTC timestamp
828
892
  if (m && m[1] === 'Z') {
829
- return new TimezoneDate(+new Date(dateString), 0);
893
+ return new LiquidDate(+new Date(dateString), locale, 0);
830
894
  }
831
895
  // has a timezone specified
832
896
  if (m && m[2] && m[3] && m[4]) {
833
897
  const [, , sign, hours, minutes] = m;
834
898
  const offset = (sign === '+' ? -1 : 1) * (parseInt(hours, 10) * 60 + parseInt(minutes, 10));
835
- return new TimezoneDate(+new Date(dateString), offset);
899
+ return new LiquidDate(+new Date(dateString), locale, offset);
836
900
  }
837
- return new Date(dateString);
901
+ return new LiquidDate(dateString, locale);
838
902
  }
839
- static getTimezoneOffset(timezoneName, date = new Date()) {
903
+ static getTimezoneOffset(timezoneName, date) {
840
904
  const localDateString = date.toLocaleString('en-US', { timeZone: timezoneName });
841
905
  const utcDateString = date.toLocaleString('en-US', { timeZone: 'UTC' });
842
906
  const localDate = new Date(localDateString);
@@ -845,6 +909,23 @@ class TimezoneDate {
845
909
  }
846
910
  }
847
911
 
912
+ class Limiter {
913
+ constructor(resource, limit) {
914
+ this.base = 0;
915
+ this.message = `${resource} limit exceeded`;
916
+ this.limit = limit;
917
+ }
918
+ use(count) {
919
+ count = toNumber(count);
920
+ assert(this.base + count <= this.limit, this.message);
921
+ this.base += count;
922
+ }
923
+ check(count) {
924
+ count = toNumber(count);
925
+ assert(count <= this.limit, this.message);
926
+ }
927
+ }
928
+
848
929
  class DelimitedToken extends Token {
849
930
  constructor(kind, [contentBegin, contentEnd], input, begin, end, trimLeft, trimRight, file) {
850
931
  super(kind, input, begin, end, file);
@@ -925,16 +1006,6 @@ class IdentifierToken extends Token {
925
1006
  this.file = file;
926
1007
  this.content = this.getText();
927
1008
  }
928
- isNumber(allowSign = false) {
929
- const begin = allowSign && TYPES[this.input.charCodeAt(this.begin)] & SIGN
930
- ? this.begin + 1
931
- : this.begin;
932
- for (let i = begin; i < this.end; i++) {
933
- if (!(TYPES[this.input.charCodeAt(i)] & NUMBER))
934
- return false;
935
- }
936
- return true;
937
- }
938
1009
  }
939
1010
 
940
1011
  class LiteralToken extends Token {
@@ -1102,7 +1173,9 @@ class LiquidTagToken extends DelimitedToken {
1102
1173
  this.name = this.tokenizer.readTagName();
1103
1174
  this.tokenizer.assert(this.name, 'illegal liquid tag syntax');
1104
1175
  this.tokenizer.skipBlank();
1105
- this.args = this.tokenizer.remaining();
1176
+ }
1177
+ get args() {
1178
+ return this.tokenizer.input.slice(this.tokenizer.p, this.contentRange[1]);
1106
1179
  }
1107
1180
  }
1108
1181
 
@@ -1123,6 +1196,15 @@ class FilteredValueToken extends Token {
1123
1196
  }
1124
1197
  }
1125
1198
 
1199
+ const polyfill = {
1200
+ now: () => Date.now()
1201
+ };
1202
+ function getPerformance() {
1203
+ return (typeof global === 'object' && global.performance) ||
1204
+ (typeof window === 'object' && window.performance) ||
1205
+ polyfill;
1206
+ }
1207
+
1126
1208
  class SimpleEmitter {
1127
1209
  constructor() {
1128
1210
  this.buffer = '';
@@ -1178,20 +1260,28 @@ class Render {
1178
1260
  if (!emitter) {
1179
1261
  emitter = ctx.opts.keepOutputType ? new KeepingTypeEmitter() : new SimpleEmitter();
1180
1262
  }
1263
+ const errors = [];
1181
1264
  for (const tpl of templates) {
1265
+ ctx.renderLimit.check(getPerformance().now());
1182
1266
  try {
1183
1267
  // if tpl.render supports emitter, it'll return empty `html`
1184
1268
  const html = yield tpl.render(ctx, emitter);
1185
1269
  // if not, it'll return an `html`, write to the emitter for it
1186
1270
  html && emitter.write(html);
1187
- if (emitter['break'] || emitter['continue'])
1271
+ if (ctx.breakCalled || ctx.continueCalled)
1188
1272
  break;
1189
1273
  }
1190
1274
  catch (e) {
1191
- const err = RenderError.is(e) ? e : new RenderError(e, tpl);
1192
- throw err;
1275
+ const err = LiquidError.is(e) ? e : new RenderError(e, tpl);
1276
+ if (ctx.opts.catchAllErrors)
1277
+ errors.push(err);
1278
+ else
1279
+ throw err;
1193
1280
  }
1194
1281
  }
1282
+ if (errors.length) {
1283
+ throw new LiquidErrors(errors);
1284
+ }
1195
1285
  return emitter.buffer;
1196
1286
  }
1197
1287
  }
@@ -1262,6 +1352,7 @@ function evalQuotedToken(token) {
1262
1352
  function* evalRangeToken(token, ctx) {
1263
1353
  const low = yield evalToken(token.lhs, ctx);
1264
1354
  const high = yield evalToken(token.rhs, ctx);
1355
+ ctx.memoryLimit.use(high - low + 1);
1265
1356
  return range(+low, +high + 1);
1266
1357
  }
1267
1358
  function* toPostfix(tokens) {
@@ -1285,6 +1376,7 @@ function isTruthy(val, ctx) {
1285
1376
  return !isFalsy(val, ctx);
1286
1377
  }
1287
1378
  function isFalsy(val, ctx) {
1379
+ val = toValue(val);
1288
1380
  if (ctx.opts.jsTruthy) {
1289
1381
  return !val;
1290
1382
  }
@@ -1352,6 +1444,9 @@ function arrayEquals(lhs, rhs) {
1352
1444
  if (lhs.length !== rhs.length)
1353
1445
  return false;
1354
1446
  return !lhs.some((value, i) => !equals(value, rhs[i]));
1447
+ }
1448
+ function arrayIncludes(arr, item) {
1449
+ return arr.some(value => equals(value, item));
1355
1450
  }
1356
1451
 
1357
1452
  class Node {
@@ -1412,7 +1507,10 @@ class LRU {
1412
1507
  }
1413
1508
  }
1414
1509
 
1415
- const requireResolve = require.resolve;
1510
+ function requireResolve (file) {
1511
+ const require = createRequire(process.cwd() + '/');
1512
+ return require.resolve(file)
1513
+ }
1416
1514
 
1417
1515
  const statAsync = promisify(stat);
1418
1516
  const readFileAsync = promisify(readFile$1);
@@ -1531,22 +1629,30 @@ const unescapeMap = {
1531
1629
  '&#39;': "'"
1532
1630
  };
1533
1631
  function escape(str) {
1534
- return stringify(str).replace(/&|<|>|"|'/g, m => escapeMap[m]);
1632
+ str = stringify(str);
1633
+ this.context.memoryLimit.use(str.length);
1634
+ return str.replace(/&|<|>|"|'/g, m => escapeMap[m]);
1535
1635
  }
1536
1636
  function xml_escape(str) {
1537
- return escape(str);
1637
+ return escape.call(this, str);
1538
1638
  }
1539
1639
  function unescape(str) {
1540
- return stringify(str).replace(/&(amp|lt|gt|#34|#39);/g, m => unescapeMap[m]);
1640
+ str = stringify(str);
1641
+ this.context.memoryLimit.use(str.length);
1642
+ return str.replace(/&(amp|lt|gt|#34|#39);/g, m => unescapeMap[m]);
1541
1643
  }
1542
1644
  function escape_once(str) {
1543
- return escape(unescape(stringify(str)));
1645
+ return escape.call(this, unescape.call(this, str));
1544
1646
  }
1545
1647
  function newline_to_br(v) {
1546
- return stringify(v).replace(/\r?\n/gm, '<br />\n');
1648
+ const str = stringify(v);
1649
+ this.context.memoryLimit.use(str.length);
1650
+ return str.replace(/\r?\n/gm, '<br />\n');
1547
1651
  }
1548
1652
  function strip_html(v) {
1549
- return stringify(v).replace(/<script[\s\S]*?<\/script>|<style[\s\S]*?<\/style>|<.*?>|<!--[\s\S]*?-->/g, '');
1653
+ const str = stringify(v);
1654
+ this.context.memoryLimit.use(str.length);
1655
+ return str.replace(/<script[\s\S]*?<\/script>|<style[\s\S]*?<\/style>|<.*?>|<!--[\s\S]*?-->/g, '');
1550
1656
  }
1551
1657
 
1552
1658
  var htmlFilters = /*#__PURE__*/Object.freeze({
@@ -1558,18 +1664,68 @@ var htmlFilters = /*#__PURE__*/Object.freeze({
1558
1664
  strip_html: strip_html
1559
1665
  });
1560
1666
 
1667
+ class MapFS {
1668
+ constructor(mapping) {
1669
+ this.mapping = mapping;
1670
+ this.sep = '/';
1671
+ }
1672
+ exists(filepath) {
1673
+ return __awaiter(this, void 0, void 0, function* () {
1674
+ return this.existsSync(filepath);
1675
+ });
1676
+ }
1677
+ existsSync(filepath) {
1678
+ return !isNil(this.mapping[filepath]);
1679
+ }
1680
+ readFile(filepath) {
1681
+ return __awaiter(this, void 0, void 0, function* () {
1682
+ return this.readFileSync(filepath);
1683
+ });
1684
+ }
1685
+ readFileSync(filepath) {
1686
+ const content = this.mapping[filepath];
1687
+ if (isNil(content))
1688
+ throw new Error(`ENOENT: ${filepath}`);
1689
+ return content;
1690
+ }
1691
+ dirname(filepath) {
1692
+ const segments = filepath.split(this.sep);
1693
+ segments.pop();
1694
+ return segments.join(this.sep);
1695
+ }
1696
+ resolve(dir, file, ext) {
1697
+ file += ext;
1698
+ if (dir === '.')
1699
+ return file;
1700
+ const segments = dir.split(/\/+/);
1701
+ for (const segment of file.split(this.sep)) {
1702
+ if (segment === '.' || segment === '')
1703
+ continue;
1704
+ else if (segment === '..') {
1705
+ if (segments.length > 1 || segments[0] !== '')
1706
+ segments.pop();
1707
+ }
1708
+ else
1709
+ segments.push(segment);
1710
+ }
1711
+ return segments.join(this.sep);
1712
+ }
1713
+ }
1714
+
1561
1715
  const defaultOptions = {
1562
1716
  root: ['.'],
1563
1717
  layouts: ['.'],
1564
1718
  partials: ['.'],
1565
1719
  relativeReference: true,
1566
1720
  jekyllInclude: false,
1721
+ keyValueSeparator: ':',
1567
1722
  cache: undefined,
1568
1723
  extname: '',
1569
1724
  fs: fs,
1570
1725
  dynamicPartials: true,
1571
1726
  jsTruthy: false,
1572
1727
  dateFormat: '%A, %B %-e, %Y at %-l:%M %P %z',
1728
+ locale: '',
1573
1729
  trimTagRight: false,
1574
1730
  trimTagLeft: false,
1575
1731
  trimOutputRight: false,
@@ -1586,9 +1742,13 @@ const defaultOptions = {
1586
1742
  lenientIf: false,
1587
1743
  globals: {},
1588
1744
  keepOutputType: false,
1589
- operators: defaultOperators
1745
+ operators: defaultOperators,
1746
+ memoryLimit: Infinity,
1747
+ parseLimit: Infinity,
1748
+ renderLimit: Infinity
1590
1749
  };
1591
1750
  function normalize(options) {
1751
+ var _a, _b;
1592
1752
  if (options.hasOwnProperty('root')) {
1593
1753
  if (!options.hasOwnProperty('partials'))
1594
1754
  options.partials = options.root;
@@ -1614,6 +1774,14 @@ function normalize(options) {
1614
1774
  options.partials = normalizeDirectoryList(options.partials);
1615
1775
  options.layouts = normalizeDirectoryList(options.layouts);
1616
1776
  options.outputEscape = options.outputEscape && getOutputEscapeFunction(options.outputEscape);
1777
+ if (!options.locale) {
1778
+ options.locale = (_b = (_a = getDateTimeFormat()) === null || _a === void 0 ? void 0 : _a().resolvedOptions().locale) !== null && _b !== void 0 ? _b : 'en-US';
1779
+ }
1780
+ if (options.templates) {
1781
+ options.fs = new MapFS(options.templates);
1782
+ options.relativeReference = true;
1783
+ options.root = options.partials = options.layouts = '.';
1784
+ }
1617
1785
  return options;
1618
1786
  }
1619
1787
  function getOutputEscapeFunction(nameOrFunction) {
@@ -1740,9 +1908,7 @@ class Tokenizer {
1740
1908
  this.skipBlank();
1741
1909
  if (this.end())
1742
1910
  return null;
1743
- this.assert(this.peek() === '|', `expected "|" before filter`);
1744
- this.p++;
1745
- const begin = this.p;
1911
+ this.assert(this.read() === '|', `expected "|" before filter`);
1746
1912
  const name = this.readIdentifier();
1747
1913
  if (!name.size()) {
1748
1914
  this.assert(this.end(), `expected filter name`);
@@ -1763,7 +1929,7 @@ class Tokenizer {
1763
1929
  else {
1764
1930
  throw this.error('expected ":" after filter name');
1765
1931
  }
1766
- return new FilterToken(name.getText(), args, this.input, begin, this.p, this.file);
1932
+ return new FilterToken(name.getText(), args, this.input, name.begin, this.p, this.file);
1767
1933
  }
1768
1934
  readFilterArg() {
1769
1935
  const key = this.readValue();
@@ -1804,7 +1970,7 @@ class Tokenizer {
1804
1970
  }
1805
1971
  return new HTMLToken(this.input, begin, this.p, this.file);
1806
1972
  }
1807
- readTagToken(options = defaultOptions) {
1973
+ readTagToken(options) {
1808
1974
  const { file, input } = this;
1809
1975
  const begin = this.p;
1810
1976
  if (this.readToDelimiter(options.tagDelimiterRight) === -1) {
@@ -1935,7 +2101,7 @@ class Tokenizer {
1935
2101
  return;
1936
2102
  let value;
1937
2103
  this.skipBlank();
1938
- const sep = jekyllStyle ? '=' : ':';
2104
+ const sep = isString(jekyllStyle) ? jekyllStyle : (jekyllStyle ? '=' : ':');
1939
2105
  if (this.peek() === sep) {
1940
2106
  ++this.p;
1941
2107
  value = this.readValue();
@@ -1951,6 +2117,9 @@ class Tokenizer {
1951
2117
  end() {
1952
2118
  return this.p >= this.N;
1953
2119
  }
2120
+ read() {
2121
+ return this.input[this.p++];
2122
+ }
1954
2123
  readTo(end) {
1955
2124
  while (this.p < this.N) {
1956
2125
  ++this.p;
@@ -2174,9 +2343,9 @@ class Tag extends TemplateImpl {
2174
2343
  * hash['reversed'] === undefined
2175
2344
  */
2176
2345
  class Hash {
2177
- constructor(markup, jekyllStyle) {
2346
+ constructor(input, jekyllStyle) {
2178
2347
  this.hash = {};
2179
- const tokenizer = new Tokenizer(markup, {});
2348
+ const tokenizer = input instanceof Tokenizer ? input : new Tokenizer(input, {});
2180
2349
  for (const hash of tokenizer.readHashes(jekyllStyle)) {
2181
2350
  this.hash[hash.name.content] = hash.value;
2182
2351
  }
@@ -2199,7 +2368,7 @@ function createTagClass(options) {
2199
2368
  }
2200
2369
  }
2201
2370
  *render(ctx, emitter) {
2202
- const hash = (yield new Hash(this.token.args).render(ctx));
2371
+ const hash = (yield new Hash(this.token.args, ctx.opts.keyValueSeparator).render(ctx));
2203
2372
  return yield options.render.call(this, ctx, emitter, hash);
2204
2373
  }
2205
2374
  };
@@ -2210,13 +2379,14 @@ function isKeyValuePair(arr) {
2210
2379
  }
2211
2380
 
2212
2381
  class Filter {
2213
- constructor(name, options, args, liquid) {
2214
- this.name = name;
2382
+ constructor(token, options, liquid) {
2383
+ this.token = token;
2384
+ this.name = token.name;
2215
2385
  this.handler = isFunction(options)
2216
2386
  ? options
2217
2387
  : (isFunction(options === null || options === void 0 ? void 0 : options.handler) ? options.handler : identify);
2218
2388
  this.raw = !isFunction(options) && !!(options === null || options === void 0 ? void 0 : options.raw);
2219
- this.args = args;
2389
+ this.args = token.args;
2220
2390
  this.liquid = liquid;
2221
2391
  }
2222
2392
  *render(value, context) {
@@ -2227,7 +2397,7 @@ class Filter {
2227
2397
  else
2228
2398
  argv.push(yield evalToken(arg, context));
2229
2399
  }
2230
- return yield this.handler.apply({ context, liquid: this.liquid }, [value, ...argv]);
2400
+ return yield this.handler.apply({ context, token: this.token, liquid: this.liquid }, [value, ...argv]);
2231
2401
  }
2232
2402
  }
2233
2403
 
@@ -2241,7 +2411,7 @@ class Value {
2241
2411
  ? new Tokenizer(input, liquid.options.operators).readFilteredValue()
2242
2412
  : input;
2243
2413
  this.initial = token.initial;
2244
- this.filters = token.filters.map(({ name, args }) => new Filter(name, this.getFilter(liquid, name), args, liquid));
2414
+ this.filters = token.filters.map(token => new Filter(token, this.getFilter(liquid, token.name), liquid));
2245
2415
  }
2246
2416
  *value(ctx, lenient) {
2247
2417
  lenient = lenient || (ctx.opts.lenientIf && this.filters.length > 0 && this.filters[0].name === 'default');
@@ -2267,13 +2437,17 @@ class Output extends TemplateImpl {
2267
2437
  const filters = this.value.filters;
2268
2438
  const outputEscape = liquid.options.outputEscape;
2269
2439
  if (!((_a = filters[filters.length - 1]) === null || _a === void 0 ? void 0 : _a.raw) && outputEscape) {
2270
- filters.push(new Filter(toString.call(outputEscape), outputEscape, [], liquid));
2440
+ const token = new FilterToken(toString.call(outputEscape), [], '', 0, 0);
2441
+ filters.push(new Filter(token, outputEscape, liquid));
2271
2442
  }
2272
2443
  }
2273
2444
  *render(ctx, emitter) {
2274
2445
  const val = yield this.value.value(ctx, false);
2275
2446
  emitter.write(val);
2276
2447
  }
2448
+ *arguments() {
2449
+ yield this.value;
2450
+ }
2277
2451
  }
2278
2452
 
2279
2453
  class HTML extends TemplateImpl {
@@ -2286,6 +2460,336 @@ class HTML extends TemplateImpl {
2286
2460
  }
2287
2461
  }
2288
2462
 
2463
+ /**
2464
+ * A variable's segments and location, which can be coerced to a string.
2465
+ */
2466
+ class Variable {
2467
+ constructor(segments, location) {
2468
+ this.segments = segments;
2469
+ this.location = location;
2470
+ }
2471
+ toString() {
2472
+ return segmentsString(this.segments, true);
2473
+ }
2474
+ /** Return this variable's segments as an array, possibly with nested arrays for nested paths. */
2475
+ toArray() {
2476
+ function* _visit(...segments) {
2477
+ for (const segment of segments) {
2478
+ if (segment instanceof Variable) {
2479
+ yield Array.from(_visit(...segment.segments));
2480
+ }
2481
+ else {
2482
+ yield segment;
2483
+ }
2484
+ }
2485
+ }
2486
+ return Array.from(_visit(...this.segments));
2487
+ }
2488
+ }
2489
+ /**
2490
+ * Group variables by the string representation of their root segment.
2491
+ */
2492
+ class VariableMap {
2493
+ constructor() {
2494
+ this.map = new Map();
2495
+ }
2496
+ get(key) {
2497
+ const k = segmentsString([key.segments[0]]);
2498
+ if (!this.map.has(k)) {
2499
+ this.map.set(k, []);
2500
+ }
2501
+ return this.map.get(k);
2502
+ }
2503
+ has(key) {
2504
+ return this.map.has(segmentsString([key.segments[0]]));
2505
+ }
2506
+ push(variable) {
2507
+ this.get(variable).push(variable);
2508
+ }
2509
+ asObject() {
2510
+ return Object.fromEntries(this.map);
2511
+ }
2512
+ }
2513
+ const defaultStaticAnalysisOptions = {
2514
+ partials: true
2515
+ };
2516
+ function* _analyze(templates, partials, sync) {
2517
+ const variables = new VariableMap();
2518
+ const globals = new VariableMap();
2519
+ const locals = new VariableMap();
2520
+ const rootScope = new DummyScope(new Set());
2521
+ // Names of partial templates that we've already analyzed.
2522
+ const seen = new Set();
2523
+ function updateVariables(variable, scope) {
2524
+ variables.push(variable);
2525
+ const aliased = scope.alias(variable);
2526
+ if (aliased !== undefined) {
2527
+ const root = aliased.segments[0];
2528
+ // TODO: What if a a template renders a rendered template? Do we need scope.parent?
2529
+ if (isString(root) && !rootScope.has(root)) {
2530
+ globals.push(aliased);
2531
+ }
2532
+ }
2533
+ else {
2534
+ const root = variable.segments[0];
2535
+ if (isString(root) && !scope.has(root)) {
2536
+ globals.push(variable);
2537
+ }
2538
+ }
2539
+ // Recurse for nested Variables
2540
+ for (const segment of variable.segments) {
2541
+ if (segment instanceof Variable) {
2542
+ updateVariables(segment, scope);
2543
+ }
2544
+ }
2545
+ }
2546
+ function* visit(template, scope) {
2547
+ if (template.arguments) {
2548
+ for (const arg of template.arguments()) {
2549
+ for (const variable of extractVariables(arg)) {
2550
+ updateVariables(variable, scope);
2551
+ }
2552
+ }
2553
+ }
2554
+ if (template.localScope) {
2555
+ for (const ident of template.localScope()) {
2556
+ scope.add(ident.content);
2557
+ scope.deleteAlias(ident.content);
2558
+ const [row, col] = ident.getPosition();
2559
+ locals.push(new Variable([ident.content], { row, col, file: ident.file }));
2560
+ }
2561
+ }
2562
+ if (template.children) {
2563
+ if (template.partialScope) {
2564
+ const partial = template.partialScope();
2565
+ if (partial === undefined) {
2566
+ // Layouts, for example, can have children that are not partials.
2567
+ for (const child of (yield template.children(partials, sync))) {
2568
+ yield visit(child, scope);
2569
+ }
2570
+ return;
2571
+ }
2572
+ if (seen.has(partial.name))
2573
+ return;
2574
+ const partialScopeNames = new Set();
2575
+ const partialScope = partial.isolated
2576
+ ? new DummyScope(partialScopeNames)
2577
+ : scope.push(partialScopeNames);
2578
+ for (const name of partial.scope) {
2579
+ if (isString(name)) {
2580
+ partialScopeNames.add(name);
2581
+ }
2582
+ else {
2583
+ const [alias, argument] = name;
2584
+ partialScopeNames.add(alias);
2585
+ const variables = Array.from(extractVariables(argument));
2586
+ if (variables.length) {
2587
+ partialScope.setAlias(alias, variables[0].segments);
2588
+ }
2589
+ }
2590
+ }
2591
+ for (const child of (yield template.children(partials, sync))) {
2592
+ yield visit(child, partialScope);
2593
+ seen.add(partial.name);
2594
+ }
2595
+ partialScope.pop();
2596
+ }
2597
+ else {
2598
+ if (template.blockScope) {
2599
+ scope.push(new Set(template.blockScope()));
2600
+ }
2601
+ for (const child of (yield template.children(partials, sync))) {
2602
+ yield visit(child, scope);
2603
+ }
2604
+ if (template.blockScope) {
2605
+ scope.pop();
2606
+ }
2607
+ }
2608
+ }
2609
+ }
2610
+ for (const template of templates) {
2611
+ yield visit(template, rootScope);
2612
+ }
2613
+ return {
2614
+ variables: variables.asObject(),
2615
+ globals: globals.asObject(),
2616
+ locals: locals.asObject()
2617
+ };
2618
+ }
2619
+ /**
2620
+ * Statically analyze a template and report variable usage.
2621
+ */
2622
+ function analyze(template, options = {}) {
2623
+ const opts = Object.assign(Object.assign({}, defaultStaticAnalysisOptions), options);
2624
+ return toPromise(_analyze(template, opts.partials, false));
2625
+ }
2626
+ /**
2627
+ * Statically analyze a template and report variable usage.
2628
+ */
2629
+ function analyzeSync(template, options = {}) {
2630
+ const opts = Object.assign(Object.assign({}, defaultStaticAnalysisOptions), options);
2631
+ return toValueSync(_analyze(template, opts.partials, true));
2632
+ }
2633
+ /**
2634
+ * A stack to manage scopes while traversing templates during static analysis.
2635
+ */
2636
+ class DummyScope {
2637
+ constructor(globals) {
2638
+ this.stack = [{ names: globals, aliases: new Map() }];
2639
+ }
2640
+ /** Return true if `name` is in scope. */
2641
+ has(name) {
2642
+ for (const scope of this.stack) {
2643
+ if (scope.names.has(name)) {
2644
+ return true;
2645
+ }
2646
+ }
2647
+ return false;
2648
+ }
2649
+ push(scope) {
2650
+ this.stack.push({ names: scope, aliases: new Map() });
2651
+ return this;
2652
+ }
2653
+ pop() {
2654
+ var _a;
2655
+ return (_a = this.stack.pop()) === null || _a === void 0 ? void 0 : _a.names;
2656
+ }
2657
+ // Add a name to the template scope.
2658
+ add(name) {
2659
+ this.stack[0].names.add(name);
2660
+ }
2661
+ /** Return the variable that `variable` aliases, or `variable` if it doesn't alias anything. */
2662
+ alias(variable) {
2663
+ const root = variable.segments[0];
2664
+ if (!isString(root))
2665
+ return undefined;
2666
+ const alias = this.getAlias(root);
2667
+ if (alias === undefined)
2668
+ return undefined;
2669
+ return new Variable([...alias, ...variable.segments.slice(1)], variable.location);
2670
+ }
2671
+ // TODO: `from` could be a path with multiple segments, like `include.x`.
2672
+ setAlias(from, to) {
2673
+ this.stack[this.stack.length - 1].aliases.set(from, to);
2674
+ }
2675
+ deleteAlias(name) {
2676
+ this.stack[this.stack.length - 1].aliases.delete(name);
2677
+ }
2678
+ getAlias(name) {
2679
+ for (const scope of this.stack) {
2680
+ if (scope.aliases.has(name)) {
2681
+ return scope.aliases.get(name);
2682
+ }
2683
+ // If a scope has defined `name`, then it masks aliases in parent scopes.
2684
+ if (scope.names.has(name)) {
2685
+ return undefined;
2686
+ }
2687
+ }
2688
+ return undefined;
2689
+ }
2690
+ }
2691
+ function* extractVariables(value) {
2692
+ if (isValueToken(value)) {
2693
+ yield* extractValueTokenVariables(value);
2694
+ }
2695
+ else if (value instanceof Value) {
2696
+ yield* extractFilteredValueVariables(value);
2697
+ }
2698
+ }
2699
+ function* extractFilteredValueVariables(value) {
2700
+ for (const token of value.initial.postfix) {
2701
+ if (isValueToken(token)) {
2702
+ yield* extractValueTokenVariables(token);
2703
+ }
2704
+ }
2705
+ for (const filter of value.filters) {
2706
+ for (const arg of filter.args) {
2707
+ if (isKeyValuePair(arg) && arg[1]) {
2708
+ yield* extractValueTokenVariables(arg[1]);
2709
+ }
2710
+ else if (isValueToken(arg)) {
2711
+ yield* extractValueTokenVariables(arg);
2712
+ }
2713
+ }
2714
+ }
2715
+ }
2716
+ function* extractValueTokenVariables(token) {
2717
+ if (isRangeToken(token)) {
2718
+ yield* extractValueTokenVariables(token.lhs);
2719
+ yield* extractValueTokenVariables(token.rhs);
2720
+ }
2721
+ else if (isPropertyAccessToken(token)) {
2722
+ yield extractPropertyAccessVariable(token);
2723
+ }
2724
+ }
2725
+ function extractPropertyAccessVariable(token) {
2726
+ const segments = [];
2727
+ // token is not guaranteed to have `file` set. We'll try to get it from a prop if not.
2728
+ let file = token.file;
2729
+ // Here we're flattening the first segment of a path if it is a nested path.
2730
+ const root = token.props[0];
2731
+ file = file || root.file;
2732
+ if (isQuotedToken(root) || isNumberToken(root) || isWordToken(root)) {
2733
+ segments.push(root.content);
2734
+ }
2735
+ else if (isPropertyAccessToken(root)) {
2736
+ // Flatten paths that start with a nested path.
2737
+ segments.push(...extractPropertyAccessVariable(root).segments);
2738
+ }
2739
+ for (const prop of token.props.slice(1)) {
2740
+ file = file || prop.file;
2741
+ if (isQuotedToken(prop) || isNumberToken(prop) || isWordToken(prop)) {
2742
+ segments.push(prop.content);
2743
+ }
2744
+ else if (isPropertyAccessToken(prop)) {
2745
+ segments.push(extractPropertyAccessVariable(prop));
2746
+ }
2747
+ }
2748
+ const [row, col] = token.getPosition();
2749
+ return new Variable(segments, {
2750
+ row,
2751
+ col,
2752
+ file
2753
+ });
2754
+ }
2755
+ // This is used to detect segments that can be represented with dot notation
2756
+ // when creating a string representation of VariableSegments.
2757
+ const RE_PROPERTY = /^[\u0080-\uFFFFa-zA-Z_][\u0080-\uFFFFa-zA-Z0-9_-]*$/;
2758
+ /**
2759
+ * Return a string representation of segments using dot notation where possible.
2760
+ * @param segments - The property names and array indices that make up a path to a variable.
2761
+ * @param bracketedRoot - If false (the default), don't surround the root segment with square brackets.
2762
+ */
2763
+ function segmentsString(segments, bracketedRoot = false) {
2764
+ const buf = [];
2765
+ const root = segments[0];
2766
+ if (isString(root)) {
2767
+ if (!bracketedRoot || root.match(RE_PROPERTY)) {
2768
+ buf.push(`${root}`);
2769
+ }
2770
+ else {
2771
+ buf.push(`['${root}']`);
2772
+ }
2773
+ }
2774
+ for (const segment of segments.slice(1)) {
2775
+ if (segment instanceof Variable) {
2776
+ buf.push(`[${segmentsString(segment.segments)}]`);
2777
+ }
2778
+ else if (isString(segment)) {
2779
+ if (segment.match(RE_PROPERTY)) {
2780
+ buf.push(`.${segment}`);
2781
+ }
2782
+ else {
2783
+ buf.push(`['${segment}']`);
2784
+ }
2785
+ }
2786
+ else {
2787
+ buf.push(`[${segment}]`);
2788
+ }
2789
+ }
2790
+ return buf.join('');
2791
+ }
2792
+
2289
2793
  var LookupType;
2290
2794
  (function (LookupType) {
2291
2795
  LookupType["Partials"] = "partials";
@@ -2302,7 +2806,7 @@ class Loader {
2302
2806
  this.shouldLoadRelative = (referencedFile) => rRelativePath.test(referencedFile);
2303
2807
  }
2304
2808
  else {
2305
- this.shouldLoadRelative = (referencedFile) => false;
2809
+ this.shouldLoadRelative = (_referencedFile) => false;
2306
2810
  }
2307
2811
  this.contains = this.options.fs.contains || (() => true);
2308
2812
  }
@@ -2359,8 +2863,11 @@ class Parser {
2359
2863
  this.fs = this.liquid.options.fs;
2360
2864
  this.parseFile = this.cache ? this._parseFileCached : this._parseFile;
2361
2865
  this.loader = new Loader(this.liquid.options);
2866
+ this.parseLimit = new Limiter('parse length', liquid.options.parseLimit);
2362
2867
  }
2363
2868
  parse(html, filepath) {
2869
+ html = String(html);
2870
+ this.parseLimit.use(html.length);
2364
2871
  const tokenizer = new Tokenizer(html, this.liquid.options.operators, filepath);
2365
2872
  const tokens = tokenizer.readTopLevelTokens(this.liquid.options);
2366
2873
  return this.parseTokens(tokens);
@@ -2368,9 +2875,20 @@ class Parser {
2368
2875
  parseTokens(tokens) {
2369
2876
  let token;
2370
2877
  const templates = [];
2878
+ const errors = [];
2371
2879
  while ((token = tokens.shift())) {
2372
- templates.push(this.parseToken(token, tokens));
2880
+ try {
2881
+ templates.push(this.parseToken(token, tokens));
2882
+ }
2883
+ catch (err) {
2884
+ if (this.liquid.options.catchAllErrors)
2885
+ errors.push(err);
2886
+ else
2887
+ throw err;
2888
+ }
2373
2889
  }
2890
+ if (errors.length)
2891
+ throw new LiquidErrors(errors);
2374
2892
  return templates;
2375
2893
  }
2376
2894
  parseToken(token, remainTokens) {
@@ -2378,7 +2896,7 @@ class Parser {
2378
2896
  if (isTagToken(token)) {
2379
2897
  const TagClass = this.liquid.tags[token.name];
2380
2898
  assert(TagClass, `tag "${token.name}" not found`);
2381
- return new TagClass(token, remainTokens, this.liquid);
2899
+ return new TagClass(token, remainTokens, this.liquid, this);
2382
2900
  }
2383
2901
  if (isOutputToken(token)) {
2384
2902
  return new Output(token, this.liquid);
@@ -2416,7 +2934,7 @@ class Parser {
2416
2934
  }
2417
2935
  *_parseFile(file, sync, type = LookupType.Root, currentFile) {
2418
2936
  const filepath = yield this.loader.lookup(file, type, sync, currentFile);
2419
- return this.liquid.parse(sync ? this.fs.readFileSync(filepath) : yield this.fs.readFile(filepath), filepath);
2937
+ return this.parse(sync ? this.fs.readFileSync(filepath) : yield this.fs.readFile(filepath), filepath);
2420
2938
  }
2421
2939
  }
2422
2940
 
@@ -2471,6 +2989,10 @@ function isWordToken(val) {
2471
2989
  function isRangeToken(val) {
2472
2990
  return getKind(val) === TokenKind.Range;
2473
2991
  }
2992
+ function isValueToken(val) {
2993
+ // valueTokenBitMask = TokenKind.Number | TokenKind.Literal | TokenKind.Quoted | TokenKind.PropertyAccess | TokenKind.Range
2994
+ return (getKind(val) & 1667) > 0;
2995
+ }
2474
2996
  function getKind(val) {
2475
2997
  return val ? val.kind : -1;
2476
2998
  }
@@ -2487,24 +3009,29 @@ var typeGuards = /*#__PURE__*/Object.freeze({
2487
3009
  isNumberToken: isNumberToken,
2488
3010
  isPropertyAccessToken: isPropertyAccessToken,
2489
3011
  isWordToken: isWordToken,
2490
- isRangeToken: isRangeToken
3012
+ isRangeToken: isRangeToken,
3013
+ isValueToken: isValueToken
2491
3014
  });
2492
3015
 
2493
3016
  class Context {
2494
- constructor(env = {}, opts = defaultOptions, renderOptions = {}) {
2495
- var _a, _b, _c;
3017
+ constructor(env = {}, opts = defaultOptions, renderOptions = {}, { memoryLimit, renderLimit } = {}) {
3018
+ var _a, _b, _c, _d, _e;
2496
3019
  /**
2497
3020
  * insert a Context-level empty scope,
2498
3021
  * for tags like `{% capture %}` `{% assign %}` to operate
2499
3022
  */
2500
3023
  this.scopes = [{}];
2501
3024
  this.registers = {};
3025
+ this.breakCalled = false;
3026
+ this.continueCalled = false;
2502
3027
  this.sync = !!renderOptions.sync;
2503
3028
  this.opts = opts;
2504
3029
  this.globals = (_a = renderOptions.globals) !== null && _a !== void 0 ? _a : opts.globals;
2505
3030
  this.environments = isObject(env) ? env : Object(env);
2506
3031
  this.strictVariables = (_b = renderOptions.strictVariables) !== null && _b !== void 0 ? _b : this.opts.strictVariables;
2507
3032
  this.ownPropertyOnly = (_c = renderOptions.ownPropertyOnly) !== null && _c !== void 0 ? _c : opts.ownPropertyOnly;
3033
+ this.memoryLimit = memoryLimit !== null && memoryLimit !== void 0 ? memoryLimit : new Limiter('memory alloc', (_d = renderOptions.memoryLimit) !== null && _d !== void 0 ? _d : opts.memoryLimit);
3034
+ this.renderLimit = renderLimit !== null && renderLimit !== void 0 ? renderLimit : new Limiter('template render', getPerformance().now() + ((_e = renderOptions.renderLimit) !== null && _e !== void 0 ? _e : opts.renderLimit));
2508
3035
  }
2509
3036
  getRegister(key) {
2510
3037
  return (this.registers[key] = this.registers[key] || {});
@@ -2532,7 +3059,7 @@ class Context {
2532
3059
  return toValueSync(this._get(paths));
2533
3060
  }
2534
3061
  *_get(paths) {
2535
- const scope = this.findScope(paths[0]);
3062
+ const scope = this.findScope(paths[0]); // first prop should always be a string
2536
3063
  return yield this._getFromScope(scope, paths);
2537
3064
  }
2538
3065
  /**
@@ -2561,6 +3088,16 @@ class Context {
2561
3088
  bottom() {
2562
3089
  return this.scopes[0];
2563
3090
  }
3091
+ spawn(scope = {}) {
3092
+ return new Context(scope, this.opts, {
3093
+ sync: this.sync,
3094
+ globals: this.globals,
3095
+ strictVariables: this.strictVariables
3096
+ }, {
3097
+ renderLimit: this.renderLimit,
3098
+ memoryLimit: this.memoryLimit
3099
+ });
3100
+ }
2564
3101
  findScope(key) {
2565
3102
  for (let i = this.scopes.length - 1; i >= 0; i--) {
2566
3103
  const candidate = this.scopes[i];
@@ -2574,6 +3111,7 @@ class Context {
2574
3111
  }
2575
3112
  function readProperty(obj, key, ownPropertyOnly) {
2576
3113
  obj = toLiquid(obj);
3114
+ key = toValue(key);
2577
3115
  if (isNil(obj))
2578
3116
  return obj;
2579
3117
  if (isArray(obj) && key < 0)
@@ -2592,7 +3130,7 @@ function readProperty(obj, key, ownPropertyOnly) {
2592
3130
  return value;
2593
3131
  }
2594
3132
  function readJSProperty(obj, key, ownPropertyOnly) {
2595
- if (ownPropertyOnly && !Object.hasOwnProperty.call(obj, key) && !(obj instanceof Drop))
3133
+ if (ownPropertyOnly && !hasOwnProperty.call(obj, key) && !(obj instanceof Drop))
2596
3134
  return undefined;
2597
3135
  return obj[key];
2598
3136
  }
@@ -2607,7 +3145,7 @@ function readLast(obj) {
2607
3145
  return obj['last'];
2608
3146
  }
2609
3147
  function readSize(obj) {
2610
- if (obj.hasOwnProperty('size') || obj['size'] !== undefined)
3148
+ if (hasOwnProperty.call(obj, 'size') || obj['size'] !== undefined)
2611
3149
  return obj['size'];
2612
3150
  if (isArray(obj) || isString(obj))
2613
3151
  return obj.length;
@@ -2714,13 +3252,25 @@ var urlFilters = /*#__PURE__*/Object.freeze({
2714
3252
  slugify: slugify
2715
3253
  });
2716
3254
 
2717
- const join = argumentsToValue((v, arg) => toArray(v).join(arg === undefined ? ' ' : arg));
2718
- const last$1 = argumentsToValue((v) => isArray(v) ? last(v) : '');
2719
- const first = argumentsToValue((v) => isArray(v) ? v[0] : '');
2720
- const reverse = argumentsToValue((v) => [...toArray(v)].reverse());
3255
+ const join = argumentsToValue(function (v, arg) {
3256
+ const array = toArray(v);
3257
+ const sep = isNil(arg) ? ' ' : stringify(arg);
3258
+ const complexity = array.length * (1 + sep.length);
3259
+ this.context.memoryLimit.use(complexity);
3260
+ return array.join(sep);
3261
+ });
3262
+ const last$1 = argumentsToValue((v) => isArrayLike(v) ? last(v) : '');
3263
+ const first = argumentsToValue((v) => isArrayLike(v) ? v[0] : '');
3264
+ const reverse = argumentsToValue(function (v) {
3265
+ const array = toArray(v);
3266
+ this.context.memoryLimit.use(array.length);
3267
+ return [...array].reverse();
3268
+ });
2721
3269
  function* sort(arr, property) {
2722
3270
  const values = [];
2723
- for (const item of toArray(arr)) {
3271
+ const array = toArray(arr);
3272
+ this.context.memoryLimit.use(array.length);
3273
+ for (const item of array) {
2724
3274
  values.push([
2725
3275
  item,
2726
3276
  property ? yield this.context._getFromScope(item, stringify(property).split('.'), false) : item
@@ -2737,35 +3287,47 @@ function sort_natural(input, property) {
2737
3287
  const compare = property === undefined
2738
3288
  ? caseInsensitiveCompare
2739
3289
  : (lhs, rhs) => caseInsensitiveCompare(lhs[propertyString], rhs[propertyString]);
2740
- return [...toArray(input)].sort(compare);
3290
+ const array = toArray(input);
3291
+ this.context.memoryLimit.use(array.length);
3292
+ return [...array].sort(compare);
2741
3293
  }
2742
3294
  const size = (v) => (v && v.length) || 0;
2743
3295
  function* map(arr, property) {
2744
3296
  const results = [];
2745
- for (const item of toArray(arr)) {
3297
+ const array = toArray(arr);
3298
+ this.context.memoryLimit.use(array.length);
3299
+ for (const item of array) {
2746
3300
  results.push(yield this.context._getFromScope(item, stringify(property), false));
2747
3301
  }
2748
3302
  return results;
2749
3303
  }
2750
3304
  function* sum(arr, property) {
2751
3305
  let sum = 0;
2752
- for (const item of toArray(arr)) {
3306
+ const array = toArray(arr);
3307
+ for (const item of array) {
2753
3308
  const data = Number(property ? yield this.context._getFromScope(item, stringify(property), false) : item);
2754
3309
  sum += Number.isNaN(data) ? 0 : data;
2755
3310
  }
2756
3311
  return sum;
2757
3312
  }
2758
3313
  function compact(arr) {
2759
- return toArray(arr).filter(x => !isNil(toValue(x)));
3314
+ const array = toArray(arr);
3315
+ this.context.memoryLimit.use(array.length);
3316
+ return array.filter(x => !isNil(toValue(x)));
2760
3317
  }
2761
3318
  function concat(v, arg = []) {
2762
- return toArray(v).concat(toArray(arg));
3319
+ const lhs = toArray(v);
3320
+ const rhs = toArray(arg);
3321
+ this.context.memoryLimit.use(lhs.length + rhs.length);
3322
+ return lhs.concat(rhs);
2763
3323
  }
2764
3324
  function push(v, arg) {
2765
- return concat(v, [arg]);
3325
+ return concat.call(this, v, [arg]);
2766
3326
  }
2767
3327
  function unshift(v, arg) {
2768
- const clone = [...toArray(v)];
3328
+ const array = toArray(v);
3329
+ this.context.memoryLimit.use(array.length);
3330
+ const clone = [...array];
2769
3331
  clone.unshift(arg);
2770
3332
  return clone;
2771
3333
  }
@@ -2775,7 +3337,9 @@ function pop(v) {
2775
3337
  return clone;
2776
3338
  }
2777
3339
  function shift(v) {
2778
- const clone = [...toArray(v)];
3340
+ const array = toArray(v);
3341
+ this.context.memoryLimit.use(array.length);
3342
+ const clone = [...array];
2779
3343
  clone.shift();
2780
3344
  return clone;
2781
3345
  }
@@ -2786,26 +3350,33 @@ function slice(v, begin, length = 1) {
2786
3350
  if (!isArray(v))
2787
3351
  v = stringify(v);
2788
3352
  begin = begin < 0 ? v.length + begin : begin;
3353
+ this.context.memoryLimit.use(length);
2789
3354
  return v.slice(begin, begin + length);
2790
3355
  }
2791
3356
  function* where(arr, property, expected) {
2792
3357
  const values = [];
2793
3358
  arr = toArray(arr);
3359
+ this.context.memoryLimit.use(arr.length);
2794
3360
  const token = new Tokenizer(stringify(property)).readScopeValue();
2795
3361
  for (const item of arr) {
2796
- values.push(yield evalToken(token, new Context(item)));
3362
+ values.push(yield evalToken(token, this.context.spawn(item)));
2797
3363
  }
3364
+ const matcher = this.context.opts.jekyllWhere
3365
+ ? (v) => EmptyDrop.is(expected) ? equals(v, expected) : (isArray(v) ? arrayIncludes(v, expected) : equals(v, expected))
3366
+ : (v) => equals(v, expected);
2798
3367
  return arr.filter((_, i) => {
2799
3368
  if (expected === undefined)
2800
3369
  return isTruthy(values[i], this.context);
2801
- return equals(values[i], expected);
3370
+ return matcher(values[i]);
2802
3371
  });
2803
3372
  }
2804
3373
  function* where_exp(arr, itemName, exp) {
2805
3374
  const filtered = [];
2806
3375
  const keyTemplate = new Value(stringify(exp), this.liquid);
2807
- for (const item of toArray(arr)) {
2808
- const value = yield keyTemplate.value(new Context({ [itemName]: item }));
3376
+ const array = toArray(arr);
3377
+ this.context.memoryLimit.use(array.length);
3378
+ for (const item of array) {
3379
+ const value = yield keyTemplate.value(this.context.spawn({ [itemName]: item }));
2809
3380
  if (value)
2810
3381
  filtered.push(item);
2811
3382
  }
@@ -2813,10 +3384,11 @@ function* where_exp(arr, itemName, exp) {
2813
3384
  }
2814
3385
  function* group_by(arr, property) {
2815
3386
  const map = new Map();
2816
- arr = toArray(arr);
3387
+ arr = toEnumerable(arr);
2817
3388
  const token = new Tokenizer(stringify(property)).readScopeValue();
3389
+ this.context.memoryLimit.use(arr.length);
2818
3390
  for (const item of arr) {
2819
- const key = yield evalToken(token, new Context(item));
3391
+ const key = yield evalToken(token, this.context.spawn(item));
2820
3392
  if (!map.has(key))
2821
3393
  map.set(key, []);
2822
3394
  map.get(key).push(item);
@@ -2826,8 +3398,10 @@ function* group_by(arr, property) {
2826
3398
  function* group_by_exp(arr, itemName, exp) {
2827
3399
  const map = new Map();
2828
3400
  const keyTemplate = new Value(stringify(exp), this.liquid);
2829
- for (const item of toArray(arr)) {
2830
- const key = yield keyTemplate.value(new Context({ [itemName]: item }));
3401
+ arr = toEnumerable(arr);
3402
+ this.context.memoryLimit.use(arr.length);
3403
+ for (const item of arr) {
3404
+ const key = yield keyTemplate.value(this.context.spawn({ [itemName]: item }));
2831
3405
  if (!map.has(key))
2832
3406
  map.set(key, []);
2833
3407
  map.get(key).push(item);
@@ -2836,31 +3410,26 @@ function* group_by_exp(arr, itemName, exp) {
2836
3410
  }
2837
3411
  function* find(arr, property, expected) {
2838
3412
  const token = new Tokenizer(stringify(property)).readScopeValue();
2839
- for (const item of toArray(arr)) {
2840
- const value = yield evalToken(token, new Context(item));
3413
+ const array = toArray(arr);
3414
+ for (const item of array) {
3415
+ const value = yield evalToken(token, this.context.spawn(item));
2841
3416
  if (equals(value, expected))
2842
3417
  return item;
2843
3418
  }
2844
- return null;
2845
3419
  }
2846
3420
  function* find_exp(arr, itemName, exp) {
2847
3421
  const predicate = new Value(stringify(exp), this.liquid);
2848
- for (const item of toArray(arr)) {
2849
- const value = yield predicate.value(new Context({ [itemName]: item }));
3422
+ const array = toArray(arr);
3423
+ for (const item of array) {
3424
+ const value = yield predicate.value(this.context.spawn({ [itemName]: item }));
2850
3425
  if (value)
2851
3426
  return item;
2852
3427
  }
2853
- return null;
2854
3428
  }
2855
3429
  function uniq(arr) {
2856
- arr = toValue(arr);
2857
- const u = {};
2858
- return (arr || []).filter(val => {
2859
- if (hasOwnProperty.call(u, String(val)))
2860
- return false;
2861
- u[String(val)] = true;
2862
- return true;
2863
- });
3430
+ arr = toArray(arr);
3431
+ this.context.memoryLimit.use(arr.length);
3432
+ return [...new Set(arr)];
2864
3433
  }
2865
3434
  function sample(v, count = 1) {
2866
3435
  v = toValue(v);
@@ -2868,6 +3437,7 @@ function sample(v, count = 1) {
2868
3437
  return [];
2869
3438
  if (!isArray(v))
2870
3439
  v = stringify(v);
3440
+ this.context.memoryLimit.use(count);
2871
3441
  const shuffled = [...v].sort(() => Math.random() - 0.5);
2872
3442
  if (count === 1)
2873
3443
  return shuffled[0];
@@ -2903,6 +3473,9 @@ var arrayFilters = /*#__PURE__*/Object.freeze({
2903
3473
  });
2904
3474
 
2905
3475
  function date(v, format, timezoneOffset) {
3476
+ var _a, _b, _c;
3477
+ const size = ((_a = v === null || v === void 0 ? void 0 : v.length) !== null && _a !== void 0 ? _a : 0) + ((_b = format === null || format === void 0 ? void 0 : format.length) !== null && _b !== void 0 ? _b : 0) + ((_c = timezoneOffset === null || timezoneOffset === void 0 ? void 0 : timezoneOffset.length) !== null && _c !== void 0 ? _c : 0);
3478
+ this.context.memoryLimit.use(size);
2906
3479
  const date = parseDate(v, this.context.opts, timezoneOffset);
2907
3480
  if (!date)
2908
3481
  return v;
@@ -2936,39 +3509,30 @@ function stringify_date(v, month_type, type, style) {
2936
3509
  }
2937
3510
  function parseDate(v, opts, timezoneOffset) {
2938
3511
  let date;
3512
+ const defaultTimezoneOffset = timezoneOffset !== null && timezoneOffset !== void 0 ? timezoneOffset : opts.timezoneOffset;
3513
+ const locale = opts.locale;
2939
3514
  v = toValue(v);
2940
3515
  if (v === 'now' || v === 'today') {
2941
- date = new Date();
3516
+ date = new LiquidDate(Date.now(), locale, defaultTimezoneOffset);
2942
3517
  }
2943
3518
  else if (isNumber(v)) {
2944
- date = new Date(v * 1000);
3519
+ date = new LiquidDate(v * 1000, locale, defaultTimezoneOffset);
2945
3520
  }
2946
3521
  else if (isString(v)) {
2947
3522
  if (/^\d+$/.test(v)) {
2948
- date = new Date(+v * 1000);
3523
+ date = new LiquidDate(+v * 1000, locale, defaultTimezoneOffset);
2949
3524
  }
2950
- else if (opts.preserveTimezones) {
2951
- date = TimezoneDate.createDateFixedToTimezone(v);
3525
+ else if (opts.preserveTimezones && timezoneOffset === undefined) {
3526
+ date = LiquidDate.createDateFixedToTimezone(v, locale);
2952
3527
  }
2953
3528
  else {
2954
- date = new Date(v);
3529
+ date = new LiquidDate(v, locale, defaultTimezoneOffset);
2955
3530
  }
2956
3531
  }
2957
3532
  else {
2958
- date = v;
2959
- }
2960
- if (!isValidDate(date))
2961
- return;
2962
- if (timezoneOffset !== undefined) {
2963
- date = new TimezoneDate(date, timezoneOffset);
2964
- }
2965
- else if (!(date instanceof TimezoneDate) && opts.timezoneOffset !== undefined) {
2966
- date = new TimezoneDate(date, opts.timezoneOffset);
3533
+ date = new LiquidDate(v, locale, defaultTimezoneOffset);
2967
3534
  }
2968
- return date;
2969
- }
2970
- function isValidDate(date) {
2971
- return (date instanceof Date || date instanceof TimezoneDate) && !isNaN(date.getTime());
3535
+ return date.valid() ? date : undefined;
2972
3536
  }
2973
3537
 
2974
3538
  var dateFilters = /*#__PURE__*/Object.freeze({
@@ -2990,33 +3554,50 @@ const rCJKWord = /[\u4E00-\u9FFF\uF900-\uFAFF\u3400-\u4DBF\u3040-\u309F\u30A0-\u
2990
3554
  const rNonCJKWord = /[^\u4E00-\u9FFF\uF900-\uFAFF\u3400-\u4DBF\u3040-\u309F\u30A0-\u30FF\uAC00-\uD7AF\s]+/gu;
2991
3555
  function append(v, arg) {
2992
3556
  assert(arguments.length === 2, 'append expect 2 arguments');
2993
- return stringify(v) + stringify(arg);
3557
+ const lhs = stringify(v);
3558
+ const rhs = stringify(arg);
3559
+ this.context.memoryLimit.use(lhs.length + rhs.length);
3560
+ return lhs + rhs;
2994
3561
  }
2995
3562
  function prepend(v, arg) {
2996
3563
  assert(arguments.length === 2, 'prepend expect 2 arguments');
2997
- return stringify(arg) + stringify(v);
3564
+ const lhs = stringify(v);
3565
+ const rhs = stringify(arg);
3566
+ this.context.memoryLimit.use(lhs.length + rhs.length);
3567
+ return rhs + lhs;
2998
3568
  }
2999
3569
  function lstrip(v, chars) {
3570
+ const str = stringify(v);
3571
+ this.context.memoryLimit.use(str.length);
3000
3572
  if (chars) {
3001
3573
  chars = escapeRegExp(stringify(chars));
3002
- return stringify(v).replace(new RegExp(`^[${chars}]+`, 'g'), '');
3574
+ return str.replace(new RegExp(`^[${chars}]+`, 'g'), '');
3003
3575
  }
3004
- return stringify(v).replace(/^\s+/, '');
3576
+ return str.replace(/^\s+/, '');
3005
3577
  }
3006
3578
  function downcase(v) {
3007
- return stringify(v).toLowerCase();
3579
+ const str = stringify(v);
3580
+ this.context.memoryLimit.use(str.length);
3581
+ return str.toLowerCase();
3008
3582
  }
3009
- function upcase(str) {
3583
+ function upcase(v) {
3584
+ const str = stringify(v);
3585
+ this.context.memoryLimit.use(str.length);
3010
3586
  return stringify(str).toUpperCase();
3011
3587
  }
3012
3588
  function remove(v, arg) {
3013
- return stringify(v).split(stringify(arg)).join('');
3589
+ const str = stringify(v);
3590
+ this.context.memoryLimit.use(str.length);
3591
+ return str.split(stringify(arg)).join('');
3014
3592
  }
3015
3593
  function remove_first(v, l) {
3016
- return stringify(v).replace(stringify(l), '');
3594
+ const str = stringify(v);
3595
+ this.context.memoryLimit.use(str.length);
3596
+ return str.replace(stringify(l), '');
3017
3597
  }
3018
3598
  function remove_last(v, l) {
3019
3599
  const str = stringify(v);
3600
+ this.context.memoryLimit.use(str.length);
3020
3601
  const pattern = stringify(l);
3021
3602
  const index = str.lastIndexOf(pattern);
3022
3603
  if (index === -1)
@@ -3024,14 +3605,18 @@ function remove_last(v, l) {
3024
3605
  return str.substring(0, index) + str.substring(index + pattern.length);
3025
3606
  }
3026
3607
  function rstrip(str, chars) {
3608
+ str = stringify(str);
3609
+ this.context.memoryLimit.use(str.length);
3027
3610
  if (chars) {
3028
3611
  chars = escapeRegExp(stringify(chars));
3029
- return stringify(str).replace(new RegExp(`[${chars}]+$`, 'g'), '');
3612
+ return str.replace(new RegExp(`[${chars}]+$`, 'g'), '');
3030
3613
  }
3031
- return stringify(str).replace(/\s+$/, '');
3614
+ return str.replace(/\s+$/, '');
3032
3615
  }
3033
3616
  function split(v, arg) {
3034
- const arr = stringify(v).split(stringify(arg));
3617
+ const str = stringify(v);
3618
+ this.context.memoryLimit.use(str.length);
3619
+ const arr = str.split(stringify(arg));
3035
3620
  // align to ruby split, which is the behavior of shopify/liquid
3036
3621
  // see: https://ruby-doc.org/core-2.4.0/String.html#method-i-split
3037
3622
  while (arr.length && arr[arr.length - 1] === '')
@@ -3039,29 +3624,39 @@ function split(v, arg) {
3039
3624
  return arr;
3040
3625
  }
3041
3626
  function strip(v, chars) {
3627
+ const str = stringify(v);
3628
+ this.context.memoryLimit.use(str.length);
3042
3629
  if (chars) {
3043
3630
  chars = escapeRegExp(stringify(chars));
3044
- return stringify(v)
3631
+ return str
3045
3632
  .replace(new RegExp(`^[${chars}]+`, 'g'), '')
3046
3633
  .replace(new RegExp(`[${chars}]+$`, 'g'), '');
3047
3634
  }
3048
- return stringify(v).trim();
3635
+ return str.trim();
3049
3636
  }
3050
3637
  function strip_newlines(v) {
3051
- return stringify(v).replace(/\r?\n/gm, '');
3638
+ const str = stringify(v);
3639
+ this.context.memoryLimit.use(str.length);
3640
+ return str.replace(/\r?\n/gm, '');
3052
3641
  }
3053
3642
  function capitalize(str) {
3054
3643
  str = stringify(str);
3644
+ this.context.memoryLimit.use(str.length);
3055
3645
  return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
3056
3646
  }
3057
3647
  function replace(v, pattern, replacement) {
3058
- return stringify(v).split(stringify(pattern)).join(replacement);
3648
+ const str = stringify(v);
3649
+ this.context.memoryLimit.use(str.length);
3650
+ return str.split(stringify(pattern)).join(replacement);
3059
3651
  }
3060
3652
  function replace_first(v, arg1, arg2) {
3061
- return stringify(v).replace(stringify(arg1), arg2);
3653
+ const str = stringify(v);
3654
+ this.context.memoryLimit.use(str.length);
3655
+ return str.replace(stringify(arg1), arg2);
3062
3656
  }
3063
3657
  function replace_last(v, arg1, arg2) {
3064
3658
  const str = stringify(v);
3659
+ this.context.memoryLimit.use(str.length);
3065
3660
  const pattern = stringify(arg1);
3066
3661
  const index = str.lastIndexOf(pattern);
3067
3662
  if (index === -1)
@@ -3070,13 +3665,16 @@ function replace_last(v, arg1, arg2) {
3070
3665
  return str.substring(0, index) + replacement + str.substring(index + pattern.length);
3071
3666
  }
3072
3667
  function truncate(v, l = 50, o = '...') {
3073
- v = stringify(v);
3074
- if (v.length <= l)
3668
+ const str = stringify(v);
3669
+ this.context.memoryLimit.use(str.length);
3670
+ if (str.length <= l)
3075
3671
  return v;
3076
- return v.substring(0, l - o.length) + o;
3672
+ return str.substring(0, l - o.length) + o;
3077
3673
  }
3078
3674
  function truncatewords(v, words = 15, o = '...') {
3079
- const arr = stringify(v).split(/\s+/);
3675
+ const str = stringify(v);
3676
+ this.context.memoryLimit.use(str.length);
3677
+ const arr = str.split(/\s+/);
3080
3678
  if (words <= 0)
3081
3679
  words = 1;
3082
3680
  let ret = arr.slice(0, words).join(' ');
@@ -3085,11 +3683,14 @@ function truncatewords(v, words = 15, o = '...') {
3085
3683
  return ret;
3086
3684
  }
3087
3685
  function normalize_whitespace(v) {
3088
- v = stringify(v);
3089
- return v.replace(/\s+/g, ' ');
3686
+ const str = stringify(v);
3687
+ this.context.memoryLimit.use(str.length);
3688
+ return str.replace(/\s+/g, ' ');
3090
3689
  }
3091
3690
  function number_of_words(input, mode) {
3092
- input = stringify(input).trim();
3691
+ const str = stringify(input);
3692
+ this.context.memoryLimit.use(str.length);
3693
+ input = str.trim();
3093
3694
  if (!input)
3094
3695
  return 0;
3095
3696
  switch (mode) {
@@ -3107,6 +3708,7 @@ function number_of_words(input, mode) {
3107
3708
  }
3108
3709
  }
3109
3710
  function array_to_sentence_string(array, connector = 'and') {
3711
+ this.context.memoryLimit.use(array.length);
3110
3712
  switch (array.length) {
3111
3713
  case 0:
3112
3714
  return '';
@@ -3149,7 +3751,8 @@ const filters = Object.assign(Object.assign(Object.assign(Object.assign(Object.a
3149
3751
  class AssignTag extends Tag {
3150
3752
  constructor(token, remainTokens, liquid) {
3151
3753
  super(token, remainTokens, liquid);
3152
- this.key = this.tokenizer.readIdentifier().content;
3754
+ this.identifier = this.tokenizer.readIdentifier();
3755
+ this.key = this.identifier.content;
3153
3756
  this.tokenizer.assert(this.key, 'expected variable name');
3154
3757
  this.tokenizer.skipBlank();
3155
3758
  this.tokenizer.assert(this.tokenizer.peek() === '=', 'expected "="');
@@ -3159,11 +3762,17 @@ class AssignTag extends Tag {
3159
3762
  *render(ctx) {
3160
3763
  ctx.bottom()[this.key] = yield this.value.value(ctx, this.liquid.options.lenientIf);
3161
3764
  }
3765
+ *arguments() {
3766
+ yield this.value;
3767
+ }
3768
+ *localScope() {
3769
+ yield this.identifier;
3770
+ }
3162
3771
  }
3163
3772
 
3164
3773
  const MODIFIERS = ['offset', 'limit', 'reversed'];
3165
3774
  class ForTag extends Tag {
3166
- constructor(token, remainTokens, liquid) {
3775
+ constructor(token, remainTokens, liquid, parser) {
3167
3776
  super(token, remainTokens, liquid);
3168
3777
  const variable = this.tokenizer.readIdentifier();
3169
3778
  const inStr = this.tokenizer.readIdentifier();
@@ -3173,18 +3782,16 @@ class ForTag extends Tag {
3173
3782
  }
3174
3783
  this.variable = variable.content;
3175
3784
  this.collection = collection;
3176
- this.hash = new Hash(this.tokenizer.remaining());
3785
+ this.hash = new Hash(this.tokenizer, liquid.options.keyValueSeparator);
3177
3786
  this.templates = [];
3178
3787
  this.elseTemplates = [];
3179
3788
  let p;
3180
- const stream = this.liquid.parser.parseStream(remainTokens)
3789
+ const stream = parser.parseStream(remainTokens)
3181
3790
  .on('start', () => (p = this.templates))
3182
- .on('tag:else', () => (p = this.elseTemplates))
3183
- .on('tag:endfor', () => stream.stop())
3791
+ .on('tag:else', tag => { assertEmpty(tag.args); p = this.elseTemplates; })
3792
+ .on('tag:endfor', tag => { assertEmpty(tag.args); stream.stop(); })
3184
3793
  .on('template', (tpl) => p.push(tpl))
3185
- .on('end', () => {
3186
- throw new Error(`tag ${token.getText()} not closed`);
3187
- });
3794
+ .on('end', () => { throw new Error(`tag ${token.getText()} not closed`); });
3188
3795
  stream.start();
3189
3796
  }
3190
3797
  *render(ctx, emitter) {
@@ -3213,16 +3820,33 @@ class ForTag extends Tag {
3213
3820
  ctx.push(scope);
3214
3821
  for (const item of collection) {
3215
3822
  scope[this.variable] = item;
3823
+ ctx.continueCalled = ctx.breakCalled = false;
3216
3824
  yield r.renderTemplates(this.templates, ctx, emitter);
3217
- if (emitter['break']) {
3218
- emitter['break'] = false;
3825
+ if (ctx.breakCalled)
3219
3826
  break;
3220
- }
3221
- emitter['continue'] = false;
3222
3827
  scope.forloop.next();
3223
3828
  }
3829
+ ctx.continueCalled = ctx.breakCalled = false;
3224
3830
  ctx.pop();
3225
3831
  }
3832
+ *children() {
3833
+ const templates = this.templates.slice();
3834
+ if (this.elseTemplates) {
3835
+ templates.push(...this.elseTemplates);
3836
+ }
3837
+ return templates;
3838
+ }
3839
+ *arguments() {
3840
+ yield this.collection;
3841
+ for (const v of Object.values(this.hash.hash)) {
3842
+ if (isValueToken(v)) {
3843
+ yield v;
3844
+ }
3845
+ }
3846
+ }
3847
+ blockScope() {
3848
+ return [this.variable, 'forloop'];
3849
+ }
3226
3850
  }
3227
3851
  function reversed(arr) {
3228
3852
  return [...arr].reverse();
@@ -3235,36 +3859,43 @@ function limit(arr, count) {
3235
3859
  }
3236
3860
 
3237
3861
  class CaptureTag extends Tag {
3238
- constructor(tagToken, remainTokens, liquid) {
3862
+ constructor(tagToken, remainTokens, liquid, parser) {
3239
3863
  super(tagToken, remainTokens, liquid);
3240
3864
  this.templates = [];
3241
- this.variable = this.readVariableName();
3865
+ this.identifier = this.readVariable();
3866
+ this.variable = this.identifier.content;
3242
3867
  while (remainTokens.length) {
3243
3868
  const token = remainTokens.shift();
3244
3869
  if (isTagToken(token) && token.name === 'endcapture')
3245
3870
  return;
3246
- this.templates.push(liquid.parser.parseToken(token, remainTokens));
3871
+ this.templates.push(parser.parseToken(token, remainTokens));
3247
3872
  }
3248
3873
  throw new Error(`tag ${tagToken.getText()} not closed`);
3249
3874
  }
3875
+ readVariable() {
3876
+ let ident = this.tokenizer.readIdentifier();
3877
+ if (ident.content)
3878
+ return ident;
3879
+ ident = this.tokenizer.readQuoted();
3880
+ if (ident)
3881
+ return ident;
3882
+ throw this.tokenizer.error('invalid capture name');
3883
+ }
3250
3884
  *render(ctx) {
3251
3885
  const r = this.liquid.renderer;
3252
3886
  const html = yield r.renderTemplates(this.templates, ctx);
3253
3887
  ctx.bottom()[this.variable] = html;
3254
3888
  }
3255
- readVariableName() {
3256
- const word = this.tokenizer.readIdentifier().content;
3257
- if (word)
3258
- return word;
3259
- const quoted = this.tokenizer.readQuoted();
3260
- if (quoted)
3261
- return evalQuotedToken(quoted);
3262
- throw this.tokenizer.error('invalid capture name');
3889
+ *children() {
3890
+ return this.templates;
3891
+ }
3892
+ *localScope() {
3893
+ yield this.identifier;
3263
3894
  }
3264
3895
  }
3265
3896
 
3266
3897
  class CaseTag extends Tag {
3267
- constructor(tagToken, remainTokens, liquid) {
3898
+ constructor(tagToken, remainTokens, liquid, parser) {
3268
3899
  super(tagToken, remainTokens, liquid);
3269
3900
  this.branches = [];
3270
3901
  this.elseTemplates = [];
@@ -3272,7 +3903,7 @@ class CaseTag extends Tag {
3272
3903
  this.elseTemplates = [];
3273
3904
  let p = [];
3274
3905
  let elseCount = 0;
3275
- const stream = this.liquid.parser.parseStream(remainTokens)
3906
+ const stream = parser.parseStream(remainTokens)
3276
3907
  .on('tag:when', (token) => {
3277
3908
  if (elseCount > 0) {
3278
3909
  return;
@@ -3327,6 +3958,17 @@ class CaseTag extends Tag {
3327
3958
  yield r.renderTemplates(this.elseTemplates, ctx, emitter);
3328
3959
  }
3329
3960
  }
3961
+ *arguments() {
3962
+ yield this.value;
3963
+ yield* this.branches.flatMap(b => b.values);
3964
+ }
3965
+ *children() {
3966
+ const templates = this.branches.flatMap(b => b.templates);
3967
+ if (this.elseTemplates) {
3968
+ templates.push(...this.elseTemplates);
3969
+ }
3970
+ return templates;
3971
+ }
3330
3972
  }
3331
3973
 
3332
3974
  class CommentTag extends Tag {
@@ -3343,10 +3985,10 @@ class CommentTag extends Tag {
3343
3985
  }
3344
3986
 
3345
3987
  class RenderTag extends Tag {
3346
- constructor(token, remainTokens, liquid) {
3988
+ constructor(token, remainTokens, liquid, parser) {
3347
3989
  super(token, remainTokens, liquid);
3348
3990
  const tokenizer = this.tokenizer;
3349
- this.file = parseFilePath(tokenizer, this.liquid);
3991
+ this.file = parseFilePath(tokenizer, this.liquid, parser);
3350
3992
  this.currentFile = token.file;
3351
3993
  while (!tokenizer.end()) {
3352
3994
  tokenizer.skipBlank();
@@ -3380,13 +4022,13 @@ class RenderTag extends Tag {
3380
4022
  tokenizer.p = begin;
3381
4023
  break;
3382
4024
  }
3383
- this.hash = new Hash(tokenizer.remaining());
4025
+ this.hash = new Hash(tokenizer, liquid.options.keyValueSeparator);
3384
4026
  }
3385
4027
  *render(ctx, emitter) {
3386
4028
  const { liquid, hash } = this;
3387
4029
  const filepath = (yield renderFilePath(this['file'], ctx, liquid));
3388
4030
  assert(filepath, () => `illegal file path "${filepath}"`);
3389
- const childCtx = new Context({}, ctx.opts, { sync: ctx.sync, globals: ctx.globals, strictVariables: ctx.strictVariables });
4031
+ const childCtx = ctx.spawn();
3390
4032
  const scope = childCtx.bottom();
3391
4033
  __assign(scope, yield hash.render(ctx));
3392
4034
  if (this['with']) {
@@ -3409,6 +4051,55 @@ class RenderTag extends Tag {
3409
4051
  yield liquid.renderer.renderTemplates(templates, childCtx, emitter);
3410
4052
  }
3411
4053
  }
4054
+ *children(partials, sync) {
4055
+ if (partials && isString(this['file'])) {
4056
+ return (yield this.liquid._parsePartialFile(this['file'], sync, this['currentFile']));
4057
+ }
4058
+ return [];
4059
+ }
4060
+ partialScope() {
4061
+ if (isString(this['file'])) {
4062
+ const names = Object.keys(this.hash.hash);
4063
+ if (this['with']) {
4064
+ const { value, alias } = this['with'];
4065
+ if (isString(alias)) {
4066
+ names.push([alias, value]);
4067
+ }
4068
+ else if (isString(this.file)) {
4069
+ names.push([this.file, value]);
4070
+ }
4071
+ }
4072
+ if (this['for']) {
4073
+ const { value, alias } = this['for'];
4074
+ if (isString(alias)) {
4075
+ names.push([alias, value]);
4076
+ }
4077
+ else if (isString(this.file)) {
4078
+ names.push([this.file, value]);
4079
+ }
4080
+ }
4081
+ return { name: this['file'], isolated: true, scope: names };
4082
+ }
4083
+ }
4084
+ *arguments() {
4085
+ for (const v of Object.values(this.hash.hash)) {
4086
+ if (isValueToken(v)) {
4087
+ yield v;
4088
+ }
4089
+ }
4090
+ if (this['with']) {
4091
+ const { value } = this['with'];
4092
+ if (isValueToken(value)) {
4093
+ yield value;
4094
+ }
4095
+ }
4096
+ if (this['for']) {
4097
+ const { value } = this['for'];
4098
+ if (isValueToken(value)) {
4099
+ yield value;
4100
+ }
4101
+ }
4102
+ }
3412
4103
  }
3413
4104
  /**
3414
4105
  * @return null for "none",
@@ -3416,7 +4107,7 @@ class RenderTag extends Tag {
3416
4107
  * @return Token for expression (not quoted)
3417
4108
  * @throws TypeError if cannot read next token
3418
4109
  */
3419
- function parseFilePath(tokenizer, liquid) {
4110
+ function parseFilePath(tokenizer, liquid, parser) {
3420
4111
  if (liquid.options.dynamicPartials) {
3421
4112
  const file = tokenizer.readValue();
3422
4113
  tokenizer.assert(file, 'illegal file path');
@@ -3424,13 +4115,13 @@ function parseFilePath(tokenizer, liquid) {
3424
4115
  return;
3425
4116
  if (isQuotedToken(file)) {
3426
4117
  // for filenames like "files/{{file}}", eval as liquid template
3427
- const templates = liquid.parse(evalQuotedToken(file));
4118
+ const templates = parser.parse(evalQuotedToken(file));
3428
4119
  return optimize(templates);
3429
4120
  }
3430
4121
  return file;
3431
4122
  }
3432
4123
  const tokens = [...tokenizer.readFileNameTemplate(liquid.options)];
3433
- const templates = optimize(liquid.parser.parseTokens(tokens));
4124
+ const templates = optimize(parser.parseTokens(tokens));
3434
4125
  return templates === 'none' ? undefined : templates;
3435
4126
  }
3436
4127
  function optimize(templates) {
@@ -3448,10 +4139,10 @@ function* renderFilePath(file, ctx, liquid) {
3448
4139
  }
3449
4140
 
3450
4141
  class IncludeTag extends Tag {
3451
- constructor(token, remainTokens, liquid) {
4142
+ constructor(token, remainTokens, liquid, parser) {
3452
4143
  super(token, remainTokens, liquid);
3453
4144
  const { tokenizer } = token;
3454
- this['file'] = parseFilePath(tokenizer, this.liquid);
4145
+ this['file'] = parseFilePath(tokenizer, this.liquid, parser);
3455
4146
  this['currentFile'] = token.file;
3456
4147
  const begin = tokenizer.p;
3457
4148
  const withStr = tokenizer.readIdentifier();
@@ -3465,7 +4156,7 @@ class IncludeTag extends Tag {
3465
4156
  }
3466
4157
  else
3467
4158
  tokenizer.p = begin;
3468
- this.hash = new Hash(tokenizer.remaining(), this.liquid.options.jekyllInclude);
4159
+ this.hash = new Hash(tokenizer, liquid.options.jekyllInclude || liquid.options.keyValueSeparator);
3469
4160
  }
3470
4161
  *render(ctx, emitter) {
3471
4162
  const { liquid, hash, withVar } = this;
@@ -3484,12 +4175,43 @@ class IncludeTag extends Tag {
3484
4175
  ctx.pop();
3485
4176
  ctx.restoreRegister(saved);
3486
4177
  }
4178
+ *children(partials, sync) {
4179
+ if (partials && isString(this['file'])) {
4180
+ return (yield this.liquid._parsePartialFile(this['file'], sync, this['currentFile']));
4181
+ }
4182
+ return [];
4183
+ }
4184
+ partialScope() {
4185
+ if (isString(this['file'])) {
4186
+ let names;
4187
+ if (this.liquid.options.jekyllInclude) {
4188
+ names = ['include'];
4189
+ }
4190
+ else {
4191
+ names = Object.keys(this.hash.hash);
4192
+ if (this.withVar) {
4193
+ names.push([this['file'], this.withVar]);
4194
+ }
4195
+ }
4196
+ return { name: this['file'], isolated: false, scope: names };
4197
+ }
4198
+ }
4199
+ *arguments() {
4200
+ yield* Object.values(this.hash.hash).filter(isValueToken);
4201
+ if (isValueToken(this['file'])) {
4202
+ yield this['file'];
4203
+ }
4204
+ if (isValueToken(this.withVar)) {
4205
+ yield this.withVar;
4206
+ }
4207
+ }
3487
4208
  }
3488
4209
 
3489
4210
  class DecrementTag extends Tag {
3490
4211
  constructor(token, remainTokens, liquid) {
3491
4212
  super(token, remainTokens, liquid);
3492
- this.variable = this.tokenizer.readIdentifier().content;
4213
+ this.identifier = this.tokenizer.readIdentifier();
4214
+ this.variable = this.identifier.content;
3493
4215
  }
3494
4216
  render(context, emitter) {
3495
4217
  const scope = context.environments;
@@ -3498,6 +4220,9 @@ class DecrementTag extends Tag {
3498
4220
  }
3499
4221
  emitter.write(stringify(--scope[this.variable]));
3500
4222
  }
4223
+ *localScope() {
4224
+ yield this.identifier;
4225
+ }
3501
4226
  }
3502
4227
 
3503
4228
  class CycleTag extends Tag {
@@ -3535,40 +4260,38 @@ class CycleTag extends Tag {
3535
4260
  groups[fingerprint] = idx;
3536
4261
  return yield evalToken(candidate, ctx);
3537
4262
  }
4263
+ *arguments() {
4264
+ yield* this.candidates;
4265
+ if (this.group) {
4266
+ yield this.group;
4267
+ }
4268
+ }
3538
4269
  }
3539
4270
 
3540
4271
  class IfTag extends Tag {
3541
- constructor(tagToken, remainTokens, liquid) {
4272
+ constructor(tagToken, remainTokens, liquid, parser) {
3542
4273
  super(tagToken, remainTokens, liquid);
3543
4274
  this.branches = [];
3544
- this.elseTemplates = [];
3545
4275
  let p = [];
3546
- let elseCount = 0;
3547
- liquid.parser.parseStream(remainTokens)
4276
+ parser.parseStream(remainTokens)
3548
4277
  .on('start', () => this.branches.push({
3549
- value: new Value(tagToken.args, this.liquid),
4278
+ value: new Value(tagToken.tokenizer.readFilteredValue(), this.liquid),
3550
4279
  templates: (p = [])
3551
4280
  }))
3552
4281
  .on('tag:elsif', (token) => {
3553
- if (elseCount > 0) {
3554
- p = [];
3555
- return;
3556
- }
4282
+ assert(!this.elseTemplates, 'unexpected elsif after else');
3557
4283
  this.branches.push({
3558
- value: new Value(token.args, this.liquid),
4284
+ value: new Value(token.tokenizer.readFilteredValue(), this.liquid),
3559
4285
  templates: (p = [])
3560
4286
  });
3561
4287
  })
3562
- .on('tag:else', () => {
3563
- elseCount++;
3564
- p = this.elseTemplates;
3565
- })
3566
- .on('tag:endif', function () { this.stop(); })
3567
- .on('template', (tpl) => {
3568
- if (p !== this.elseTemplates || elseCount === 1) {
3569
- p.push(tpl);
3570
- }
4288
+ .on('tag:else', tag => {
4289
+ assertEmpty(tag.args);
4290
+ assert(!this.elseTemplates, 'duplicated else');
4291
+ p = this.elseTemplates = [];
3571
4292
  })
4293
+ .on('tag:endif', function (tag) { assertEmpty(tag.args); this.stop(); })
4294
+ .on('template', (tpl) => p.push(tpl))
3572
4295
  .on('end', () => { throw new Error(`tag ${tagToken.getText()} not closed`); })
3573
4296
  .start();
3574
4297
  }
@@ -3581,14 +4304,25 @@ class IfTag extends Tag {
3581
4304
  return;
3582
4305
  }
3583
4306
  }
3584
- yield r.renderTemplates(this.elseTemplates, ctx, emitter);
4307
+ yield r.renderTemplates(this.elseTemplates || [], ctx, emitter);
4308
+ }
4309
+ *children() {
4310
+ const templates = this.branches.flatMap(b => b.templates);
4311
+ if (this.elseTemplates) {
4312
+ templates.push(...this.elseTemplates);
4313
+ }
4314
+ return templates;
4315
+ }
4316
+ arguments() {
4317
+ return this.branches.map(b => b.value);
3585
4318
  }
3586
4319
  }
3587
4320
 
3588
4321
  class IncrementTag extends Tag {
3589
4322
  constructor(token, remainTokens, liquid) {
3590
4323
  super(token, remainTokens, liquid);
3591
- this.variable = this.tokenizer.readIdentifier().content;
4324
+ this.identifier = this.tokenizer.readIdentifier();
4325
+ this.variable = this.identifier.content;
3592
4326
  }
3593
4327
  render(context, emitter) {
3594
4328
  const scope = context.environments;
@@ -3599,15 +4333,18 @@ class IncrementTag extends Tag {
3599
4333
  scope[this.variable]++;
3600
4334
  emitter.write(stringify(val));
3601
4335
  }
4336
+ *localScope() {
4337
+ yield this.identifier;
4338
+ }
3602
4339
  }
3603
4340
 
3604
4341
  class LayoutTag extends Tag {
3605
- constructor(token, remainTokens, liquid) {
4342
+ constructor(token, remainTokens, liquid, parser) {
3606
4343
  super(token, remainTokens, liquid);
3607
- this.file = parseFilePath(this.tokenizer, this.liquid);
4344
+ this.file = parseFilePath(this.tokenizer, this.liquid, parser);
3608
4345
  this['currentFile'] = token.file;
3609
- this.args = new Hash(this.tokenizer.remaining());
3610
- this.templates = this.liquid.parser.parseTokens(remainTokens);
4346
+ this.args = new Hash(this.tokenizer, liquid.options.keyValueSeparator);
4347
+ this.templates = parser.parseTokens(remainTokens);
3611
4348
  }
3612
4349
  *render(ctx, emitter) {
3613
4350
  const { liquid, args, file } = this;
@@ -3633,10 +4370,32 @@ class LayoutTag extends Tag {
3633
4370
  yield renderer.renderTemplates(templates, ctx, emitter);
3634
4371
  ctx.pop();
3635
4372
  }
4373
+ *children(partials) {
4374
+ const templates = this.templates.slice();
4375
+ if (partials && isString(this.file)) {
4376
+ templates.push(...(yield this.liquid._parsePartialFile(this.file, true, this['currentFile'])));
4377
+ }
4378
+ return templates;
4379
+ }
4380
+ *arguments() {
4381
+ for (const v of Object.values(this.args.hash)) {
4382
+ if (isValueToken(v)) {
4383
+ yield v;
4384
+ }
4385
+ }
4386
+ if (isValueToken(this.file)) {
4387
+ yield this.file;
4388
+ }
4389
+ }
4390
+ partialScope() {
4391
+ if (isString(this.file)) {
4392
+ return { name: this.file, isolated: false, scope: Object.keys(this.args.hash) };
4393
+ }
4394
+ }
3636
4395
  }
3637
4396
 
3638
4397
  class BlockTag extends Tag {
3639
- constructor(token, remainTokens, liquid) {
4398
+ constructor(token, remainTokens, liquid, parser) {
3640
4399
  super(token, remainTokens, liquid);
3641
4400
  this.templates = [];
3642
4401
  const match = /\w+/.exec(token.args);
@@ -3645,7 +4404,7 @@ class BlockTag extends Tag {
3645
4404
  const token = remainTokens.shift();
3646
4405
  if (isTagToken(token) && token.name === 'endblock')
3647
4406
  return;
3648
- const template = liquid.parser.parseToken(token, remainTokens);
4407
+ const template = parser.parseToken(token, remainTokens);
3649
4408
  this.templates.push(template);
3650
4409
  }
3651
4410
  throw new Error(`tag ${token.getText()} not closed`);
@@ -3672,6 +4431,12 @@ class BlockTag extends Tag {
3672
4431
  ? (superBlock, emitter) => renderChild(new BlockDrop(() => renderCurrent(superBlock, emitter)), emitter)
3673
4432
  : renderCurrent;
3674
4433
  }
4434
+ *children() {
4435
+ return this.templates;
4436
+ }
4437
+ blockScope() {
4438
+ return ['block'];
4439
+ }
3675
4440
  }
3676
4441
 
3677
4442
  class RawTag extends Tag {
@@ -3715,7 +4480,7 @@ class TablerowloopDrop extends ForloopDrop {
3715
4480
  }
3716
4481
 
3717
4482
  class TablerowTag extends Tag {
3718
- constructor(tagToken, remainTokens, liquid) {
4483
+ constructor(tagToken, remainTokens, liquid, parser) {
3719
4484
  super(tagToken, remainTokens, liquid);
3720
4485
  const variable = this.tokenizer.readIdentifier();
3721
4486
  this.tokenizer.skipBlank();
@@ -3726,10 +4491,10 @@ class TablerowTag extends Tag {
3726
4491
  }
3727
4492
  this.variable = variable.content;
3728
4493
  this.collection = collectionToken;
3729
- this.args = new Hash(this.tokenizer.remaining());
4494
+ this.args = new Hash(this.tokenizer, liquid.options.keyValueSeparator);
3730
4495
  this.templates = [];
3731
4496
  let p;
3732
- const stream = this.liquid.parser.parseStream(remainTokens)
4497
+ const stream = parser.parseStream(remainTokens)
3733
4498
  .on('start', () => (p = this.templates))
3734
4499
  .on('tag:endtablerow', () => stream.stop())
3735
4500
  .on('template', (tpl) => p.push(tpl))
@@ -3764,18 +4529,32 @@ class TablerowTag extends Tag {
3764
4529
  emitter.write('</tr>');
3765
4530
  ctx.pop();
3766
4531
  }
4532
+ *children() {
4533
+ return this.templates;
4534
+ }
4535
+ *arguments() {
4536
+ yield this.collection;
4537
+ for (const v of Object.values(this.args.hash)) {
4538
+ if (isValueToken(v)) {
4539
+ yield v;
4540
+ }
4541
+ }
4542
+ }
4543
+ blockScope() {
4544
+ return [this.variable, 'tablerowloop'];
4545
+ }
3767
4546
  }
3768
4547
 
3769
4548
  class UnlessTag extends Tag {
3770
- constructor(tagToken, remainTokens, liquid) {
4549
+ constructor(tagToken, remainTokens, liquid, parser) {
3771
4550
  super(tagToken, remainTokens, liquid);
3772
4551
  this.branches = [];
3773
4552
  this.elseTemplates = [];
3774
4553
  let p = [];
3775
4554
  let elseCount = 0;
3776
- this.liquid.parser.parseStream(remainTokens)
4555
+ parser.parseStream(remainTokens)
3777
4556
  .on('start', () => this.branches.push({
3778
- value: new Value(tagToken.args, this.liquid),
4557
+ value: new Value(tagToken.tokenizer.readFilteredValue(), this.liquid),
3779
4558
  test: isFalsy,
3780
4559
  templates: (p = [])
3781
4560
  }))
@@ -3785,7 +4564,7 @@ class UnlessTag extends Tag {
3785
4564
  return;
3786
4565
  }
3787
4566
  this.branches.push({
3788
- value: new Value(token.args, this.liquid),
4567
+ value: new Value(token.tokenizer.readFilteredValue(), this.liquid),
3789
4568
  test: isTruthy,
3790
4569
  templates: (p = [])
3791
4570
  });
@@ -3814,17 +4593,27 @@ class UnlessTag extends Tag {
3814
4593
  }
3815
4594
  yield r.renderTemplates(this.elseTemplates, ctx, emitter);
3816
4595
  }
4596
+ *children() {
4597
+ const children = this.branches.flatMap(b => b.templates);
4598
+ if (this.elseTemplates) {
4599
+ children.push(...this.elseTemplates);
4600
+ }
4601
+ return children;
4602
+ }
4603
+ arguments() {
4604
+ return this.branches.map(b => b.value);
4605
+ }
3817
4606
  }
3818
4607
 
3819
4608
  class BreakTag extends Tag {
3820
- render(ctx, emitter) {
3821
- emitter['break'] = true;
4609
+ render(ctx, _emitter) {
4610
+ ctx.breakCalled = true;
3822
4611
  }
3823
4612
  }
3824
4613
 
3825
4614
  class ContinueTag extends Tag {
3826
- render(ctx, emitter) {
3827
- emitter['continue'] = true;
4615
+ render(ctx, _emitter) {
4616
+ ctx.continueCalled = true;
3828
4617
  }
3829
4618
  }
3830
4619
 
@@ -3842,17 +4631,25 @@ class EchoTag extends Tag {
3842
4631
  const val = yield this.value.value(ctx, false);
3843
4632
  emitter.write(val);
3844
4633
  }
4634
+ *arguments() {
4635
+ if (this.value) {
4636
+ yield this.value;
4637
+ }
4638
+ }
3845
4639
  }
3846
4640
 
3847
4641
  class LiquidTag extends Tag {
3848
- constructor(token, remainTokens, liquid) {
4642
+ constructor(token, remainTokens, liquid, parser) {
3849
4643
  super(token, remainTokens, liquid);
3850
4644
  const tokens = this.tokenizer.readLiquidTagTokens(this.liquid.options);
3851
- this.templates = this.liquid.parser.parseTokens(tokens);
4645
+ this.templates = parser.parseTokens(tokens);
3852
4646
  }
3853
4647
  *render(ctx, emitter) {
3854
4648
  yield this.liquid.renderer.renderTemplates(this.templates, ctx, emitter);
3855
4649
  }
4650
+ *children() {
4651
+ return this.templates;
4652
+ }
3856
4653
  }
3857
4654
 
3858
4655
  class InlineCommentTag extends Tag {
@@ -3895,12 +4692,14 @@ class Liquid {
3895
4692
  this.filters = {};
3896
4693
  this.tags = {};
3897
4694
  this.options = normalize(opts);
4695
+ // eslint-disable-next-line deprecation/deprecation
3898
4696
  this.parser = new Parser(this);
3899
4697
  forOwn(tags, (conf, name) => this.registerTag(name, conf));
3900
4698
  forOwn(filters, (handler, name) => this.registerFilter(name, handler));
3901
4699
  }
3902
4700
  parse(html, filepath) {
3903
- return this.parser.parse(html, filepath);
4701
+ const parser = new Parser(this);
4702
+ return parser.parse(html, filepath);
3904
4703
  }
3905
4704
  _render(tpl, scope, renderOptions) {
3906
4705
  const ctx = scope instanceof Context ? scope : new Context(scope, this.options, renderOptions);
@@ -3931,21 +4730,21 @@ class Liquid {
3931
4730
  return toValueSync(this._parseAndRender(html, scope, Object.assign(Object.assign({}, renderOptions), { sync: true })));
3932
4731
  }
3933
4732
  _parsePartialFile(file, sync, currentFile) {
3934
- return this.parser.parseFile(file, sync, LookupType.Partials, currentFile);
4733
+ return new Parser(this).parseFile(file, sync, LookupType.Partials, currentFile);
3935
4734
  }
3936
4735
  _parseLayoutFile(file, sync, currentFile) {
3937
- return this.parser.parseFile(file, sync, LookupType.Layouts, currentFile);
4736
+ return new Parser(this).parseFile(file, sync, LookupType.Layouts, currentFile);
3938
4737
  }
3939
4738
  _parseFile(file, sync, lookupType, currentFile) {
3940
- return this.parser.parseFile(file, sync, lookupType, currentFile);
4739
+ return new Parser(this).parseFile(file, sync, lookupType, currentFile);
3941
4740
  }
3942
4741
  parseFile(file, lookupType) {
3943
4742
  return __awaiter(this, void 0, void 0, function* () {
3944
- return toPromise(this.parser.parseFile(file, false, lookupType));
4743
+ return toPromise(new Parser(this).parseFile(file, false, lookupType));
3945
4744
  });
3946
4745
  }
3947
4746
  parseFileSync(file, lookupType) {
3948
- return toValueSync(this.parser.parseFile(file, true, lookupType));
4747
+ return toValueSync(new Parser(this).parseFile(file, true, lookupType));
3949
4748
  }
3950
4749
  *_renderFile(file, ctx, renderFileOptions) {
3951
4750
  const templates = (yield this._parseFile(file, renderFileOptions.sync, renderFileOptions.lookupType));
@@ -4001,9 +4800,97 @@ class Liquid {
4001
4800
  self.renderFile(filePath, ctx).then(html => callback(null, html), callback);
4002
4801
  };
4003
4802
  }
4803
+ analyze(template, options = {}) {
4804
+ return __awaiter(this, void 0, void 0, function* () {
4805
+ return analyze(template, options);
4806
+ });
4807
+ }
4808
+ analyzeSync(template, options = {}) {
4809
+ return analyzeSync(template, options);
4810
+ }
4811
+ parseAndAnalyze(html, filename, options = {}) {
4812
+ return __awaiter(this, void 0, void 0, function* () {
4813
+ return analyze(this.parse(html, filename), options);
4814
+ });
4815
+ }
4816
+ parseAndAnalyzeSync(html, filename, options = {}) {
4817
+ return analyzeSync(this.parse(html, filename), options);
4818
+ }
4819
+ /** Return an array of all variables without their properties. */
4820
+ variables(template, options = {}) {
4821
+ return __awaiter(this, void 0, void 0, function* () {
4822
+ const analysis = yield analyze(isString(template) ? this.parse(template) : template, options);
4823
+ return Object.keys(analysis.variables);
4824
+ });
4825
+ }
4826
+ /** Return an array of all variables without their properties. */
4827
+ variablesSync(template, options = {}) {
4828
+ const analysis = analyzeSync(isString(template) ? this.parse(template) : template, options);
4829
+ return Object.keys(analysis.variables);
4830
+ }
4831
+ /** Return an array of all variables including their properties/paths. */
4832
+ fullVariables(template, options = {}) {
4833
+ return __awaiter(this, void 0, void 0, function* () {
4834
+ const analysis = yield analyze(isString(template) ? this.parse(template) : template, options);
4835
+ return Array.from(new Set(Object.values(analysis.variables).flatMap((a) => a.map((v) => String(v)))));
4836
+ });
4837
+ }
4838
+ /** Return an array of all variables including their properties/paths. */
4839
+ fullVariablesSync(template, options = {}) {
4840
+ const analysis = analyzeSync(isString(template) ? this.parse(template) : template, options);
4841
+ return Array.from(new Set(Object.values(analysis.variables).flatMap((a) => a.map((v) => String(v)))));
4842
+ }
4843
+ /** Return an array of all variables, each as an array of properties/segments. */
4844
+ variableSegments(template, options = {}) {
4845
+ return __awaiter(this, void 0, void 0, function* () {
4846
+ const analysis = yield analyze(isString(template) ? this.parse(template) : template, options);
4847
+ return Array.from(strictUniq(Object.values(analysis.variables).flatMap((a) => a.map((v) => v.toArray()))));
4848
+ });
4849
+ }
4850
+ /** Return an array of all variables, each as an array of properties/segments. */
4851
+ variableSegmentsSync(template, options = {}) {
4852
+ const analysis = analyzeSync(isString(template) ? this.parse(template) : template, options);
4853
+ return Array.from(strictUniq(Object.values(analysis.variables).flatMap((a) => a.map((v) => v.toArray()))));
4854
+ }
4855
+ /** Return an array of all expected context variables without their properties. */
4856
+ globalVariables(template, options = {}) {
4857
+ return __awaiter(this, void 0, void 0, function* () {
4858
+ const analysis = yield analyze(isString(template) ? this.parse(template) : template, options);
4859
+ return Object.keys(analysis.globals);
4860
+ });
4861
+ }
4862
+ /** Return an array of all expected context variables without their properties. */
4863
+ globalVariablesSync(template, options = {}) {
4864
+ const analysis = analyzeSync(isString(template) ? this.parse(template) : template, options);
4865
+ return Object.keys(analysis.globals);
4866
+ }
4867
+ /** Return an array of all expected context variables including their properties/paths. */
4868
+ globalFullVariables(template, options = {}) {
4869
+ return __awaiter(this, void 0, void 0, function* () {
4870
+ const analysis = yield analyze(isString(template) ? this.parse(template) : template, options);
4871
+ return Array.from(new Set(Object.values(analysis.globals).flatMap((a) => a.map((v) => String(v)))));
4872
+ });
4873
+ }
4874
+ /** Return an array of all expected context variables including their properties/paths. */
4875
+ globalFullVariablesSync(template, options = {}) {
4876
+ const analysis = analyzeSync(isString(template) ? this.parse(template) : template, options);
4877
+ return Array.from(new Set(Object.values(analysis.globals).flatMap((a) => a.map((v) => String(v)))));
4878
+ }
4879
+ /** Return an array of all expected context variables, each as an array of properties/segments. */
4880
+ globalVariableSegments(template, options = {}) {
4881
+ return __awaiter(this, void 0, void 0, function* () {
4882
+ const analysis = yield analyze(isString(template) ? this.parse(template) : template, options);
4883
+ return Array.from(strictUniq(Object.values(analysis.globals).flatMap((a) => a.map((v) => v.toArray()))));
4884
+ });
4885
+ }
4886
+ /** Return an array of all expected context variables, each as an array of properties/segments. */
4887
+ globalVariableSegmentsSync(template, options = {}) {
4888
+ const analysis = analyzeSync(isString(template) ? this.parse(template) : template, options);
4889
+ return Array.from(strictUniq(Object.values(analysis.globals).flatMap((a) => a.map((v) => v.toArray()))));
4890
+ }
4004
4891
  }
4005
4892
 
4006
4893
  /* istanbul ignore file */
4007
- const version = '10.13.0';
4894
+ const version = '[VI]{version}[/VI]';
4008
4895
 
4009
- 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 };
4896
+ export { AssertionError, AssignTag, BlockTag, BreakTag, CaptureTag, CaseTag, CommentTag, Context, ContinueTag, CycleTag, DecrementTag, Drop, EchoTag, Expression, Filter, ForTag, Hash, IfTag, IncludeTag, IncrementTag, InlineCommentTag, LayoutTag, Liquid, LiquidError, LiquidTag, Output, ParseError, ParseStream, Parser, RawTag, RenderError, RenderTag, TablerowTag, Tag, TagToken, Token, TokenKind, TokenizationError, Tokenizer, typeGuards as TypeGuards, UndefinedVariableError, UnlessTag, Value, Variable, analyze, analyzeSync, assert, createTrie, defaultOperators, defaultOptions, evalQuotedToken, evalToken, filters, isFalsy, isTruthy, tags, toPromise, toValue, toValueSync, version };