@skeptools/skep-organization-project 0.0.15 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/.jsii +138 -3
  2. package/API.md +137 -44
  3. package/lib/skep-organization-project.js +1 -1
  4. package/node_modules/liquidjs/README.md +29 -17
  5. package/node_modules/liquidjs/dist/context/context.d.ts +13 -4
  6. package/node_modules/liquidjs/dist/drop/blank-drop.d.ts +1 -0
  7. package/node_modules/liquidjs/dist/drop/drop.d.ts +1 -1
  8. package/node_modules/liquidjs/dist/drop/empty-drop.d.ts +1 -0
  9. package/node_modules/liquidjs/dist/filters/array.d.ts +14 -14
  10. package/node_modules/liquidjs/dist/filters/html.d.ts +6 -5
  11. package/node_modules/liquidjs/dist/filters/math.d.ts +9 -9
  12. package/node_modules/liquidjs/dist/filters/string.d.ts +22 -21
  13. package/node_modules/liquidjs/dist/fs/map-fs.d.ts +13 -0
  14. package/node_modules/liquidjs/dist/fs/node-require.d.ts +1 -2
  15. package/node_modules/liquidjs/dist/index.d.ts +3 -3
  16. package/node_modules/liquidjs/dist/liquid-options.d.ts +30 -2
  17. package/node_modules/liquidjs/dist/liquid.browser.min.js +1 -1
  18. package/node_modules/liquidjs/dist/liquid.browser.min.js.map +1 -1
  19. package/node_modules/liquidjs/dist/{liquid.browser.esm.js → liquid.browser.mjs} +1147 -264
  20. package/node_modules/liquidjs/dist/liquid.browser.umd.js +2072 -478
  21. package/node_modules/liquidjs/dist/liquid.browser.umd.js.map +1 -1
  22. package/node_modules/liquidjs/dist/liquid.d.ts +32 -1
  23. package/node_modules/liquidjs/dist/{liquid.node.cjs.js → liquid.node.js} +1123 -264
  24. package/node_modules/liquidjs/dist/{liquid.node.esm.js → liquid.node.mjs} +1152 -265
  25. package/node_modules/liquidjs/dist/parser/parser.d.ts +1 -0
  26. package/node_modules/liquidjs/dist/parser/parser.spec.d.ts +1 -0
  27. package/node_modules/liquidjs/dist/parser/tokenizer.d.ts +4 -3
  28. package/node_modules/liquidjs/dist/render/expression.d.ts +1 -1
  29. package/node_modules/liquidjs/dist/render/operator.d.ts +1 -0
  30. package/node_modules/liquidjs/dist/tags/assign.d.ts +5 -0
  31. package/node_modules/liquidjs/dist/tags/block.d.ts +4 -1
  32. package/node_modules/liquidjs/dist/tags/break.d.ts +1 -1
  33. package/node_modules/liquidjs/dist/tags/capture.d.ts +7 -2
  34. package/node_modules/liquidjs/dist/tags/case.d.ts +5 -1
  35. package/node_modules/liquidjs/dist/tags/continue.d.ts +1 -1
  36. package/node_modules/liquidjs/dist/tags/cycle.d.ts +2 -0
  37. package/node_modules/liquidjs/dist/tags/decrement.d.ts +3 -0
  38. package/node_modules/liquidjs/dist/tags/echo.d.ts +2 -0
  39. package/node_modules/liquidjs/dist/tags/for.d.ts +6 -1
  40. package/node_modules/liquidjs/dist/tags/if.d.ts +6 -2
  41. package/node_modules/liquidjs/dist/tags/include.d.ts +7 -2
  42. package/node_modules/liquidjs/dist/tags/increment.d.ts +3 -0
  43. package/node_modules/liquidjs/dist/tags/layout.d.ts +6 -1
  44. package/node_modules/liquidjs/dist/tags/liquid.d.ts +3 -1
  45. package/node_modules/liquidjs/dist/tags/render.d.ts +7 -2
  46. package/node_modules/liquidjs/dist/tags/tablerow.d.ts +6 -1
  47. package/node_modules/liquidjs/dist/tags/unless.d.ts +5 -1
  48. package/node_modules/liquidjs/dist/template/analysis.d.ts +85 -0
  49. package/node_modules/liquidjs/dist/template/analysis.spec.d.ts +1 -0
  50. package/node_modules/liquidjs/dist/template/filter-impl-options.d.ts +2 -0
  51. package/node_modules/liquidjs/dist/template/filter.d.ts +3 -1
  52. package/node_modules/liquidjs/dist/template/hash.d.ts +2 -1
  53. package/node_modules/liquidjs/dist/template/index.d.ts +1 -0
  54. package/node_modules/liquidjs/dist/template/output.d.ts +2 -1
  55. package/node_modules/liquidjs/dist/template/tag.d.ts +2 -2
  56. package/node_modules/liquidjs/dist/template/template.d.ts +29 -0
  57. package/node_modules/liquidjs/dist/tokens/identifier-token.d.ts +0 -1
  58. package/node_modules/liquidjs/dist/tokens/liquid-tag-token.d.ts +1 -1
  59. package/node_modules/liquidjs/dist/util/assert.d.ts +1 -0
  60. package/node_modules/liquidjs/dist/util/error.d.ts +7 -2
  61. package/node_modules/liquidjs/dist/util/error.spec.d.ts +1 -0
  62. package/node_modules/liquidjs/dist/util/index.d.ts +2 -1
  63. package/node_modules/liquidjs/dist/util/intl.d.ts +6 -0
  64. package/node_modules/liquidjs/dist/util/limiter.d.ts +8 -0
  65. package/node_modules/liquidjs/dist/util/liquid-date.d.ts +39 -7
  66. package/node_modules/liquidjs/dist/util/liquid-date.spec.d.ts +1 -0
  67. package/node_modules/liquidjs/dist/util/performance.d.ts +5 -0
  68. package/node_modules/liquidjs/dist/util/performance.spec.d.ts +1 -0
  69. package/node_modules/liquidjs/dist/util/type-guards.d.ts +2 -1
  70. package/node_modules/liquidjs/dist/util/type-guards.spec.d.ts +1 -0
  71. package/node_modules/liquidjs/dist/util/underscore.d.ts +5 -1
  72. package/node_modules/liquidjs/package.json +11 -7
  73. package/package.json +17 -13
  74. package/node_modules/liquidjs/dist/util/timezone-date.d.ts +0 -44
  75. /package/node_modules/liquidjs/dist/{util/timezone-date.spec.d.ts → fs/map-fs.spec.d.ts} +0 -0
@@ -1,6 +1,6 @@
1
1
  /*
2
- * liquidjs@10.13.0, https://github.com/harttle/liquidjs
3
- * (c) 2016-2024 harttle
2
+ * liquidjs@10.20.1, https://github.com/harttle/liquidjs
3
+ * (c) 2016-2025 harttle
4
4
  * Released under the MIT License.
5
5
  */
6
6
  'use strict';
@@ -106,6 +106,10 @@ function toArray(val) {
106
106
  function toValue(value) {
107
107
  return (value instanceof Drop && isFunction(value.valueOf)) ? value.valueOf() : value;
108
108
  }
109
+ function toNumber(value) {
110
+ value = Number(value);
111
+ return isNaN(value) ? 0 : value;
112
+ }
109
113
  function isNumber(value) {
110
114
  return typeof value === 'number';
111
115
  }
@@ -124,6 +128,9 @@ function isArray(value) {
124
128
  // be compatible with IE 8
125
129
  return toString$1.call(value) === '[object Array]';
126
130
  }
131
+ function isArrayLike(value) {
132
+ return value && isNumber(value.length);
133
+ }
127
134
  function isIterable(value) {
128
135
  return isObject(value) && Symbol.iterator in value;
129
136
  }
@@ -205,14 +212,25 @@ function caseInsensitiveCompare(a, b) {
205
212
  return 0;
206
213
  }
207
214
  function argumentsToValue(fn) {
208
- return (...args) => fn(...args.map(toValue));
215
+ return function (...args) { return fn.call(this, ...args.map(toValue)); };
209
216
  }
210
217
  function escapeRegExp(text) {
211
218
  return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
219
+ }
220
+ /** Return an array containing unique elements from _array_. Works with nested arrays and objects. */
221
+ function* strictUniq(array) {
222
+ const seen = new Set();
223
+ for (const element of array) {
224
+ const key = JSON.stringify(element);
225
+ if (!seen.has(key)) {
226
+ seen.add(key);
227
+ yield element;
228
+ }
229
+ }
212
230
  }
213
231
 
214
232
  /**
215
- * targeting ES5, extends Error won't create a proper prototype chain, need a trait to kee track of classes
233
+ * targeting ES5, extends Error won't create a proper prototype chain, need a trait to keep track of classes
216
234
  */
217
235
  const TRAIT = '__liquidClass__';
218
236
  class LiquidError extends Error {
@@ -266,6 +284,19 @@ class RenderError extends LiquidError {
266
284
  return obj.name === 'RenderError';
267
285
  }
268
286
  }
287
+ class LiquidErrors extends LiquidError {
288
+ constructor(errors) {
289
+ super(errors[0], errors[0].token);
290
+ this.errors = errors;
291
+ this.name = 'LiquidErrors';
292
+ const s = errors.length > 1 ? 's' : '';
293
+ this.message = `${errors.length} error${s} found`;
294
+ super.update();
295
+ }
296
+ static is(obj) {
297
+ return obj.name === 'LiquidErrors';
298
+ }
299
+ }
269
300
  class UndefinedVariableError extends LiquidError {
270
301
  constructor(err, token) {
271
302
  super(err, token);
@@ -344,6 +375,9 @@ function assert(predicate, message) {
344
375
  : (message || `expect ${predicate} to be true`);
345
376
  throw new AssertionError(msg);
346
377
  }
378
+ }
379
+ function assertEmpty(predicate, message = `unexpected ${JSON.stringify(predicate)}`) {
380
+ assert(!predicate, message);
347
381
  }
348
382
 
349
383
  class NullDrop extends Drop {
@@ -393,6 +427,9 @@ class EmptyDrop extends Drop {
393
427
  valueOf() {
394
428
  return '';
395
429
  }
430
+ static is(value) {
431
+ return value instanceof EmptyDrop;
432
+ }
396
433
  }
397
434
 
398
435
  class BlankDrop extends EmptyDrop {
@@ -405,6 +442,9 @@ class BlankDrop extends EmptyDrop {
405
442
  return /^\s*$/.test(value);
406
443
  return super.equals(value);
407
444
  }
445
+ static is(value) {
446
+ return value instanceof BlankDrop;
447
+ }
408
448
  }
409
449
 
410
450
  class ForloopDrop extends Drop {
@@ -457,7 +497,12 @@ class BlockDrop extends Drop {
457
497
  }
458
498
 
459
499
  function isComparable(arg) {
460
- return arg && isFunction(arg.equals);
500
+ return (arg &&
501
+ isFunction(arg.equals) &&
502
+ isFunction(arg.gt) &&
503
+ isFunction(arg.geq) &&
504
+ isFunction(arg.lt) &&
505
+ isFunction(arg.leq));
461
506
  }
462
507
 
463
508
  const nil = new NullDrop();
@@ -539,18 +584,6 @@ function toValueSync(val) {
539
584
  }
540
585
 
541
586
  const rFormat = /%([-_0^#:]+)?(\d+)?([EO])?(.)/;
542
- const monthNames = [
543
- 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August',
544
- 'September', 'October', 'November', 'December'
545
- ];
546
- const dayNames = [
547
- 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'
548
- ];
549
- const monthNamesShort = monthNames.map(abbr);
550
- const dayNamesShort = dayNames.map(abbr);
551
- function abbr(str) {
552
- return str.slice(0, 3);
553
- }
554
587
  // prototype extensions
555
588
  function daysInMonth(d) {
556
589
  const feb = isLeapYear(d) ? 29 : 28;
@@ -605,19 +638,7 @@ const padWidths = {
605
638
  U: 2,
606
639
  W: 2
607
640
  };
608
- // default to '0'
609
- const padChars = {
610
- a: ' ',
611
- A: ' ',
612
- b: ' ',
613
- B: ' ',
614
- c: ' ',
615
- e: ' ',
616
- k: ' ',
617
- l: ' ',
618
- p: ' ',
619
- P: ' '
620
- };
641
+ const padSpaceChars = new Set('aAbBceklpP');
621
642
  function getTimezoneOffset(d, opts) {
622
643
  const nOffset = Math.abs(d.getTimezoneOffset());
623
644
  const h = Math.floor(nOffset / 60);
@@ -628,10 +649,10 @@ function getTimezoneOffset(d, opts) {
628
649
  padStart(m, 2, '0');
629
650
  }
630
651
  const formatCodes = {
631
- a: (d) => dayNamesShort[d.getDay()],
632
- A: (d) => dayNames[d.getDay()],
633
- b: (d) => monthNamesShort[d.getMonth()],
634
- B: (d) => monthNames[d.getMonth()],
652
+ a: (d) => d.getShortWeekdayName(),
653
+ A: (d) => d.getLongWeekdayName(),
654
+ b: (d) => d.getShortMonthName(),
655
+ B: (d) => d.getLongMonthName(),
635
656
  c: (d) => d.toLocaleString(),
636
657
  C: (d) => century(d),
637
658
  d: (d) => d.getDate(),
@@ -663,12 +684,7 @@ const formatCodes = {
663
684
  y: (d) => d.getFullYear().toString().slice(2, 4),
664
685
  Y: (d) => d.getFullYear(),
665
686
  z: getTimezoneOffset,
666
- Z: (d, opts) => {
667
- if (d.getTimezoneName) {
668
- return d.getTimezoneName() || getTimezoneOffset(d, opts);
669
- }
670
- return (typeof Intl !== 'undefined' ? Intl.DateTimeFormat().resolvedOptions().timeZone : '');
671
- },
687
+ Z: (d, opts) => d.getTimeZoneName() || getTimezoneOffset(d, opts),
672
688
  't': () => '\t',
673
689
  'n': () => '\n',
674
690
  '%': () => '%'
@@ -694,7 +710,7 @@ function format(d, match) {
694
710
  for (const flag of flagStr)
695
711
  flags[flag] = true;
696
712
  let ret = String(convert(d, { flags, width, modifier }));
697
- let padChar = padChars[conversion] || '0';
713
+ let padChar = padSpaceChars.has(conversion) ? ' ' : '0';
698
714
  let padWidth = width || padWidths[conversion] || 0;
699
715
  if (flags['^'])
700
716
  ret = ret.toUpperCase();
@@ -709,9 +725,26 @@ function format(d, match) {
709
725
  return padStart(ret, padWidth, padChar);
710
726
  }
711
727
 
728
+ function getDateTimeFormat() {
729
+ return (typeof Intl !== 'undefined' ? Intl.DateTimeFormat : undefined);
730
+ }
731
+
712
732
  // one minute in milliseconds
713
733
  const OneMinute = 60000;
714
- const ISO8601_TIMEZONE_PATTERN = /([zZ]|([+-])(\d{2}):(\d{2}))$/;
734
+ /**
735
+ * Need support both ISO8601 and RFC2822 as in major browsers & NodeJS
736
+ * RFC2822: https://datatracker.ietf.org/doc/html/rfc2822#section-3.3
737
+ */
738
+ const TIMEZONE_PATTERN = /([zZ]|([+-])(\d{2}):?(\d{2}))$/;
739
+ const monthNames = [
740
+ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August',
741
+ 'September', 'October', 'November', 'December'
742
+ ];
743
+ const monthNamesShort = monthNames.map(name => name.slice(0, 3));
744
+ const dayNames = [
745
+ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'
746
+ ];
747
+ const dayNamesShort = dayNames.map(name => name.slice(0, 3));
715
748
  /**
716
749
  * A date implementation with timezone info, just like Ruby date
717
750
  *
@@ -719,12 +752,16 @@ const ISO8601_TIMEZONE_PATTERN = /([zZ]|([+-])(\d{2}):(\d{2}))$/;
719
752
  * - create a Date offset by it's timezone difference, avoiding overriding a bunch of methods
720
753
  * - rewrite getTimezoneOffset() to trick strftime
721
754
  */
722
- class TimezoneDate {
723
- constructor(init, timezone) {
724
- this.date = init instanceof TimezoneDate
725
- ? init.date
726
- : new Date(init);
727
- this.timezoneOffset = isString(timezone) ? TimezoneDate.getTimezoneOffset(timezone, this.date) : timezone;
755
+ class LiquidDate {
756
+ constructor(init, locale, timezone) {
757
+ this.locale = locale;
758
+ this.DateTimeFormat = getDateTimeFormat();
759
+ this.date = new Date(init);
760
+ this.timezoneFixed = timezone !== undefined;
761
+ if (timezone === undefined) {
762
+ timezone = this.date.getTimezoneOffset();
763
+ }
764
+ this.timezoneOffset = isString(timezone) ? LiquidDate.getTimezoneOffset(timezone, this.date) : timezone;
728
765
  this.timezoneName = isString(timezone) ? timezone : '';
729
766
  const diff = (this.date.getTimezoneOffset() - this.timezoneOffset) * OneMinute;
730
767
  const time = this.date.getTime() + diff;
@@ -772,8 +809,30 @@ class TimezoneDate {
772
809
  getTimezoneOffset() {
773
810
  return this.timezoneOffset;
774
811
  }
775
- getTimezoneName() {
776
- return this.timezoneName;
812
+ getTimeZoneName() {
813
+ if (this.timezoneFixed)
814
+ return this.timezoneName;
815
+ if (!this.DateTimeFormat)
816
+ return;
817
+ return this.DateTimeFormat().resolvedOptions().timeZone;
818
+ }
819
+ getLongMonthName() {
820
+ return this.format({ month: 'long' }) ?? monthNames[this.getMonth()];
821
+ }
822
+ getShortMonthName() {
823
+ return this.format({ month: 'short' }) ?? monthNamesShort[this.getMonth()];
824
+ }
825
+ getLongWeekdayName() {
826
+ return this.format({ weekday: 'long' }) ?? dayNames[this.displayDate.getDay()];
827
+ }
828
+ getShortWeekdayName() {
829
+ return this.format({ weekday: 'short' }) ?? dayNamesShort[this.displayDate.getDay()];
830
+ }
831
+ valid() {
832
+ return !isNaN(this.getTime());
833
+ }
834
+ format(options) {
835
+ return this.DateTimeFormat && this.DateTimeFormat(this.locale, options).format(this.displayDate);
777
836
  }
778
837
  /**
779
838
  * Create a Date object fixed to it's declared Timezone. Both
@@ -788,21 +847,21 @@ class TimezoneDate {
788
847
  * we create a different Date to trick strftime, it's both simpler and more performant.
789
848
  * Given that a template is expected to be parsed fewer times than rendered.
790
849
  */
791
- static createDateFixedToTimezone(dateString) {
792
- const m = dateString.match(ISO8601_TIMEZONE_PATTERN);
850
+ static createDateFixedToTimezone(dateString, locale) {
851
+ const m = dateString.match(TIMEZONE_PATTERN);
793
852
  // representing a UTC timestamp
794
853
  if (m && m[1] === 'Z') {
795
- return new TimezoneDate(+new Date(dateString), 0);
854
+ return new LiquidDate(+new Date(dateString), locale, 0);
796
855
  }
797
856
  // has a timezone specified
798
857
  if (m && m[2] && m[3] && m[4]) {
799
858
  const [, , sign, hours, minutes] = m;
800
859
  const offset = (sign === '+' ? -1 : 1) * (parseInt(hours, 10) * 60 + parseInt(minutes, 10));
801
- return new TimezoneDate(+new Date(dateString), offset);
860
+ return new LiquidDate(+new Date(dateString), locale, offset);
802
861
  }
803
- return new Date(dateString);
862
+ return new LiquidDate(dateString, locale);
804
863
  }
805
- static getTimezoneOffset(timezoneName, date = new Date()) {
864
+ static getTimezoneOffset(timezoneName, date) {
806
865
  const localDateString = date.toLocaleString('en-US', { timeZone: timezoneName });
807
866
  const utcDateString = date.toLocaleString('en-US', { timeZone: 'UTC' });
808
867
  const localDate = new Date(localDateString);
@@ -811,6 +870,23 @@ class TimezoneDate {
811
870
  }
812
871
  }
813
872
 
873
+ class Limiter {
874
+ constructor(resource, limit) {
875
+ this.base = 0;
876
+ this.message = `${resource} limit exceeded`;
877
+ this.limit = limit;
878
+ }
879
+ use(count) {
880
+ count = toNumber(count);
881
+ assert(this.base + count <= this.limit, this.message);
882
+ this.base += count;
883
+ }
884
+ check(count) {
885
+ count = toNumber(count);
886
+ assert(count <= this.limit, this.message);
887
+ }
888
+ }
889
+
814
890
  class DelimitedToken extends Token {
815
891
  constructor(kind, [contentBegin, contentEnd], input, begin, end, trimLeft, trimRight, file) {
816
892
  super(kind, input, begin, end, file);
@@ -891,16 +967,6 @@ class IdentifierToken extends Token {
891
967
  this.file = file;
892
968
  this.content = this.getText();
893
969
  }
894
- isNumber(allowSign = false) {
895
- const begin = allowSign && TYPES[this.input.charCodeAt(this.begin)] & SIGN
896
- ? this.begin + 1
897
- : this.begin;
898
- for (let i = begin; i < this.end; i++) {
899
- if (!(TYPES[this.input.charCodeAt(i)] & NUMBER))
900
- return false;
901
- }
902
- return true;
903
- }
904
970
  }
905
971
 
906
972
  class LiteralToken extends Token {
@@ -1068,7 +1134,9 @@ class LiquidTagToken extends DelimitedToken {
1068
1134
  this.name = this.tokenizer.readTagName();
1069
1135
  this.tokenizer.assert(this.name, 'illegal liquid tag syntax');
1070
1136
  this.tokenizer.skipBlank();
1071
- this.args = this.tokenizer.remaining();
1137
+ }
1138
+ get args() {
1139
+ return this.tokenizer.input.slice(this.tokenizer.p, this.contentRange[1]);
1072
1140
  }
1073
1141
  }
1074
1142
 
@@ -1089,6 +1157,15 @@ class FilteredValueToken extends Token {
1089
1157
  }
1090
1158
  }
1091
1159
 
1160
+ const polyfill = {
1161
+ now: () => Date.now()
1162
+ };
1163
+ function getPerformance() {
1164
+ return (typeof global === 'object' && global.performance) ||
1165
+ (typeof window === 'object' && window.performance) ||
1166
+ polyfill;
1167
+ }
1168
+
1092
1169
  class SimpleEmitter {
1093
1170
  constructor() {
1094
1171
  this.buffer = '';
@@ -1144,20 +1221,28 @@ class Render {
1144
1221
  if (!emitter) {
1145
1222
  emitter = ctx.opts.keepOutputType ? new KeepingTypeEmitter() : new SimpleEmitter();
1146
1223
  }
1224
+ const errors = [];
1147
1225
  for (const tpl of templates) {
1226
+ ctx.renderLimit.check(getPerformance().now());
1148
1227
  try {
1149
1228
  // if tpl.render supports emitter, it'll return empty `html`
1150
1229
  const html = yield tpl.render(ctx, emitter);
1151
1230
  // if not, it'll return an `html`, write to the emitter for it
1152
1231
  html && emitter.write(html);
1153
- if (emitter['break'] || emitter['continue'])
1232
+ if (ctx.breakCalled || ctx.continueCalled)
1154
1233
  break;
1155
1234
  }
1156
1235
  catch (e) {
1157
- const err = RenderError.is(e) ? e : new RenderError(e, tpl);
1158
- throw err;
1236
+ const err = LiquidError.is(e) ? e : new RenderError(e, tpl);
1237
+ if (ctx.opts.catchAllErrors)
1238
+ errors.push(err);
1239
+ else
1240
+ throw err;
1159
1241
  }
1160
1242
  }
1243
+ if (errors.length) {
1244
+ throw new LiquidErrors(errors);
1245
+ }
1161
1246
  return emitter.buffer;
1162
1247
  }
1163
1248
  }
@@ -1228,6 +1313,7 @@ function evalQuotedToken(token) {
1228
1313
  function* evalRangeToken(token, ctx) {
1229
1314
  const low = yield evalToken(token.lhs, ctx);
1230
1315
  const high = yield evalToken(token.rhs, ctx);
1316
+ ctx.memoryLimit.use(high - low + 1);
1231
1317
  return range(+low, +high + 1);
1232
1318
  }
1233
1319
  function* toPostfix(tokens) {
@@ -1251,6 +1337,7 @@ function isTruthy(val, ctx) {
1251
1337
  return !isFalsy(val, ctx);
1252
1338
  }
1253
1339
  function isFalsy(val, ctx) {
1340
+ val = toValue(val);
1254
1341
  if (ctx.opts.jsTruthy) {
1255
1342
  return !val;
1256
1343
  }
@@ -1318,6 +1405,9 @@ function arrayEquals(lhs, rhs) {
1318
1405
  if (lhs.length !== rhs.length)
1319
1406
  return false;
1320
1407
  return !lhs.some((value, i) => !equals(value, rhs[i]));
1408
+ }
1409
+ function arrayIncludes(arr, item) {
1410
+ return arr.some(value => equals(value, item));
1321
1411
  }
1322
1412
 
1323
1413
  class Node {
@@ -1378,7 +1468,7 @@ class LRU {
1378
1468
  }
1379
1469
  }
1380
1470
 
1381
- const requireResolve = require.resolve;
1471
+ const requireResolve = (partial) => require.resolve(partial, { paths: ['.'] });
1382
1472
 
1383
1473
  const statAsync = promisify(fs$1.stat);
1384
1474
  const readFileAsync = promisify(fs$1.readFile);
@@ -1495,22 +1585,30 @@ const unescapeMap = {
1495
1585
  '&#39;': "'"
1496
1586
  };
1497
1587
  function escape(str) {
1498
- return stringify(str).replace(/&|<|>|"|'/g, m => escapeMap[m]);
1588
+ str = stringify(str);
1589
+ this.context.memoryLimit.use(str.length);
1590
+ return str.replace(/&|<|>|"|'/g, m => escapeMap[m]);
1499
1591
  }
1500
1592
  function xml_escape(str) {
1501
- return escape(str);
1593
+ return escape.call(this, str);
1502
1594
  }
1503
1595
  function unescape(str) {
1504
- return stringify(str).replace(/&(amp|lt|gt|#34|#39);/g, m => unescapeMap[m]);
1596
+ str = stringify(str);
1597
+ this.context.memoryLimit.use(str.length);
1598
+ return str.replace(/&(amp|lt|gt|#34|#39);/g, m => unescapeMap[m]);
1505
1599
  }
1506
1600
  function escape_once(str) {
1507
- return escape(unescape(stringify(str)));
1601
+ return escape.call(this, unescape.call(this, str));
1508
1602
  }
1509
1603
  function newline_to_br(v) {
1510
- return stringify(v).replace(/\r?\n/gm, '<br />\n');
1604
+ const str = stringify(v);
1605
+ this.context.memoryLimit.use(str.length);
1606
+ return str.replace(/\r?\n/gm, '<br />\n');
1511
1607
  }
1512
1608
  function strip_html(v) {
1513
- return stringify(v).replace(/<script[\s\S]*?<\/script>|<style[\s\S]*?<\/style>|<.*?>|<!--[\s\S]*?-->/g, '');
1609
+ const str = stringify(v);
1610
+ this.context.memoryLimit.use(str.length);
1611
+ return str.replace(/<script[\s\S]*?<\/script>|<style[\s\S]*?<\/style>|<.*?>|<!--[\s\S]*?-->/g, '');
1514
1612
  }
1515
1613
 
1516
1614
  var htmlFilters = /*#__PURE__*/Object.freeze({
@@ -1522,18 +1620,64 @@ var htmlFilters = /*#__PURE__*/Object.freeze({
1522
1620
  strip_html: strip_html
1523
1621
  });
1524
1622
 
1623
+ class MapFS {
1624
+ constructor(mapping) {
1625
+ this.mapping = mapping;
1626
+ this.sep = '/';
1627
+ }
1628
+ async exists(filepath) {
1629
+ return this.existsSync(filepath);
1630
+ }
1631
+ existsSync(filepath) {
1632
+ return !isNil(this.mapping[filepath]);
1633
+ }
1634
+ async readFile(filepath) {
1635
+ return this.readFileSync(filepath);
1636
+ }
1637
+ readFileSync(filepath) {
1638
+ const content = this.mapping[filepath];
1639
+ if (isNil(content))
1640
+ throw new Error(`ENOENT: ${filepath}`);
1641
+ return content;
1642
+ }
1643
+ dirname(filepath) {
1644
+ const segments = filepath.split(this.sep);
1645
+ segments.pop();
1646
+ return segments.join(this.sep);
1647
+ }
1648
+ resolve(dir, file, ext) {
1649
+ file += ext;
1650
+ if (dir === '.')
1651
+ return file;
1652
+ const segments = dir.split(/\/+/);
1653
+ for (const segment of file.split(this.sep)) {
1654
+ if (segment === '.' || segment === '')
1655
+ continue;
1656
+ else if (segment === '..') {
1657
+ if (segments.length > 1 || segments[0] !== '')
1658
+ segments.pop();
1659
+ }
1660
+ else
1661
+ segments.push(segment);
1662
+ }
1663
+ return segments.join(this.sep);
1664
+ }
1665
+ }
1666
+
1525
1667
  const defaultOptions = {
1526
1668
  root: ['.'],
1527
1669
  layouts: ['.'],
1528
1670
  partials: ['.'],
1529
1671
  relativeReference: true,
1530
1672
  jekyllInclude: false,
1673
+ keyValueSeparator: ':',
1531
1674
  cache: undefined,
1532
1675
  extname: '',
1533
1676
  fs: fs,
1534
1677
  dynamicPartials: true,
1535
1678
  jsTruthy: false,
1536
1679
  dateFormat: '%A, %B %-e, %Y at %-l:%M %P %z',
1680
+ locale: '',
1537
1681
  trimTagRight: false,
1538
1682
  trimTagLeft: false,
1539
1683
  trimOutputRight: false,
@@ -1550,7 +1694,10 @@ const defaultOptions = {
1550
1694
  lenientIf: false,
1551
1695
  globals: {},
1552
1696
  keepOutputType: false,
1553
- operators: defaultOperators
1697
+ operators: defaultOperators,
1698
+ memoryLimit: Infinity,
1699
+ parseLimit: Infinity,
1700
+ renderLimit: Infinity
1554
1701
  };
1555
1702
  function normalize(options) {
1556
1703
  if (options.hasOwnProperty('root')) {
@@ -1578,6 +1725,14 @@ function normalize(options) {
1578
1725
  options.partials = normalizeDirectoryList(options.partials);
1579
1726
  options.layouts = normalizeDirectoryList(options.layouts);
1580
1727
  options.outputEscape = options.outputEscape && getOutputEscapeFunction(options.outputEscape);
1728
+ if (!options.locale) {
1729
+ options.locale = getDateTimeFormat()?.().resolvedOptions().locale ?? 'en-US';
1730
+ }
1731
+ if (options.templates) {
1732
+ options.fs = new MapFS(options.templates);
1733
+ options.relativeReference = true;
1734
+ options.root = options.partials = options.layouts = '.';
1735
+ }
1581
1736
  return options;
1582
1737
  }
1583
1738
  function getOutputEscapeFunction(nameOrFunction) {
@@ -1704,9 +1859,7 @@ class Tokenizer {
1704
1859
  this.skipBlank();
1705
1860
  if (this.end())
1706
1861
  return null;
1707
- this.assert(this.peek() === '|', `expected "|" before filter`);
1708
- this.p++;
1709
- const begin = this.p;
1862
+ this.assert(this.read() === '|', `expected "|" before filter`);
1710
1863
  const name = this.readIdentifier();
1711
1864
  if (!name.size()) {
1712
1865
  this.assert(this.end(), `expected filter name`);
@@ -1727,7 +1880,7 @@ class Tokenizer {
1727
1880
  else {
1728
1881
  throw this.error('expected ":" after filter name');
1729
1882
  }
1730
- return new FilterToken(name.getText(), args, this.input, begin, this.p, this.file);
1883
+ return new FilterToken(name.getText(), args, this.input, name.begin, this.p, this.file);
1731
1884
  }
1732
1885
  readFilterArg() {
1733
1886
  const key = this.readValue();
@@ -1768,7 +1921,7 @@ class Tokenizer {
1768
1921
  }
1769
1922
  return new HTMLToken(this.input, begin, this.p, this.file);
1770
1923
  }
1771
- readTagToken(options = defaultOptions) {
1924
+ readTagToken(options) {
1772
1925
  const { file, input } = this;
1773
1926
  const begin = this.p;
1774
1927
  if (this.readToDelimiter(options.tagDelimiterRight) === -1) {
@@ -1899,7 +2052,7 @@ class Tokenizer {
1899
2052
  return;
1900
2053
  let value;
1901
2054
  this.skipBlank();
1902
- const sep = jekyllStyle ? '=' : ':';
2055
+ const sep = isString(jekyllStyle) ? jekyllStyle : (jekyllStyle ? '=' : ':');
1903
2056
  if (this.peek() === sep) {
1904
2057
  ++this.p;
1905
2058
  value = this.readValue();
@@ -1915,6 +2068,9 @@ class Tokenizer {
1915
2068
  end() {
1916
2069
  return this.p >= this.N;
1917
2070
  }
2071
+ read() {
2072
+ return this.input[this.p++];
2073
+ }
1918
2074
  readTo(end) {
1919
2075
  while (this.p < this.N) {
1920
2076
  ++this.p;
@@ -2138,9 +2294,9 @@ class Tag extends TemplateImpl {
2138
2294
  * hash['reversed'] === undefined
2139
2295
  */
2140
2296
  class Hash {
2141
- constructor(markup, jekyllStyle) {
2297
+ constructor(input, jekyllStyle) {
2142
2298
  this.hash = {};
2143
- const tokenizer = new Tokenizer(markup, {});
2299
+ const tokenizer = input instanceof Tokenizer ? input : new Tokenizer(input, {});
2144
2300
  for (const hash of tokenizer.readHashes(jekyllStyle)) {
2145
2301
  this.hash[hash.name.content] = hash.value;
2146
2302
  }
@@ -2163,7 +2319,7 @@ function createTagClass(options) {
2163
2319
  }
2164
2320
  }
2165
2321
  *render(ctx, emitter) {
2166
- const hash = (yield new Hash(this.token.args).render(ctx));
2322
+ const hash = (yield new Hash(this.token.args, ctx.opts.keyValueSeparator).render(ctx));
2167
2323
  return yield options.render.call(this, ctx, emitter, hash);
2168
2324
  }
2169
2325
  };
@@ -2174,13 +2330,14 @@ function isKeyValuePair(arr) {
2174
2330
  }
2175
2331
 
2176
2332
  class Filter {
2177
- constructor(name, options, args, liquid) {
2178
- this.name = name;
2333
+ constructor(token, options, liquid) {
2334
+ this.token = token;
2335
+ this.name = token.name;
2179
2336
  this.handler = isFunction(options)
2180
2337
  ? options
2181
2338
  : (isFunction(options?.handler) ? options.handler : identify);
2182
2339
  this.raw = !isFunction(options) && !!options?.raw;
2183
- this.args = args;
2340
+ this.args = token.args;
2184
2341
  this.liquid = liquid;
2185
2342
  }
2186
2343
  *render(value, context) {
@@ -2191,7 +2348,7 @@ class Filter {
2191
2348
  else
2192
2349
  argv.push(yield evalToken(arg, context));
2193
2350
  }
2194
- return yield this.handler.apply({ context, liquid: this.liquid }, [value, ...argv]);
2351
+ return yield this.handler.apply({ context, token: this.token, liquid: this.liquid }, [value, ...argv]);
2195
2352
  }
2196
2353
  }
2197
2354
 
@@ -2205,7 +2362,7 @@ class Value {
2205
2362
  ? new Tokenizer(input, liquid.options.operators).readFilteredValue()
2206
2363
  : input;
2207
2364
  this.initial = token.initial;
2208
- this.filters = token.filters.map(({ name, args }) => new Filter(name, this.getFilter(liquid, name), args, liquid));
2365
+ this.filters = token.filters.map(token => new Filter(token, this.getFilter(liquid, token.name), liquid));
2209
2366
  }
2210
2367
  *value(ctx, lenient) {
2211
2368
  lenient = lenient || (ctx.opts.lenientIf && this.filters.length > 0 && this.filters[0].name === 'default');
@@ -2230,13 +2387,17 @@ class Output extends TemplateImpl {
2230
2387
  const filters = this.value.filters;
2231
2388
  const outputEscape = liquid.options.outputEscape;
2232
2389
  if (!filters[filters.length - 1]?.raw && outputEscape) {
2233
- filters.push(new Filter(toString.call(outputEscape), outputEscape, [], liquid));
2390
+ const token = new FilterToken(toString.call(outputEscape), [], '', 0, 0);
2391
+ filters.push(new Filter(token, outputEscape, liquid));
2234
2392
  }
2235
2393
  }
2236
2394
  *render(ctx, emitter) {
2237
2395
  const val = yield this.value.value(ctx, false);
2238
2396
  emitter.write(val);
2239
2397
  }
2398
+ *arguments() {
2399
+ yield this.value;
2400
+ }
2240
2401
  }
2241
2402
 
2242
2403
  class HTML extends TemplateImpl {
@@ -2249,6 +2410,335 @@ class HTML extends TemplateImpl {
2249
2410
  }
2250
2411
  }
2251
2412
 
2413
+ /**
2414
+ * A variable's segments and location, which can be coerced to a string.
2415
+ */
2416
+ class Variable {
2417
+ constructor(segments, location) {
2418
+ this.segments = segments;
2419
+ this.location = location;
2420
+ }
2421
+ toString() {
2422
+ return segmentsString(this.segments, true);
2423
+ }
2424
+ /** Return this variable's segments as an array, possibly with nested arrays for nested paths. */
2425
+ toArray() {
2426
+ function* _visit(...segments) {
2427
+ for (const segment of segments) {
2428
+ if (segment instanceof Variable) {
2429
+ yield Array.from(_visit(...segment.segments));
2430
+ }
2431
+ else {
2432
+ yield segment;
2433
+ }
2434
+ }
2435
+ }
2436
+ return Array.from(_visit(...this.segments));
2437
+ }
2438
+ }
2439
+ /**
2440
+ * Group variables by the string representation of their root segment.
2441
+ */
2442
+ class VariableMap {
2443
+ constructor() {
2444
+ this.map = new Map();
2445
+ }
2446
+ get(key) {
2447
+ const k = segmentsString([key.segments[0]]);
2448
+ if (!this.map.has(k)) {
2449
+ this.map.set(k, []);
2450
+ }
2451
+ return this.map.get(k);
2452
+ }
2453
+ has(key) {
2454
+ return this.map.has(segmentsString([key.segments[0]]));
2455
+ }
2456
+ push(variable) {
2457
+ this.get(variable).push(variable);
2458
+ }
2459
+ asObject() {
2460
+ return Object.fromEntries(this.map);
2461
+ }
2462
+ }
2463
+ const defaultStaticAnalysisOptions = {
2464
+ partials: true
2465
+ };
2466
+ function* _analyze(templates, partials, sync) {
2467
+ const variables = new VariableMap();
2468
+ const globals = new VariableMap();
2469
+ const locals = new VariableMap();
2470
+ const rootScope = new DummyScope(new Set());
2471
+ // Names of partial templates that we've already analyzed.
2472
+ const seen = new Set();
2473
+ function updateVariables(variable, scope) {
2474
+ variables.push(variable);
2475
+ const aliased = scope.alias(variable);
2476
+ if (aliased !== undefined) {
2477
+ const root = aliased.segments[0];
2478
+ // TODO: What if a a template renders a rendered template? Do we need scope.parent?
2479
+ if (isString(root) && !rootScope.has(root)) {
2480
+ globals.push(aliased);
2481
+ }
2482
+ }
2483
+ else {
2484
+ const root = variable.segments[0];
2485
+ if (isString(root) && !scope.has(root)) {
2486
+ globals.push(variable);
2487
+ }
2488
+ }
2489
+ // Recurse for nested Variables
2490
+ for (const segment of variable.segments) {
2491
+ if (segment instanceof Variable) {
2492
+ updateVariables(segment, scope);
2493
+ }
2494
+ }
2495
+ }
2496
+ function* visit(template, scope) {
2497
+ if (template.arguments) {
2498
+ for (const arg of template.arguments()) {
2499
+ for (const variable of extractVariables(arg)) {
2500
+ updateVariables(variable, scope);
2501
+ }
2502
+ }
2503
+ }
2504
+ if (template.localScope) {
2505
+ for (const ident of template.localScope()) {
2506
+ scope.add(ident.content);
2507
+ scope.deleteAlias(ident.content);
2508
+ const [row, col] = ident.getPosition();
2509
+ locals.push(new Variable([ident.content], { row, col, file: ident.file }));
2510
+ }
2511
+ }
2512
+ if (template.children) {
2513
+ if (template.partialScope) {
2514
+ const partial = template.partialScope();
2515
+ if (partial === undefined) {
2516
+ // Layouts, for example, can have children that are not partials.
2517
+ for (const child of (yield template.children(partials, sync))) {
2518
+ yield visit(child, scope);
2519
+ }
2520
+ return;
2521
+ }
2522
+ if (seen.has(partial.name))
2523
+ return;
2524
+ const partialScopeNames = new Set();
2525
+ const partialScope = partial.isolated
2526
+ ? new DummyScope(partialScopeNames)
2527
+ : scope.push(partialScopeNames);
2528
+ for (const name of partial.scope) {
2529
+ if (isString(name)) {
2530
+ partialScopeNames.add(name);
2531
+ }
2532
+ else {
2533
+ const [alias, argument] = name;
2534
+ partialScopeNames.add(alias);
2535
+ const variables = Array.from(extractVariables(argument));
2536
+ if (variables.length) {
2537
+ partialScope.setAlias(alias, variables[0].segments);
2538
+ }
2539
+ }
2540
+ }
2541
+ for (const child of (yield template.children(partials, sync))) {
2542
+ yield visit(child, partialScope);
2543
+ seen.add(partial.name);
2544
+ }
2545
+ partialScope.pop();
2546
+ }
2547
+ else {
2548
+ if (template.blockScope) {
2549
+ scope.push(new Set(template.blockScope()));
2550
+ }
2551
+ for (const child of (yield template.children(partials, sync))) {
2552
+ yield visit(child, scope);
2553
+ }
2554
+ if (template.blockScope) {
2555
+ scope.pop();
2556
+ }
2557
+ }
2558
+ }
2559
+ }
2560
+ for (const template of templates) {
2561
+ yield visit(template, rootScope);
2562
+ }
2563
+ return {
2564
+ variables: variables.asObject(),
2565
+ globals: globals.asObject(),
2566
+ locals: locals.asObject()
2567
+ };
2568
+ }
2569
+ /**
2570
+ * Statically analyze a template and report variable usage.
2571
+ */
2572
+ function analyze(template, options = {}) {
2573
+ const opts = { ...defaultStaticAnalysisOptions, ...options };
2574
+ return toPromise(_analyze(template, opts.partials, false));
2575
+ }
2576
+ /**
2577
+ * Statically analyze a template and report variable usage.
2578
+ */
2579
+ function analyzeSync(template, options = {}) {
2580
+ const opts = { ...defaultStaticAnalysisOptions, ...options };
2581
+ return toValueSync(_analyze(template, opts.partials, true));
2582
+ }
2583
+ /**
2584
+ * A stack to manage scopes while traversing templates during static analysis.
2585
+ */
2586
+ class DummyScope {
2587
+ constructor(globals) {
2588
+ this.stack = [{ names: globals, aliases: new Map() }];
2589
+ }
2590
+ /** Return true if `name` is in scope. */
2591
+ has(name) {
2592
+ for (const scope of this.stack) {
2593
+ if (scope.names.has(name)) {
2594
+ return true;
2595
+ }
2596
+ }
2597
+ return false;
2598
+ }
2599
+ push(scope) {
2600
+ this.stack.push({ names: scope, aliases: new Map() });
2601
+ return this;
2602
+ }
2603
+ pop() {
2604
+ return this.stack.pop()?.names;
2605
+ }
2606
+ // Add a name to the template scope.
2607
+ add(name) {
2608
+ this.stack[0].names.add(name);
2609
+ }
2610
+ /** Return the variable that `variable` aliases, or `variable` if it doesn't alias anything. */
2611
+ alias(variable) {
2612
+ const root = variable.segments[0];
2613
+ if (!isString(root))
2614
+ return undefined;
2615
+ const alias = this.getAlias(root);
2616
+ if (alias === undefined)
2617
+ return undefined;
2618
+ return new Variable([...alias, ...variable.segments.slice(1)], variable.location);
2619
+ }
2620
+ // TODO: `from` could be a path with multiple segments, like `include.x`.
2621
+ setAlias(from, to) {
2622
+ this.stack[this.stack.length - 1].aliases.set(from, to);
2623
+ }
2624
+ deleteAlias(name) {
2625
+ this.stack[this.stack.length - 1].aliases.delete(name);
2626
+ }
2627
+ getAlias(name) {
2628
+ for (const scope of this.stack) {
2629
+ if (scope.aliases.has(name)) {
2630
+ return scope.aliases.get(name);
2631
+ }
2632
+ // If a scope has defined `name`, then it masks aliases in parent scopes.
2633
+ if (scope.names.has(name)) {
2634
+ return undefined;
2635
+ }
2636
+ }
2637
+ return undefined;
2638
+ }
2639
+ }
2640
+ function* extractVariables(value) {
2641
+ if (isValueToken(value)) {
2642
+ yield* extractValueTokenVariables(value);
2643
+ }
2644
+ else if (value instanceof Value) {
2645
+ yield* extractFilteredValueVariables(value);
2646
+ }
2647
+ }
2648
+ function* extractFilteredValueVariables(value) {
2649
+ for (const token of value.initial.postfix) {
2650
+ if (isValueToken(token)) {
2651
+ yield* extractValueTokenVariables(token);
2652
+ }
2653
+ }
2654
+ for (const filter of value.filters) {
2655
+ for (const arg of filter.args) {
2656
+ if (isKeyValuePair(arg) && arg[1]) {
2657
+ yield* extractValueTokenVariables(arg[1]);
2658
+ }
2659
+ else if (isValueToken(arg)) {
2660
+ yield* extractValueTokenVariables(arg);
2661
+ }
2662
+ }
2663
+ }
2664
+ }
2665
+ function* extractValueTokenVariables(token) {
2666
+ if (isRangeToken(token)) {
2667
+ yield* extractValueTokenVariables(token.lhs);
2668
+ yield* extractValueTokenVariables(token.rhs);
2669
+ }
2670
+ else if (isPropertyAccessToken(token)) {
2671
+ yield extractPropertyAccessVariable(token);
2672
+ }
2673
+ }
2674
+ function extractPropertyAccessVariable(token) {
2675
+ const segments = [];
2676
+ // token is not guaranteed to have `file` set. We'll try to get it from a prop if not.
2677
+ let file = token.file;
2678
+ // Here we're flattening the first segment of a path if it is a nested path.
2679
+ const root = token.props[0];
2680
+ file = file || root.file;
2681
+ if (isQuotedToken(root) || isNumberToken(root) || isWordToken(root)) {
2682
+ segments.push(root.content);
2683
+ }
2684
+ else if (isPropertyAccessToken(root)) {
2685
+ // Flatten paths that start with a nested path.
2686
+ segments.push(...extractPropertyAccessVariable(root).segments);
2687
+ }
2688
+ for (const prop of token.props.slice(1)) {
2689
+ file = file || prop.file;
2690
+ if (isQuotedToken(prop) || isNumberToken(prop) || isWordToken(prop)) {
2691
+ segments.push(prop.content);
2692
+ }
2693
+ else if (isPropertyAccessToken(prop)) {
2694
+ segments.push(extractPropertyAccessVariable(prop));
2695
+ }
2696
+ }
2697
+ const [row, col] = token.getPosition();
2698
+ return new Variable(segments, {
2699
+ row,
2700
+ col,
2701
+ file
2702
+ });
2703
+ }
2704
+ // This is used to detect segments that can be represented with dot notation
2705
+ // when creating a string representation of VariableSegments.
2706
+ const RE_PROPERTY = /^[\u0080-\uFFFFa-zA-Z_][\u0080-\uFFFFa-zA-Z0-9_-]*$/;
2707
+ /**
2708
+ * Return a string representation of segments using dot notation where possible.
2709
+ * @param segments - The property names and array indices that make up a path to a variable.
2710
+ * @param bracketedRoot - If false (the default), don't surround the root segment with square brackets.
2711
+ */
2712
+ function segmentsString(segments, bracketedRoot = false) {
2713
+ const buf = [];
2714
+ const root = segments[0];
2715
+ if (isString(root)) {
2716
+ if (!bracketedRoot || root.match(RE_PROPERTY)) {
2717
+ buf.push(`${root}`);
2718
+ }
2719
+ else {
2720
+ buf.push(`['${root}']`);
2721
+ }
2722
+ }
2723
+ for (const segment of segments.slice(1)) {
2724
+ if (segment instanceof Variable) {
2725
+ buf.push(`[${segmentsString(segment.segments)}]`);
2726
+ }
2727
+ else if (isString(segment)) {
2728
+ if (segment.match(RE_PROPERTY)) {
2729
+ buf.push(`.${segment}`);
2730
+ }
2731
+ else {
2732
+ buf.push(`['${segment}']`);
2733
+ }
2734
+ }
2735
+ else {
2736
+ buf.push(`[${segment}]`);
2737
+ }
2738
+ }
2739
+ return buf.join('');
2740
+ }
2741
+
2252
2742
  var LookupType;
2253
2743
  (function (LookupType) {
2254
2744
  LookupType["Partials"] = "partials";
@@ -2265,7 +2755,7 @@ class Loader {
2265
2755
  this.shouldLoadRelative = (referencedFile) => rRelativePath.test(referencedFile);
2266
2756
  }
2267
2757
  else {
2268
- this.shouldLoadRelative = (referencedFile) => false;
2758
+ this.shouldLoadRelative = (_referencedFile) => false;
2269
2759
  }
2270
2760
  this.contains = this.options.fs.contains || (() => true);
2271
2761
  }
@@ -2322,8 +2812,11 @@ class Parser {
2322
2812
  this.fs = this.liquid.options.fs;
2323
2813
  this.parseFile = this.cache ? this._parseFileCached : this._parseFile;
2324
2814
  this.loader = new Loader(this.liquid.options);
2815
+ this.parseLimit = new Limiter('parse length', liquid.options.parseLimit);
2325
2816
  }
2326
2817
  parse(html, filepath) {
2818
+ html = String(html);
2819
+ this.parseLimit.use(html.length);
2327
2820
  const tokenizer = new Tokenizer(html, this.liquid.options.operators, filepath);
2328
2821
  const tokens = tokenizer.readTopLevelTokens(this.liquid.options);
2329
2822
  return this.parseTokens(tokens);
@@ -2331,9 +2824,20 @@ class Parser {
2331
2824
  parseTokens(tokens) {
2332
2825
  let token;
2333
2826
  const templates = [];
2827
+ const errors = [];
2334
2828
  while ((token = tokens.shift())) {
2335
- templates.push(this.parseToken(token, tokens));
2829
+ try {
2830
+ templates.push(this.parseToken(token, tokens));
2831
+ }
2832
+ catch (err) {
2833
+ if (this.liquid.options.catchAllErrors)
2834
+ errors.push(err);
2835
+ else
2836
+ throw err;
2837
+ }
2336
2838
  }
2839
+ if (errors.length)
2840
+ throw new LiquidErrors(errors);
2337
2841
  return templates;
2338
2842
  }
2339
2843
  parseToken(token, remainTokens) {
@@ -2341,7 +2845,7 @@ class Parser {
2341
2845
  if (isTagToken(token)) {
2342
2846
  const TagClass = this.liquid.tags[token.name];
2343
2847
  assert(TagClass, `tag "${token.name}" not found`);
2344
- return new TagClass(token, remainTokens, this.liquid);
2848
+ return new TagClass(token, remainTokens, this.liquid, this);
2345
2849
  }
2346
2850
  if (isOutputToken(token)) {
2347
2851
  return new Output(token, this.liquid);
@@ -2379,7 +2883,7 @@ class Parser {
2379
2883
  }
2380
2884
  *_parseFile(file, sync, type = LookupType.Root, currentFile) {
2381
2885
  const filepath = yield this.loader.lookup(file, type, sync, currentFile);
2382
- return this.liquid.parse(sync ? this.fs.readFileSync(filepath) : yield this.fs.readFile(filepath), filepath);
2886
+ return this.parse(sync ? this.fs.readFileSync(filepath) : yield this.fs.readFile(filepath), filepath);
2383
2887
  }
2384
2888
  }
2385
2889
 
@@ -2433,6 +2937,10 @@ function isWordToken(val) {
2433
2937
  function isRangeToken(val) {
2434
2938
  return getKind(val) === exports.TokenKind.Range;
2435
2939
  }
2940
+ function isValueToken(val) {
2941
+ // valueTokenBitMask = TokenKind.Number | TokenKind.Literal | TokenKind.Quoted | TokenKind.PropertyAccess | TokenKind.Range
2942
+ return (getKind(val) & 1667) > 0;
2943
+ }
2436
2944
  function getKind(val) {
2437
2945
  return val ? val.kind : -1;
2438
2946
  }
@@ -2449,7 +2957,8 @@ var typeGuards = /*#__PURE__*/Object.freeze({
2449
2957
  isNumberToken: isNumberToken,
2450
2958
  isPropertyAccessToken: isPropertyAccessToken,
2451
2959
  isWordToken: isWordToken,
2452
- isRangeToken: isRangeToken
2960
+ isRangeToken: isRangeToken,
2961
+ isValueToken: isValueToken
2453
2962
  });
2454
2963
 
2455
2964
  /******************************************************************************
@@ -2479,19 +2988,23 @@ var __assign = function() {
2479
2988
  };
2480
2989
 
2481
2990
  class Context {
2482
- constructor(env = {}, opts = defaultOptions, renderOptions = {}) {
2991
+ constructor(env = {}, opts = defaultOptions, renderOptions = {}, { memoryLimit, renderLimit } = {}) {
2483
2992
  /**
2484
2993
  * insert a Context-level empty scope,
2485
2994
  * for tags like `{% capture %}` `{% assign %}` to operate
2486
2995
  */
2487
2996
  this.scopes = [{}];
2488
2997
  this.registers = {};
2998
+ this.breakCalled = false;
2999
+ this.continueCalled = false;
2489
3000
  this.sync = !!renderOptions.sync;
2490
3001
  this.opts = opts;
2491
3002
  this.globals = renderOptions.globals ?? opts.globals;
2492
3003
  this.environments = isObject(env) ? env : Object(env);
2493
3004
  this.strictVariables = renderOptions.strictVariables ?? this.opts.strictVariables;
2494
3005
  this.ownPropertyOnly = renderOptions.ownPropertyOnly ?? opts.ownPropertyOnly;
3006
+ this.memoryLimit = memoryLimit ?? new Limiter('memory alloc', renderOptions.memoryLimit ?? opts.memoryLimit);
3007
+ this.renderLimit = renderLimit ?? new Limiter('template render', getPerformance().now() + (renderOptions.renderLimit ?? opts.renderLimit));
2495
3008
  }
2496
3009
  getRegister(key) {
2497
3010
  return (this.registers[key] = this.registers[key] || {});
@@ -2519,7 +3032,7 @@ class Context {
2519
3032
  return toValueSync(this._get(paths));
2520
3033
  }
2521
3034
  *_get(paths) {
2522
- const scope = this.findScope(paths[0]);
3035
+ const scope = this.findScope(paths[0]); // first prop should always be a string
2523
3036
  return yield this._getFromScope(scope, paths);
2524
3037
  }
2525
3038
  /**
@@ -2548,6 +3061,16 @@ class Context {
2548
3061
  bottom() {
2549
3062
  return this.scopes[0];
2550
3063
  }
3064
+ spawn(scope = {}) {
3065
+ return new Context(scope, this.opts, {
3066
+ sync: this.sync,
3067
+ globals: this.globals,
3068
+ strictVariables: this.strictVariables
3069
+ }, {
3070
+ renderLimit: this.renderLimit,
3071
+ memoryLimit: this.memoryLimit
3072
+ });
3073
+ }
2551
3074
  findScope(key) {
2552
3075
  for (let i = this.scopes.length - 1; i >= 0; i--) {
2553
3076
  const candidate = this.scopes[i];
@@ -2561,6 +3084,7 @@ class Context {
2561
3084
  }
2562
3085
  function readProperty(obj, key, ownPropertyOnly) {
2563
3086
  obj = toLiquid(obj);
3087
+ key = toValue(key);
2564
3088
  if (isNil(obj))
2565
3089
  return obj;
2566
3090
  if (isArray(obj) && key < 0)
@@ -2579,7 +3103,7 @@ function readProperty(obj, key, ownPropertyOnly) {
2579
3103
  return value;
2580
3104
  }
2581
3105
  function readJSProperty(obj, key, ownPropertyOnly) {
2582
- if (ownPropertyOnly && !Object.hasOwnProperty.call(obj, key) && !(obj instanceof Drop))
3106
+ if (ownPropertyOnly && !hasOwnProperty.call(obj, key) && !(obj instanceof Drop))
2583
3107
  return undefined;
2584
3108
  return obj[key];
2585
3109
  }
@@ -2594,7 +3118,7 @@ function readLast(obj) {
2594
3118
  return obj['last'];
2595
3119
  }
2596
3120
  function readSize(obj) {
2597
- if (obj.hasOwnProperty('size') || obj['size'] !== undefined)
3121
+ if (hasOwnProperty.call(obj, 'size') || obj['size'] !== undefined)
2598
3122
  return obj['size'];
2599
3123
  if (isArray(obj) || isString(obj))
2600
3124
  return obj.length;
@@ -2701,13 +3225,25 @@ var urlFilters = /*#__PURE__*/Object.freeze({
2701
3225
  slugify: slugify
2702
3226
  });
2703
3227
 
2704
- const join = argumentsToValue((v, arg) => toArray(v).join(arg === undefined ? ' ' : arg));
2705
- const last$1 = argumentsToValue((v) => isArray(v) ? last(v) : '');
2706
- const first = argumentsToValue((v) => isArray(v) ? v[0] : '');
2707
- const reverse = argumentsToValue((v) => [...toArray(v)].reverse());
3228
+ const join = argumentsToValue(function (v, arg) {
3229
+ const array = toArray(v);
3230
+ const sep = isNil(arg) ? ' ' : stringify(arg);
3231
+ const complexity = array.length * (1 + sep.length);
3232
+ this.context.memoryLimit.use(complexity);
3233
+ return array.join(sep);
3234
+ });
3235
+ const last$1 = argumentsToValue((v) => isArrayLike(v) ? last(v) : '');
3236
+ const first = argumentsToValue((v) => isArrayLike(v) ? v[0] : '');
3237
+ const reverse = argumentsToValue(function (v) {
3238
+ const array = toArray(v);
3239
+ this.context.memoryLimit.use(array.length);
3240
+ return [...array].reverse();
3241
+ });
2708
3242
  function* sort(arr, property) {
2709
3243
  const values = [];
2710
- for (const item of toArray(arr)) {
3244
+ const array = toArray(arr);
3245
+ this.context.memoryLimit.use(array.length);
3246
+ for (const item of array) {
2711
3247
  values.push([
2712
3248
  item,
2713
3249
  property ? yield this.context._getFromScope(item, stringify(property).split('.'), false) : item
@@ -2724,35 +3260,47 @@ function sort_natural(input, property) {
2724
3260
  const compare = property === undefined
2725
3261
  ? caseInsensitiveCompare
2726
3262
  : (lhs, rhs) => caseInsensitiveCompare(lhs[propertyString], rhs[propertyString]);
2727
- return [...toArray(input)].sort(compare);
3263
+ const array = toArray(input);
3264
+ this.context.memoryLimit.use(array.length);
3265
+ return [...array].sort(compare);
2728
3266
  }
2729
3267
  const size = (v) => (v && v.length) || 0;
2730
3268
  function* map(arr, property) {
2731
3269
  const results = [];
2732
- for (const item of toArray(arr)) {
3270
+ const array = toArray(arr);
3271
+ this.context.memoryLimit.use(array.length);
3272
+ for (const item of array) {
2733
3273
  results.push(yield this.context._getFromScope(item, stringify(property), false));
2734
3274
  }
2735
3275
  return results;
2736
3276
  }
2737
3277
  function* sum(arr, property) {
2738
3278
  let sum = 0;
2739
- for (const item of toArray(arr)) {
3279
+ const array = toArray(arr);
3280
+ for (const item of array) {
2740
3281
  const data = Number(property ? yield this.context._getFromScope(item, stringify(property), false) : item);
2741
3282
  sum += Number.isNaN(data) ? 0 : data;
2742
3283
  }
2743
3284
  return sum;
2744
3285
  }
2745
3286
  function compact(arr) {
2746
- return toArray(arr).filter(x => !isNil(toValue(x)));
3287
+ const array = toArray(arr);
3288
+ this.context.memoryLimit.use(array.length);
3289
+ return array.filter(x => !isNil(toValue(x)));
2747
3290
  }
2748
3291
  function concat(v, arg = []) {
2749
- return toArray(v).concat(toArray(arg));
3292
+ const lhs = toArray(v);
3293
+ const rhs = toArray(arg);
3294
+ this.context.memoryLimit.use(lhs.length + rhs.length);
3295
+ return lhs.concat(rhs);
2750
3296
  }
2751
3297
  function push(v, arg) {
2752
- return concat(v, [arg]);
3298
+ return concat.call(this, v, [arg]);
2753
3299
  }
2754
3300
  function unshift(v, arg) {
2755
- const clone = [...toArray(v)];
3301
+ const array = toArray(v);
3302
+ this.context.memoryLimit.use(array.length);
3303
+ const clone = [...array];
2756
3304
  clone.unshift(arg);
2757
3305
  return clone;
2758
3306
  }
@@ -2762,7 +3310,9 @@ function pop(v) {
2762
3310
  return clone;
2763
3311
  }
2764
3312
  function shift(v) {
2765
- const clone = [...toArray(v)];
3313
+ const array = toArray(v);
3314
+ this.context.memoryLimit.use(array.length);
3315
+ const clone = [...array];
2766
3316
  clone.shift();
2767
3317
  return clone;
2768
3318
  }
@@ -2773,26 +3323,33 @@ function slice(v, begin, length = 1) {
2773
3323
  if (!isArray(v))
2774
3324
  v = stringify(v);
2775
3325
  begin = begin < 0 ? v.length + begin : begin;
3326
+ this.context.memoryLimit.use(length);
2776
3327
  return v.slice(begin, begin + length);
2777
3328
  }
2778
3329
  function* where(arr, property, expected) {
2779
3330
  const values = [];
2780
3331
  arr = toArray(arr);
3332
+ this.context.memoryLimit.use(arr.length);
2781
3333
  const token = new Tokenizer(stringify(property)).readScopeValue();
2782
3334
  for (const item of arr) {
2783
- values.push(yield evalToken(token, new Context(item)));
3335
+ values.push(yield evalToken(token, this.context.spawn(item)));
2784
3336
  }
3337
+ const matcher = this.context.opts.jekyllWhere
3338
+ ? (v) => EmptyDrop.is(expected) ? equals(v, expected) : (isArray(v) ? arrayIncludes(v, expected) : equals(v, expected))
3339
+ : (v) => equals(v, expected);
2785
3340
  return arr.filter((_, i) => {
2786
3341
  if (expected === undefined)
2787
3342
  return isTruthy(values[i], this.context);
2788
- return equals(values[i], expected);
3343
+ return matcher(values[i]);
2789
3344
  });
2790
3345
  }
2791
3346
  function* where_exp(arr, itemName, exp) {
2792
3347
  const filtered = [];
2793
3348
  const keyTemplate = new Value(stringify(exp), this.liquid);
2794
- for (const item of toArray(arr)) {
2795
- const value = yield keyTemplate.value(new Context({ [itemName]: item }));
3349
+ const array = toArray(arr);
3350
+ this.context.memoryLimit.use(array.length);
3351
+ for (const item of array) {
3352
+ const value = yield keyTemplate.value(this.context.spawn({ [itemName]: item }));
2796
3353
  if (value)
2797
3354
  filtered.push(item);
2798
3355
  }
@@ -2800,10 +3357,11 @@ function* where_exp(arr, itemName, exp) {
2800
3357
  }
2801
3358
  function* group_by(arr, property) {
2802
3359
  const map = new Map();
2803
- arr = toArray(arr);
3360
+ arr = toEnumerable(arr);
2804
3361
  const token = new Tokenizer(stringify(property)).readScopeValue();
3362
+ this.context.memoryLimit.use(arr.length);
2805
3363
  for (const item of arr) {
2806
- const key = yield evalToken(token, new Context(item));
3364
+ const key = yield evalToken(token, this.context.spawn(item));
2807
3365
  if (!map.has(key))
2808
3366
  map.set(key, []);
2809
3367
  map.get(key).push(item);
@@ -2813,8 +3371,10 @@ function* group_by(arr, property) {
2813
3371
  function* group_by_exp(arr, itemName, exp) {
2814
3372
  const map = new Map();
2815
3373
  const keyTemplate = new Value(stringify(exp), this.liquid);
2816
- for (const item of toArray(arr)) {
2817
- const key = yield keyTemplate.value(new Context({ [itemName]: item }));
3374
+ arr = toEnumerable(arr);
3375
+ this.context.memoryLimit.use(arr.length);
3376
+ for (const item of arr) {
3377
+ const key = yield keyTemplate.value(this.context.spawn({ [itemName]: item }));
2818
3378
  if (!map.has(key))
2819
3379
  map.set(key, []);
2820
3380
  map.get(key).push(item);
@@ -2823,31 +3383,26 @@ function* group_by_exp(arr, itemName, exp) {
2823
3383
  }
2824
3384
  function* find(arr, property, expected) {
2825
3385
  const token = new Tokenizer(stringify(property)).readScopeValue();
2826
- for (const item of toArray(arr)) {
2827
- const value = yield evalToken(token, new Context(item));
3386
+ const array = toArray(arr);
3387
+ for (const item of array) {
3388
+ const value = yield evalToken(token, this.context.spawn(item));
2828
3389
  if (equals(value, expected))
2829
3390
  return item;
2830
3391
  }
2831
- return null;
2832
3392
  }
2833
3393
  function* find_exp(arr, itemName, exp) {
2834
3394
  const predicate = new Value(stringify(exp), this.liquid);
2835
- for (const item of toArray(arr)) {
2836
- const value = yield predicate.value(new Context({ [itemName]: item }));
3395
+ const array = toArray(arr);
3396
+ for (const item of array) {
3397
+ const value = yield predicate.value(this.context.spawn({ [itemName]: item }));
2837
3398
  if (value)
2838
3399
  return item;
2839
3400
  }
2840
- return null;
2841
3401
  }
2842
3402
  function uniq(arr) {
2843
- arr = toValue(arr);
2844
- const u = {};
2845
- return (arr || []).filter(val => {
2846
- if (hasOwnProperty.call(u, String(val)))
2847
- return false;
2848
- u[String(val)] = true;
2849
- return true;
2850
- });
3403
+ arr = toArray(arr);
3404
+ this.context.memoryLimit.use(arr.length);
3405
+ return [...new Set(arr)];
2851
3406
  }
2852
3407
  function sample(v, count = 1) {
2853
3408
  v = toValue(v);
@@ -2855,6 +3410,7 @@ function sample(v, count = 1) {
2855
3410
  return [];
2856
3411
  if (!isArray(v))
2857
3412
  v = stringify(v);
3413
+ this.context.memoryLimit.use(count);
2858
3414
  const shuffled = [...v].sort(() => Math.random() - 0.5);
2859
3415
  if (count === 1)
2860
3416
  return shuffled[0];
@@ -2890,6 +3446,8 @@ var arrayFilters = /*#__PURE__*/Object.freeze({
2890
3446
  });
2891
3447
 
2892
3448
  function date(v, format, timezoneOffset) {
3449
+ const size = (v?.length ?? 0) + (format?.length ?? 0) + (timezoneOffset?.length ?? 0);
3450
+ this.context.memoryLimit.use(size);
2893
3451
  const date = parseDate(v, this.context.opts, timezoneOffset);
2894
3452
  if (!date)
2895
3453
  return v;
@@ -2923,39 +3481,30 @@ function stringify_date(v, month_type, type, style) {
2923
3481
  }
2924
3482
  function parseDate(v, opts, timezoneOffset) {
2925
3483
  let date;
3484
+ const defaultTimezoneOffset = timezoneOffset ?? opts.timezoneOffset;
3485
+ const locale = opts.locale;
2926
3486
  v = toValue(v);
2927
3487
  if (v === 'now' || v === 'today') {
2928
- date = new Date();
3488
+ date = new LiquidDate(Date.now(), locale, defaultTimezoneOffset);
2929
3489
  }
2930
3490
  else if (isNumber(v)) {
2931
- date = new Date(v * 1000);
3491
+ date = new LiquidDate(v * 1000, locale, defaultTimezoneOffset);
2932
3492
  }
2933
3493
  else if (isString(v)) {
2934
3494
  if (/^\d+$/.test(v)) {
2935
- date = new Date(+v * 1000);
3495
+ date = new LiquidDate(+v * 1000, locale, defaultTimezoneOffset);
2936
3496
  }
2937
- else if (opts.preserveTimezones) {
2938
- date = TimezoneDate.createDateFixedToTimezone(v);
3497
+ else if (opts.preserveTimezones && timezoneOffset === undefined) {
3498
+ date = LiquidDate.createDateFixedToTimezone(v, locale);
2939
3499
  }
2940
3500
  else {
2941
- date = new Date(v);
3501
+ date = new LiquidDate(v, locale, defaultTimezoneOffset);
2942
3502
  }
2943
3503
  }
2944
3504
  else {
2945
- date = v;
2946
- }
2947
- if (!isValidDate(date))
2948
- return;
2949
- if (timezoneOffset !== undefined) {
2950
- date = new TimezoneDate(date, timezoneOffset);
2951
- }
2952
- else if (!(date instanceof TimezoneDate) && opts.timezoneOffset !== undefined) {
2953
- date = new TimezoneDate(date, opts.timezoneOffset);
3505
+ date = new LiquidDate(v, locale, defaultTimezoneOffset);
2954
3506
  }
2955
- return date;
2956
- }
2957
- function isValidDate(date) {
2958
- return (date instanceof Date || date instanceof TimezoneDate) && !isNaN(date.getTime());
3507
+ return date.valid() ? date : undefined;
2959
3508
  }
2960
3509
 
2961
3510
  var dateFilters = /*#__PURE__*/Object.freeze({
@@ -2977,33 +3526,50 @@ const rCJKWord = /[\u4E00-\u9FFF\uF900-\uFAFF\u3400-\u4DBF\u3040-\u309F\u30A0-\u
2977
3526
  const rNonCJKWord = /[^\u4E00-\u9FFF\uF900-\uFAFF\u3400-\u4DBF\u3040-\u309F\u30A0-\u30FF\uAC00-\uD7AF\s]+/gu;
2978
3527
  function append(v, arg) {
2979
3528
  assert(arguments.length === 2, 'append expect 2 arguments');
2980
- return stringify(v) + stringify(arg);
3529
+ const lhs = stringify(v);
3530
+ const rhs = stringify(arg);
3531
+ this.context.memoryLimit.use(lhs.length + rhs.length);
3532
+ return lhs + rhs;
2981
3533
  }
2982
3534
  function prepend(v, arg) {
2983
3535
  assert(arguments.length === 2, 'prepend expect 2 arguments');
2984
- return stringify(arg) + stringify(v);
3536
+ const lhs = stringify(v);
3537
+ const rhs = stringify(arg);
3538
+ this.context.memoryLimit.use(lhs.length + rhs.length);
3539
+ return rhs + lhs;
2985
3540
  }
2986
3541
  function lstrip(v, chars) {
3542
+ const str = stringify(v);
3543
+ this.context.memoryLimit.use(str.length);
2987
3544
  if (chars) {
2988
3545
  chars = escapeRegExp(stringify(chars));
2989
- return stringify(v).replace(new RegExp(`^[${chars}]+`, 'g'), '');
3546
+ return str.replace(new RegExp(`^[${chars}]+`, 'g'), '');
2990
3547
  }
2991
- return stringify(v).replace(/^\s+/, '');
3548
+ return str.replace(/^\s+/, '');
2992
3549
  }
2993
3550
  function downcase(v) {
2994
- return stringify(v).toLowerCase();
3551
+ const str = stringify(v);
3552
+ this.context.memoryLimit.use(str.length);
3553
+ return str.toLowerCase();
2995
3554
  }
2996
- function upcase(str) {
3555
+ function upcase(v) {
3556
+ const str = stringify(v);
3557
+ this.context.memoryLimit.use(str.length);
2997
3558
  return stringify(str).toUpperCase();
2998
3559
  }
2999
3560
  function remove(v, arg) {
3000
- return stringify(v).split(stringify(arg)).join('');
3561
+ const str = stringify(v);
3562
+ this.context.memoryLimit.use(str.length);
3563
+ return str.split(stringify(arg)).join('');
3001
3564
  }
3002
3565
  function remove_first(v, l) {
3003
- return stringify(v).replace(stringify(l), '');
3566
+ const str = stringify(v);
3567
+ this.context.memoryLimit.use(str.length);
3568
+ return str.replace(stringify(l), '');
3004
3569
  }
3005
3570
  function remove_last(v, l) {
3006
3571
  const str = stringify(v);
3572
+ this.context.memoryLimit.use(str.length);
3007
3573
  const pattern = stringify(l);
3008
3574
  const index = str.lastIndexOf(pattern);
3009
3575
  if (index === -1)
@@ -3011,14 +3577,18 @@ function remove_last(v, l) {
3011
3577
  return str.substring(0, index) + str.substring(index + pattern.length);
3012
3578
  }
3013
3579
  function rstrip(str, chars) {
3580
+ str = stringify(str);
3581
+ this.context.memoryLimit.use(str.length);
3014
3582
  if (chars) {
3015
3583
  chars = escapeRegExp(stringify(chars));
3016
- return stringify(str).replace(new RegExp(`[${chars}]+$`, 'g'), '');
3584
+ return str.replace(new RegExp(`[${chars}]+$`, 'g'), '');
3017
3585
  }
3018
- return stringify(str).replace(/\s+$/, '');
3586
+ return str.replace(/\s+$/, '');
3019
3587
  }
3020
3588
  function split(v, arg) {
3021
- const arr = stringify(v).split(stringify(arg));
3589
+ const str = stringify(v);
3590
+ this.context.memoryLimit.use(str.length);
3591
+ const arr = str.split(stringify(arg));
3022
3592
  // align to ruby split, which is the behavior of shopify/liquid
3023
3593
  // see: https://ruby-doc.org/core-2.4.0/String.html#method-i-split
3024
3594
  while (arr.length && arr[arr.length - 1] === '')
@@ -3026,29 +3596,39 @@ function split(v, arg) {
3026
3596
  return arr;
3027
3597
  }
3028
3598
  function strip(v, chars) {
3599
+ const str = stringify(v);
3600
+ this.context.memoryLimit.use(str.length);
3029
3601
  if (chars) {
3030
3602
  chars = escapeRegExp(stringify(chars));
3031
- return stringify(v)
3603
+ return str
3032
3604
  .replace(new RegExp(`^[${chars}]+`, 'g'), '')
3033
3605
  .replace(new RegExp(`[${chars}]+$`, 'g'), '');
3034
3606
  }
3035
- return stringify(v).trim();
3607
+ return str.trim();
3036
3608
  }
3037
3609
  function strip_newlines(v) {
3038
- return stringify(v).replace(/\r?\n/gm, '');
3610
+ const str = stringify(v);
3611
+ this.context.memoryLimit.use(str.length);
3612
+ return str.replace(/\r?\n/gm, '');
3039
3613
  }
3040
3614
  function capitalize(str) {
3041
3615
  str = stringify(str);
3616
+ this.context.memoryLimit.use(str.length);
3042
3617
  return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
3043
3618
  }
3044
3619
  function replace(v, pattern, replacement) {
3045
- return stringify(v).split(stringify(pattern)).join(replacement);
3620
+ const str = stringify(v);
3621
+ this.context.memoryLimit.use(str.length);
3622
+ return str.split(stringify(pattern)).join(replacement);
3046
3623
  }
3047
3624
  function replace_first(v, arg1, arg2) {
3048
- return stringify(v).replace(stringify(arg1), arg2);
3625
+ const str = stringify(v);
3626
+ this.context.memoryLimit.use(str.length);
3627
+ return str.replace(stringify(arg1), arg2);
3049
3628
  }
3050
3629
  function replace_last(v, arg1, arg2) {
3051
3630
  const str = stringify(v);
3631
+ this.context.memoryLimit.use(str.length);
3052
3632
  const pattern = stringify(arg1);
3053
3633
  const index = str.lastIndexOf(pattern);
3054
3634
  if (index === -1)
@@ -3057,13 +3637,16 @@ function replace_last(v, arg1, arg2) {
3057
3637
  return str.substring(0, index) + replacement + str.substring(index + pattern.length);
3058
3638
  }
3059
3639
  function truncate(v, l = 50, o = '...') {
3060
- v = stringify(v);
3061
- if (v.length <= l)
3640
+ const str = stringify(v);
3641
+ this.context.memoryLimit.use(str.length);
3642
+ if (str.length <= l)
3062
3643
  return v;
3063
- return v.substring(0, l - o.length) + o;
3644
+ return str.substring(0, l - o.length) + o;
3064
3645
  }
3065
3646
  function truncatewords(v, words = 15, o = '...') {
3066
- const arr = stringify(v).split(/\s+/);
3647
+ const str = stringify(v);
3648
+ this.context.memoryLimit.use(str.length);
3649
+ const arr = str.split(/\s+/);
3067
3650
  if (words <= 0)
3068
3651
  words = 1;
3069
3652
  let ret = arr.slice(0, words).join(' ');
@@ -3072,11 +3655,14 @@ function truncatewords(v, words = 15, o = '...') {
3072
3655
  return ret;
3073
3656
  }
3074
3657
  function normalize_whitespace(v) {
3075
- v = stringify(v);
3076
- return v.replace(/\s+/g, ' ');
3658
+ const str = stringify(v);
3659
+ this.context.memoryLimit.use(str.length);
3660
+ return str.replace(/\s+/g, ' ');
3077
3661
  }
3078
3662
  function number_of_words(input, mode) {
3079
- input = stringify(input).trim();
3663
+ const str = stringify(input);
3664
+ this.context.memoryLimit.use(str.length);
3665
+ input = str.trim();
3080
3666
  if (!input)
3081
3667
  return 0;
3082
3668
  switch (mode) {
@@ -3094,6 +3680,7 @@ function number_of_words(input, mode) {
3094
3680
  }
3095
3681
  }
3096
3682
  function array_to_sentence_string(array, connector = 'and') {
3683
+ this.context.memoryLimit.use(array.length);
3097
3684
  switch (array.length) {
3098
3685
  case 0:
3099
3686
  return '';
@@ -3144,7 +3731,8 @@ const filters = {
3144
3731
  class AssignTag extends Tag {
3145
3732
  constructor(token, remainTokens, liquid) {
3146
3733
  super(token, remainTokens, liquid);
3147
- this.key = this.tokenizer.readIdentifier().content;
3734
+ this.identifier = this.tokenizer.readIdentifier();
3735
+ this.key = this.identifier.content;
3148
3736
  this.tokenizer.assert(this.key, 'expected variable name');
3149
3737
  this.tokenizer.skipBlank();
3150
3738
  this.tokenizer.assert(this.tokenizer.peek() === '=', 'expected "="');
@@ -3154,11 +3742,17 @@ class AssignTag extends Tag {
3154
3742
  *render(ctx) {
3155
3743
  ctx.bottom()[this.key] = yield this.value.value(ctx, this.liquid.options.lenientIf);
3156
3744
  }
3745
+ *arguments() {
3746
+ yield this.value;
3747
+ }
3748
+ *localScope() {
3749
+ yield this.identifier;
3750
+ }
3157
3751
  }
3158
3752
 
3159
3753
  const MODIFIERS = ['offset', 'limit', 'reversed'];
3160
3754
  class ForTag extends Tag {
3161
- constructor(token, remainTokens, liquid) {
3755
+ constructor(token, remainTokens, liquid, parser) {
3162
3756
  super(token, remainTokens, liquid);
3163
3757
  const variable = this.tokenizer.readIdentifier();
3164
3758
  const inStr = this.tokenizer.readIdentifier();
@@ -3168,18 +3762,16 @@ class ForTag extends Tag {
3168
3762
  }
3169
3763
  this.variable = variable.content;
3170
3764
  this.collection = collection;
3171
- this.hash = new Hash(this.tokenizer.remaining());
3765
+ this.hash = new Hash(this.tokenizer, liquid.options.keyValueSeparator);
3172
3766
  this.templates = [];
3173
3767
  this.elseTemplates = [];
3174
3768
  let p;
3175
- const stream = this.liquid.parser.parseStream(remainTokens)
3769
+ const stream = parser.parseStream(remainTokens)
3176
3770
  .on('start', () => (p = this.templates))
3177
- .on('tag:else', () => (p = this.elseTemplates))
3178
- .on('tag:endfor', () => stream.stop())
3771
+ .on('tag:else', tag => { assertEmpty(tag.args); p = this.elseTemplates; })
3772
+ .on('tag:endfor', tag => { assertEmpty(tag.args); stream.stop(); })
3179
3773
  .on('template', (tpl) => p.push(tpl))
3180
- .on('end', () => {
3181
- throw new Error(`tag ${token.getText()} not closed`);
3182
- });
3774
+ .on('end', () => { throw new Error(`tag ${token.getText()} not closed`); });
3183
3775
  stream.start();
3184
3776
  }
3185
3777
  *render(ctx, emitter) {
@@ -3208,16 +3800,33 @@ class ForTag extends Tag {
3208
3800
  ctx.push(scope);
3209
3801
  for (const item of collection) {
3210
3802
  scope[this.variable] = item;
3803
+ ctx.continueCalled = ctx.breakCalled = false;
3211
3804
  yield r.renderTemplates(this.templates, ctx, emitter);
3212
- if (emitter['break']) {
3213
- emitter['break'] = false;
3805
+ if (ctx.breakCalled)
3214
3806
  break;
3215
- }
3216
- emitter['continue'] = false;
3217
3807
  scope.forloop.next();
3218
3808
  }
3809
+ ctx.continueCalled = ctx.breakCalled = false;
3219
3810
  ctx.pop();
3220
3811
  }
3812
+ *children() {
3813
+ const templates = this.templates.slice();
3814
+ if (this.elseTemplates) {
3815
+ templates.push(...this.elseTemplates);
3816
+ }
3817
+ return templates;
3818
+ }
3819
+ *arguments() {
3820
+ yield this.collection;
3821
+ for (const v of Object.values(this.hash.hash)) {
3822
+ if (isValueToken(v)) {
3823
+ yield v;
3824
+ }
3825
+ }
3826
+ }
3827
+ blockScope() {
3828
+ return [this.variable, 'forloop'];
3829
+ }
3221
3830
  }
3222
3831
  function reversed(arr) {
3223
3832
  return [...arr].reverse();
@@ -3230,36 +3839,43 @@ function limit(arr, count) {
3230
3839
  }
3231
3840
 
3232
3841
  class CaptureTag extends Tag {
3233
- constructor(tagToken, remainTokens, liquid) {
3842
+ constructor(tagToken, remainTokens, liquid, parser) {
3234
3843
  super(tagToken, remainTokens, liquid);
3235
3844
  this.templates = [];
3236
- this.variable = this.readVariableName();
3845
+ this.identifier = this.readVariable();
3846
+ this.variable = this.identifier.content;
3237
3847
  while (remainTokens.length) {
3238
3848
  const token = remainTokens.shift();
3239
3849
  if (isTagToken(token) && token.name === 'endcapture')
3240
3850
  return;
3241
- this.templates.push(liquid.parser.parseToken(token, remainTokens));
3851
+ this.templates.push(parser.parseToken(token, remainTokens));
3242
3852
  }
3243
3853
  throw new Error(`tag ${tagToken.getText()} not closed`);
3244
3854
  }
3855
+ readVariable() {
3856
+ let ident = this.tokenizer.readIdentifier();
3857
+ if (ident.content)
3858
+ return ident;
3859
+ ident = this.tokenizer.readQuoted();
3860
+ if (ident)
3861
+ return ident;
3862
+ throw this.tokenizer.error('invalid capture name');
3863
+ }
3245
3864
  *render(ctx) {
3246
3865
  const r = this.liquid.renderer;
3247
3866
  const html = yield r.renderTemplates(this.templates, ctx);
3248
3867
  ctx.bottom()[this.variable] = html;
3249
3868
  }
3250
- readVariableName() {
3251
- const word = this.tokenizer.readIdentifier().content;
3252
- if (word)
3253
- return word;
3254
- const quoted = this.tokenizer.readQuoted();
3255
- if (quoted)
3256
- return evalQuotedToken(quoted);
3257
- throw this.tokenizer.error('invalid capture name');
3869
+ *children() {
3870
+ return this.templates;
3871
+ }
3872
+ *localScope() {
3873
+ yield this.identifier;
3258
3874
  }
3259
3875
  }
3260
3876
 
3261
3877
  class CaseTag extends Tag {
3262
- constructor(tagToken, remainTokens, liquid) {
3878
+ constructor(tagToken, remainTokens, liquid, parser) {
3263
3879
  super(tagToken, remainTokens, liquid);
3264
3880
  this.branches = [];
3265
3881
  this.elseTemplates = [];
@@ -3267,7 +3883,7 @@ class CaseTag extends Tag {
3267
3883
  this.elseTemplates = [];
3268
3884
  let p = [];
3269
3885
  let elseCount = 0;
3270
- const stream = this.liquid.parser.parseStream(remainTokens)
3886
+ const stream = parser.parseStream(remainTokens)
3271
3887
  .on('tag:when', (token) => {
3272
3888
  if (elseCount > 0) {
3273
3889
  return;
@@ -3322,6 +3938,17 @@ class CaseTag extends Tag {
3322
3938
  yield r.renderTemplates(this.elseTemplates, ctx, emitter);
3323
3939
  }
3324
3940
  }
3941
+ *arguments() {
3942
+ yield this.value;
3943
+ yield* this.branches.flatMap(b => b.values);
3944
+ }
3945
+ *children() {
3946
+ const templates = this.branches.flatMap(b => b.templates);
3947
+ if (this.elseTemplates) {
3948
+ templates.push(...this.elseTemplates);
3949
+ }
3950
+ return templates;
3951
+ }
3325
3952
  }
3326
3953
 
3327
3954
  class CommentTag extends Tag {
@@ -3338,10 +3965,10 @@ class CommentTag extends Tag {
3338
3965
  }
3339
3966
 
3340
3967
  class RenderTag extends Tag {
3341
- constructor(token, remainTokens, liquid) {
3968
+ constructor(token, remainTokens, liquid, parser) {
3342
3969
  super(token, remainTokens, liquid);
3343
3970
  const tokenizer = this.tokenizer;
3344
- this.file = parseFilePath(tokenizer, this.liquid);
3971
+ this.file = parseFilePath(tokenizer, this.liquid, parser);
3345
3972
  this.currentFile = token.file;
3346
3973
  while (!tokenizer.end()) {
3347
3974
  tokenizer.skipBlank();
@@ -3375,13 +4002,13 @@ class RenderTag extends Tag {
3375
4002
  tokenizer.p = begin;
3376
4003
  break;
3377
4004
  }
3378
- this.hash = new Hash(tokenizer.remaining());
4005
+ this.hash = new Hash(tokenizer, liquid.options.keyValueSeparator);
3379
4006
  }
3380
4007
  *render(ctx, emitter) {
3381
4008
  const { liquid, hash } = this;
3382
4009
  const filepath = (yield renderFilePath(this['file'], ctx, liquid));
3383
4010
  assert(filepath, () => `illegal file path "${filepath}"`);
3384
- const childCtx = new Context({}, ctx.opts, { sync: ctx.sync, globals: ctx.globals, strictVariables: ctx.strictVariables });
4011
+ const childCtx = ctx.spawn();
3385
4012
  const scope = childCtx.bottom();
3386
4013
  __assign(scope, yield hash.render(ctx));
3387
4014
  if (this['with']) {
@@ -3404,6 +4031,55 @@ class RenderTag extends Tag {
3404
4031
  yield liquid.renderer.renderTemplates(templates, childCtx, emitter);
3405
4032
  }
3406
4033
  }
4034
+ *children(partials, sync) {
4035
+ if (partials && isString(this['file'])) {
4036
+ return (yield this.liquid._parsePartialFile(this['file'], sync, this['currentFile']));
4037
+ }
4038
+ return [];
4039
+ }
4040
+ partialScope() {
4041
+ if (isString(this['file'])) {
4042
+ const names = Object.keys(this.hash.hash);
4043
+ if (this['with']) {
4044
+ const { value, alias } = this['with'];
4045
+ if (isString(alias)) {
4046
+ names.push([alias, value]);
4047
+ }
4048
+ else if (isString(this.file)) {
4049
+ names.push([this.file, value]);
4050
+ }
4051
+ }
4052
+ if (this['for']) {
4053
+ const { value, alias } = this['for'];
4054
+ if (isString(alias)) {
4055
+ names.push([alias, value]);
4056
+ }
4057
+ else if (isString(this.file)) {
4058
+ names.push([this.file, value]);
4059
+ }
4060
+ }
4061
+ return { name: this['file'], isolated: true, scope: names };
4062
+ }
4063
+ }
4064
+ *arguments() {
4065
+ for (const v of Object.values(this.hash.hash)) {
4066
+ if (isValueToken(v)) {
4067
+ yield v;
4068
+ }
4069
+ }
4070
+ if (this['with']) {
4071
+ const { value } = this['with'];
4072
+ if (isValueToken(value)) {
4073
+ yield value;
4074
+ }
4075
+ }
4076
+ if (this['for']) {
4077
+ const { value } = this['for'];
4078
+ if (isValueToken(value)) {
4079
+ yield value;
4080
+ }
4081
+ }
4082
+ }
3407
4083
  }
3408
4084
  /**
3409
4085
  * @return null for "none",
@@ -3411,7 +4087,7 @@ class RenderTag extends Tag {
3411
4087
  * @return Token for expression (not quoted)
3412
4088
  * @throws TypeError if cannot read next token
3413
4089
  */
3414
- function parseFilePath(tokenizer, liquid) {
4090
+ function parseFilePath(tokenizer, liquid, parser) {
3415
4091
  if (liquid.options.dynamicPartials) {
3416
4092
  const file = tokenizer.readValue();
3417
4093
  tokenizer.assert(file, 'illegal file path');
@@ -3419,13 +4095,13 @@ function parseFilePath(tokenizer, liquid) {
3419
4095
  return;
3420
4096
  if (isQuotedToken(file)) {
3421
4097
  // for filenames like "files/{{file}}", eval as liquid template
3422
- const templates = liquid.parse(evalQuotedToken(file));
4098
+ const templates = parser.parse(evalQuotedToken(file));
3423
4099
  return optimize(templates);
3424
4100
  }
3425
4101
  return file;
3426
4102
  }
3427
4103
  const tokens = [...tokenizer.readFileNameTemplate(liquid.options)];
3428
- const templates = optimize(liquid.parser.parseTokens(tokens));
4104
+ const templates = optimize(parser.parseTokens(tokens));
3429
4105
  return templates === 'none' ? undefined : templates;
3430
4106
  }
3431
4107
  function optimize(templates) {
@@ -3443,10 +4119,10 @@ function* renderFilePath(file, ctx, liquid) {
3443
4119
  }
3444
4120
 
3445
4121
  class IncludeTag extends Tag {
3446
- constructor(token, remainTokens, liquid) {
4122
+ constructor(token, remainTokens, liquid, parser) {
3447
4123
  super(token, remainTokens, liquid);
3448
4124
  const { tokenizer } = token;
3449
- this['file'] = parseFilePath(tokenizer, this.liquid);
4125
+ this['file'] = parseFilePath(tokenizer, this.liquid, parser);
3450
4126
  this['currentFile'] = token.file;
3451
4127
  const begin = tokenizer.p;
3452
4128
  const withStr = tokenizer.readIdentifier();
@@ -3460,7 +4136,7 @@ class IncludeTag extends Tag {
3460
4136
  }
3461
4137
  else
3462
4138
  tokenizer.p = begin;
3463
- this.hash = new Hash(tokenizer.remaining(), this.liquid.options.jekyllInclude);
4139
+ this.hash = new Hash(tokenizer, liquid.options.jekyllInclude || liquid.options.keyValueSeparator);
3464
4140
  }
3465
4141
  *render(ctx, emitter) {
3466
4142
  const { liquid, hash, withVar } = this;
@@ -3479,12 +4155,43 @@ class IncludeTag extends Tag {
3479
4155
  ctx.pop();
3480
4156
  ctx.restoreRegister(saved);
3481
4157
  }
4158
+ *children(partials, sync) {
4159
+ if (partials && isString(this['file'])) {
4160
+ return (yield this.liquid._parsePartialFile(this['file'], sync, this['currentFile']));
4161
+ }
4162
+ return [];
4163
+ }
4164
+ partialScope() {
4165
+ if (isString(this['file'])) {
4166
+ let names;
4167
+ if (this.liquid.options.jekyllInclude) {
4168
+ names = ['include'];
4169
+ }
4170
+ else {
4171
+ names = Object.keys(this.hash.hash);
4172
+ if (this.withVar) {
4173
+ names.push([this['file'], this.withVar]);
4174
+ }
4175
+ }
4176
+ return { name: this['file'], isolated: false, scope: names };
4177
+ }
4178
+ }
4179
+ *arguments() {
4180
+ yield* Object.values(this.hash.hash).filter(isValueToken);
4181
+ if (isValueToken(this['file'])) {
4182
+ yield this['file'];
4183
+ }
4184
+ if (isValueToken(this.withVar)) {
4185
+ yield this.withVar;
4186
+ }
4187
+ }
3482
4188
  }
3483
4189
 
3484
4190
  class DecrementTag extends Tag {
3485
4191
  constructor(token, remainTokens, liquid) {
3486
4192
  super(token, remainTokens, liquid);
3487
- this.variable = this.tokenizer.readIdentifier().content;
4193
+ this.identifier = this.tokenizer.readIdentifier();
4194
+ this.variable = this.identifier.content;
3488
4195
  }
3489
4196
  render(context, emitter) {
3490
4197
  const scope = context.environments;
@@ -3493,6 +4200,9 @@ class DecrementTag extends Tag {
3493
4200
  }
3494
4201
  emitter.write(stringify(--scope[this.variable]));
3495
4202
  }
4203
+ *localScope() {
4204
+ yield this.identifier;
4205
+ }
3496
4206
  }
3497
4207
 
3498
4208
  class CycleTag extends Tag {
@@ -3530,40 +4240,38 @@ class CycleTag extends Tag {
3530
4240
  groups[fingerprint] = idx;
3531
4241
  return yield evalToken(candidate, ctx);
3532
4242
  }
4243
+ *arguments() {
4244
+ yield* this.candidates;
4245
+ if (this.group) {
4246
+ yield this.group;
4247
+ }
4248
+ }
3533
4249
  }
3534
4250
 
3535
4251
  class IfTag extends Tag {
3536
- constructor(tagToken, remainTokens, liquid) {
4252
+ constructor(tagToken, remainTokens, liquid, parser) {
3537
4253
  super(tagToken, remainTokens, liquid);
3538
4254
  this.branches = [];
3539
- this.elseTemplates = [];
3540
4255
  let p = [];
3541
- let elseCount = 0;
3542
- liquid.parser.parseStream(remainTokens)
4256
+ parser.parseStream(remainTokens)
3543
4257
  .on('start', () => this.branches.push({
3544
- value: new Value(tagToken.args, this.liquid),
4258
+ value: new Value(tagToken.tokenizer.readFilteredValue(), this.liquid),
3545
4259
  templates: (p = [])
3546
4260
  }))
3547
4261
  .on('tag:elsif', (token) => {
3548
- if (elseCount > 0) {
3549
- p = [];
3550
- return;
3551
- }
4262
+ assert(!this.elseTemplates, 'unexpected elsif after else');
3552
4263
  this.branches.push({
3553
- value: new Value(token.args, this.liquid),
4264
+ value: new Value(token.tokenizer.readFilteredValue(), this.liquid),
3554
4265
  templates: (p = [])
3555
4266
  });
3556
4267
  })
3557
- .on('tag:else', () => {
3558
- elseCount++;
3559
- p = this.elseTemplates;
3560
- })
3561
- .on('tag:endif', function () { this.stop(); })
3562
- .on('template', (tpl) => {
3563
- if (p !== this.elseTemplates || elseCount === 1) {
3564
- p.push(tpl);
3565
- }
4268
+ .on('tag:else', tag => {
4269
+ assertEmpty(tag.args);
4270
+ assert(!this.elseTemplates, 'duplicated else');
4271
+ p = this.elseTemplates = [];
3566
4272
  })
4273
+ .on('tag:endif', function (tag) { assertEmpty(tag.args); this.stop(); })
4274
+ .on('template', (tpl) => p.push(tpl))
3567
4275
  .on('end', () => { throw new Error(`tag ${tagToken.getText()} not closed`); })
3568
4276
  .start();
3569
4277
  }
@@ -3576,14 +4284,25 @@ class IfTag extends Tag {
3576
4284
  return;
3577
4285
  }
3578
4286
  }
3579
- yield r.renderTemplates(this.elseTemplates, ctx, emitter);
4287
+ yield r.renderTemplates(this.elseTemplates || [], ctx, emitter);
4288
+ }
4289
+ *children() {
4290
+ const templates = this.branches.flatMap(b => b.templates);
4291
+ if (this.elseTemplates) {
4292
+ templates.push(...this.elseTemplates);
4293
+ }
4294
+ return templates;
4295
+ }
4296
+ arguments() {
4297
+ return this.branches.map(b => b.value);
3580
4298
  }
3581
4299
  }
3582
4300
 
3583
4301
  class IncrementTag extends Tag {
3584
4302
  constructor(token, remainTokens, liquid) {
3585
4303
  super(token, remainTokens, liquid);
3586
- this.variable = this.tokenizer.readIdentifier().content;
4304
+ this.identifier = this.tokenizer.readIdentifier();
4305
+ this.variable = this.identifier.content;
3587
4306
  }
3588
4307
  render(context, emitter) {
3589
4308
  const scope = context.environments;
@@ -3594,15 +4313,18 @@ class IncrementTag extends Tag {
3594
4313
  scope[this.variable]++;
3595
4314
  emitter.write(stringify(val));
3596
4315
  }
4316
+ *localScope() {
4317
+ yield this.identifier;
4318
+ }
3597
4319
  }
3598
4320
 
3599
4321
  class LayoutTag extends Tag {
3600
- constructor(token, remainTokens, liquid) {
4322
+ constructor(token, remainTokens, liquid, parser) {
3601
4323
  super(token, remainTokens, liquid);
3602
- this.file = parseFilePath(this.tokenizer, this.liquid);
4324
+ this.file = parseFilePath(this.tokenizer, this.liquid, parser);
3603
4325
  this['currentFile'] = token.file;
3604
- this.args = new Hash(this.tokenizer.remaining());
3605
- this.templates = this.liquid.parser.parseTokens(remainTokens);
4326
+ this.args = new Hash(this.tokenizer, liquid.options.keyValueSeparator);
4327
+ this.templates = parser.parseTokens(remainTokens);
3606
4328
  }
3607
4329
  *render(ctx, emitter) {
3608
4330
  const { liquid, args, file } = this;
@@ -3628,10 +4350,32 @@ class LayoutTag extends Tag {
3628
4350
  yield renderer.renderTemplates(templates, ctx, emitter);
3629
4351
  ctx.pop();
3630
4352
  }
4353
+ *children(partials) {
4354
+ const templates = this.templates.slice();
4355
+ if (partials && isString(this.file)) {
4356
+ templates.push(...(yield this.liquid._parsePartialFile(this.file, true, this['currentFile'])));
4357
+ }
4358
+ return templates;
4359
+ }
4360
+ *arguments() {
4361
+ for (const v of Object.values(this.args.hash)) {
4362
+ if (isValueToken(v)) {
4363
+ yield v;
4364
+ }
4365
+ }
4366
+ if (isValueToken(this.file)) {
4367
+ yield this.file;
4368
+ }
4369
+ }
4370
+ partialScope() {
4371
+ if (isString(this.file)) {
4372
+ return { name: this.file, isolated: false, scope: Object.keys(this.args.hash) };
4373
+ }
4374
+ }
3631
4375
  }
3632
4376
 
3633
4377
  class BlockTag extends Tag {
3634
- constructor(token, remainTokens, liquid) {
4378
+ constructor(token, remainTokens, liquid, parser) {
3635
4379
  super(token, remainTokens, liquid);
3636
4380
  this.templates = [];
3637
4381
  const match = /\w+/.exec(token.args);
@@ -3640,7 +4384,7 @@ class BlockTag extends Tag {
3640
4384
  const token = remainTokens.shift();
3641
4385
  if (isTagToken(token) && token.name === 'endblock')
3642
4386
  return;
3643
- const template = liquid.parser.parseToken(token, remainTokens);
4387
+ const template = parser.parseToken(token, remainTokens);
3644
4388
  this.templates.push(template);
3645
4389
  }
3646
4390
  throw new Error(`tag ${token.getText()} not closed`);
@@ -3667,6 +4411,12 @@ class BlockTag extends Tag {
3667
4411
  ? (superBlock, emitter) => renderChild(new BlockDrop(() => renderCurrent(superBlock, emitter)), emitter)
3668
4412
  : renderCurrent;
3669
4413
  }
4414
+ *children() {
4415
+ return this.templates;
4416
+ }
4417
+ blockScope() {
4418
+ return ['block'];
4419
+ }
3670
4420
  }
3671
4421
 
3672
4422
  class RawTag extends Tag {
@@ -3710,7 +4460,7 @@ class TablerowloopDrop extends ForloopDrop {
3710
4460
  }
3711
4461
 
3712
4462
  class TablerowTag extends Tag {
3713
- constructor(tagToken, remainTokens, liquid) {
4463
+ constructor(tagToken, remainTokens, liquid, parser) {
3714
4464
  super(tagToken, remainTokens, liquid);
3715
4465
  const variable = this.tokenizer.readIdentifier();
3716
4466
  this.tokenizer.skipBlank();
@@ -3721,10 +4471,10 @@ class TablerowTag extends Tag {
3721
4471
  }
3722
4472
  this.variable = variable.content;
3723
4473
  this.collection = collectionToken;
3724
- this.args = new Hash(this.tokenizer.remaining());
4474
+ this.args = new Hash(this.tokenizer, liquid.options.keyValueSeparator);
3725
4475
  this.templates = [];
3726
4476
  let p;
3727
- const stream = this.liquid.parser.parseStream(remainTokens)
4477
+ const stream = parser.parseStream(remainTokens)
3728
4478
  .on('start', () => (p = this.templates))
3729
4479
  .on('tag:endtablerow', () => stream.stop())
3730
4480
  .on('template', (tpl) => p.push(tpl))
@@ -3759,18 +4509,32 @@ class TablerowTag extends Tag {
3759
4509
  emitter.write('</tr>');
3760
4510
  ctx.pop();
3761
4511
  }
4512
+ *children() {
4513
+ return this.templates;
4514
+ }
4515
+ *arguments() {
4516
+ yield this.collection;
4517
+ for (const v of Object.values(this.args.hash)) {
4518
+ if (isValueToken(v)) {
4519
+ yield v;
4520
+ }
4521
+ }
4522
+ }
4523
+ blockScope() {
4524
+ return [this.variable, 'tablerowloop'];
4525
+ }
3762
4526
  }
3763
4527
 
3764
4528
  class UnlessTag extends Tag {
3765
- constructor(tagToken, remainTokens, liquid) {
4529
+ constructor(tagToken, remainTokens, liquid, parser) {
3766
4530
  super(tagToken, remainTokens, liquid);
3767
4531
  this.branches = [];
3768
4532
  this.elseTemplates = [];
3769
4533
  let p = [];
3770
4534
  let elseCount = 0;
3771
- this.liquid.parser.parseStream(remainTokens)
4535
+ parser.parseStream(remainTokens)
3772
4536
  .on('start', () => this.branches.push({
3773
- value: new Value(tagToken.args, this.liquid),
4537
+ value: new Value(tagToken.tokenizer.readFilteredValue(), this.liquid),
3774
4538
  test: isFalsy,
3775
4539
  templates: (p = [])
3776
4540
  }))
@@ -3780,7 +4544,7 @@ class UnlessTag extends Tag {
3780
4544
  return;
3781
4545
  }
3782
4546
  this.branches.push({
3783
- value: new Value(token.args, this.liquid),
4547
+ value: new Value(token.tokenizer.readFilteredValue(), this.liquid),
3784
4548
  test: isTruthy,
3785
4549
  templates: (p = [])
3786
4550
  });
@@ -3809,17 +4573,27 @@ class UnlessTag extends Tag {
3809
4573
  }
3810
4574
  yield r.renderTemplates(this.elseTemplates, ctx, emitter);
3811
4575
  }
4576
+ *children() {
4577
+ const children = this.branches.flatMap(b => b.templates);
4578
+ if (this.elseTemplates) {
4579
+ children.push(...this.elseTemplates);
4580
+ }
4581
+ return children;
4582
+ }
4583
+ arguments() {
4584
+ return this.branches.map(b => b.value);
4585
+ }
3812
4586
  }
3813
4587
 
3814
4588
  class BreakTag extends Tag {
3815
- render(ctx, emitter) {
3816
- emitter['break'] = true;
4589
+ render(ctx, _emitter) {
4590
+ ctx.breakCalled = true;
3817
4591
  }
3818
4592
  }
3819
4593
 
3820
4594
  class ContinueTag extends Tag {
3821
- render(ctx, emitter) {
3822
- emitter['continue'] = true;
4595
+ render(ctx, _emitter) {
4596
+ ctx.continueCalled = true;
3823
4597
  }
3824
4598
  }
3825
4599
 
@@ -3837,17 +4611,25 @@ class EchoTag extends Tag {
3837
4611
  const val = yield this.value.value(ctx, false);
3838
4612
  emitter.write(val);
3839
4613
  }
4614
+ *arguments() {
4615
+ if (this.value) {
4616
+ yield this.value;
4617
+ }
4618
+ }
3840
4619
  }
3841
4620
 
3842
4621
  class LiquidTag extends Tag {
3843
- constructor(token, remainTokens, liquid) {
4622
+ constructor(token, remainTokens, liquid, parser) {
3844
4623
  super(token, remainTokens, liquid);
3845
4624
  const tokens = this.tokenizer.readLiquidTagTokens(this.liquid.options);
3846
- this.templates = this.liquid.parser.parseTokens(tokens);
4625
+ this.templates = parser.parseTokens(tokens);
3847
4626
  }
3848
4627
  *render(ctx, emitter) {
3849
4628
  yield this.liquid.renderer.renderTemplates(this.templates, ctx, emitter);
3850
4629
  }
4630
+ *children() {
4631
+ return this.templates;
4632
+ }
3851
4633
  }
3852
4634
 
3853
4635
  class InlineCommentTag extends Tag {
@@ -3890,12 +4672,14 @@ class Liquid {
3890
4672
  this.filters = {};
3891
4673
  this.tags = {};
3892
4674
  this.options = normalize(opts);
4675
+ // eslint-disable-next-line deprecation/deprecation
3893
4676
  this.parser = new Parser(this);
3894
4677
  forOwn(tags, (conf, name) => this.registerTag(name, conf));
3895
4678
  forOwn(filters, (handler, name) => this.registerFilter(name, handler));
3896
4679
  }
3897
4680
  parse(html, filepath) {
3898
- return this.parser.parse(html, filepath);
4681
+ const parser = new Parser(this);
4682
+ return parser.parse(html, filepath);
3899
4683
  }
3900
4684
  _render(tpl, scope, renderOptions) {
3901
4685
  const ctx = scope instanceof Context ? scope : new Context(scope, this.options, renderOptions);
@@ -3922,19 +4706,19 @@ class Liquid {
3922
4706
  return toValueSync(this._parseAndRender(html, scope, { ...renderOptions, sync: true }));
3923
4707
  }
3924
4708
  _parsePartialFile(file, sync, currentFile) {
3925
- return this.parser.parseFile(file, sync, LookupType.Partials, currentFile);
4709
+ return new Parser(this).parseFile(file, sync, LookupType.Partials, currentFile);
3926
4710
  }
3927
4711
  _parseLayoutFile(file, sync, currentFile) {
3928
- return this.parser.parseFile(file, sync, LookupType.Layouts, currentFile);
4712
+ return new Parser(this).parseFile(file, sync, LookupType.Layouts, currentFile);
3929
4713
  }
3930
4714
  _parseFile(file, sync, lookupType, currentFile) {
3931
- return this.parser.parseFile(file, sync, lookupType, currentFile);
4715
+ return new Parser(this).parseFile(file, sync, lookupType, currentFile);
3932
4716
  }
3933
4717
  async parseFile(file, lookupType) {
3934
- return toPromise(this.parser.parseFile(file, false, lookupType));
4718
+ return toPromise(new Parser(this).parseFile(file, false, lookupType));
3935
4719
  }
3936
4720
  parseFileSync(file, lookupType) {
3937
- return toValueSync(this.parser.parseFile(file, true, lookupType));
4721
+ return toValueSync(new Parser(this).parseFile(file, true, lookupType));
3938
4722
  }
3939
4723
  *_renderFile(file, ctx, renderFileOptions) {
3940
4724
  const templates = (yield this._parseFile(file, renderFileOptions.sync, renderFileOptions.lookupType));
@@ -3984,10 +4768,82 @@ class Liquid {
3984
4768
  self.renderFile(filePath, ctx).then(html => callback(null, html), callback);
3985
4769
  };
3986
4770
  }
4771
+ async analyze(template, options = {}) {
4772
+ return analyze(template, options);
4773
+ }
4774
+ analyzeSync(template, options = {}) {
4775
+ return analyzeSync(template, options);
4776
+ }
4777
+ async parseAndAnalyze(html, filename, options = {}) {
4778
+ return analyze(this.parse(html, filename), options);
4779
+ }
4780
+ parseAndAnalyzeSync(html, filename, options = {}) {
4781
+ return analyzeSync(this.parse(html, filename), options);
4782
+ }
4783
+ /** Return an array of all variables without their properties. */
4784
+ async variables(template, options = {}) {
4785
+ const analysis = await analyze(isString(template) ? this.parse(template) : template, options);
4786
+ return Object.keys(analysis.variables);
4787
+ }
4788
+ /** Return an array of all variables without their properties. */
4789
+ variablesSync(template, options = {}) {
4790
+ const analysis = analyzeSync(isString(template) ? this.parse(template) : template, options);
4791
+ return Object.keys(analysis.variables);
4792
+ }
4793
+ /** Return an array of all variables including their properties/paths. */
4794
+ async fullVariables(template, options = {}) {
4795
+ const analysis = await analyze(isString(template) ? this.parse(template) : template, options);
4796
+ return Array.from(new Set(Object.values(analysis.variables).flatMap((a) => a.map((v) => String(v)))));
4797
+ }
4798
+ /** Return an array of all variables including their properties/paths. */
4799
+ fullVariablesSync(template, options = {}) {
4800
+ const analysis = analyzeSync(isString(template) ? this.parse(template) : template, options);
4801
+ return Array.from(new Set(Object.values(analysis.variables).flatMap((a) => a.map((v) => String(v)))));
4802
+ }
4803
+ /** Return an array of all variables, each as an array of properties/segments. */
4804
+ async variableSegments(template, options = {}) {
4805
+ const analysis = await analyze(isString(template) ? this.parse(template) : template, options);
4806
+ return Array.from(strictUniq(Object.values(analysis.variables).flatMap((a) => a.map((v) => v.toArray()))));
4807
+ }
4808
+ /** Return an array of all variables, each as an array of properties/segments. */
4809
+ variableSegmentsSync(template, options = {}) {
4810
+ const analysis = analyzeSync(isString(template) ? this.parse(template) : template, options);
4811
+ return Array.from(strictUniq(Object.values(analysis.variables).flatMap((a) => a.map((v) => v.toArray()))));
4812
+ }
4813
+ /** Return an array of all expected context variables without their properties. */
4814
+ async globalVariables(template, options = {}) {
4815
+ const analysis = await analyze(isString(template) ? this.parse(template) : template, options);
4816
+ return Object.keys(analysis.globals);
4817
+ }
4818
+ /** Return an array of all expected context variables without their properties. */
4819
+ globalVariablesSync(template, options = {}) {
4820
+ const analysis = analyzeSync(isString(template) ? this.parse(template) : template, options);
4821
+ return Object.keys(analysis.globals);
4822
+ }
4823
+ /** Return an array of all expected context variables including their properties/paths. */
4824
+ async globalFullVariables(template, options = {}) {
4825
+ const analysis = await analyze(isString(template) ? this.parse(template) : template, options);
4826
+ return Array.from(new Set(Object.values(analysis.globals).flatMap((a) => a.map((v) => String(v)))));
4827
+ }
4828
+ /** Return an array of all expected context variables including their properties/paths. */
4829
+ globalFullVariablesSync(template, options = {}) {
4830
+ const analysis = analyzeSync(isString(template) ? this.parse(template) : template, options);
4831
+ return Array.from(new Set(Object.values(analysis.globals).flatMap((a) => a.map((v) => String(v)))));
4832
+ }
4833
+ /** Return an array of all expected context variables, each as an array of properties/segments. */
4834
+ async globalVariableSegments(template, options = {}) {
4835
+ const analysis = await analyze(isString(template) ? this.parse(template) : template, options);
4836
+ return Array.from(strictUniq(Object.values(analysis.globals).flatMap((a) => a.map((v) => v.toArray()))));
4837
+ }
4838
+ /** Return an array of all expected context variables, each as an array of properties/segments. */
4839
+ globalVariableSegmentsSync(template, options = {}) {
4840
+ const analysis = analyzeSync(isString(template) ? this.parse(template) : template, options);
4841
+ return Array.from(strictUniq(Object.values(analysis.globals).flatMap((a) => a.map((v) => v.toArray()))));
4842
+ }
3987
4843
  }
3988
4844
 
3989
4845
  /* istanbul ignore file */
3990
- const version = '10.13.0';
4846
+ const version = '10.20.1';
3991
4847
 
3992
4848
  exports.AssertionError = AssertionError;
3993
4849
  exports.AssignTag = AssignTag;
@@ -4017,13 +4873,13 @@ exports.LiquidTag = LiquidTag;
4017
4873
  exports.Output = Output;
4018
4874
  exports.ParseError = ParseError;
4019
4875
  exports.ParseStream = ParseStream;
4876
+ exports.Parser = Parser;
4020
4877
  exports.RawTag = RawTag;
4021
4878
  exports.RenderError = RenderError;
4022
4879
  exports.RenderTag = RenderTag;
4023
4880
  exports.TablerowTag = TablerowTag;
4024
4881
  exports.Tag = Tag;
4025
4882
  exports.TagToken = TagToken;
4026
- exports.TimezoneDate = TimezoneDate;
4027
4883
  exports.Token = Token;
4028
4884
  exports.TokenizationError = TokenizationError;
4029
4885
  exports.Tokenizer = Tokenizer;
@@ -4031,6 +4887,9 @@ exports.TypeGuards = typeGuards;
4031
4887
  exports.UndefinedVariableError = UndefinedVariableError;
4032
4888
  exports.UnlessTag = UnlessTag;
4033
4889
  exports.Value = Value;
4890
+ exports.Variable = Variable;
4891
+ exports.analyze = analyze;
4892
+ exports.analyzeSync = analyzeSync;
4034
4893
  exports.assert = assert;
4035
4894
  exports.createTrie = createTrie;
4036
4895
  exports.defaultOperators = defaultOperators;