diff --git a/.all-contributorsrc b/.all-contributorsrc
index e9b36cc3b1..7a4652127d 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -712,6 +712,15 @@
"contributions": [
"doc"
]
+ },
+ {
+ "login": "timvandam",
+ "name": "Tim van Dam",
+ "avatar_url": "https://avatars.githubusercontent.com/u/35376389?v=4",
+ "profile": "https://tovd.dev",
+ "contributions": [
+ "code"
+ ]
}
],
"contributorsPerLine": 7,
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4a0c232c9f..418d730f16 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,12 @@
+## [10.19.1](https://github.com/harttle/liquidjs/compare/v10.19.0...v10.19.1) (2024-12-22)
+
+
+### Bug Fixes
+
+* add sideEffects=false to package.json ([734eb52](https://github.com/harttle/liquidjs/commit/734eb52b987d46d33cf8f03281a3773a0f1f0e4a))
+* inconsistent continue behaviour, fixes [#779](https://github.com/harttle/liquidjs/issues/779) ([e3ef574](https://github.com/harttle/liquidjs/commit/e3ef574674c5a21a37b3ffc929f514c8a3d0b866))
+* memoryLimit doesn't work in for tag, [#776](https://github.com/harttle/liquidjs/issues/776) ([2af297f](https://github.com/harttle/liquidjs/commit/2af297f81ac465feb3277ba7b92f7236409370b0))
+
# [10.19.0](https://github.com/harttle/liquidjs/compare/v10.18.0...v10.19.0) (2024-11-17)
diff --git a/README.md b/README.md
index 5541747580..bd9fc02abb 100644
--- a/README.md
+++ b/README.md
@@ -208,6 +208,9 @@ Want to contribute? see [Contribution Guidelines][contribution]. Thanks goes to
Koen 💻 |
Matthieu Bacconnier 📖 |
+
+ Tim van Dam 💻 |
+
diff --git a/docs/package.json b/docs/package.json
index 60b2bcba1b..71fbcf87ab 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -3,7 +3,7 @@
"version": "0.0.0",
"private": true,
"hexo": {
- "version": "5.4.0"
+ "version": "7.3.0"
},
"scripts": {
"build": "hexo generate",
diff --git a/docs/themes/navy/source/js/main.js b/docs/themes/navy/source/js/main.js
index ec4f2d7a24..2a33323cba 100644
--- a/docs/themes/navy/source/js/main.js
+++ b/docs/themes/navy/source/js/main.js
@@ -60,7 +60,10 @@
/* global liquidjs, ace */
if (!location.pathname.match(/playground.html$/)) return;
updateVersion(liquidjs.version);
- const engine = new liquidjs.Liquid();
+ const engine = new liquidjs.Liquid({
+ memoryLimit: 1e5,
+ renderLimit: 1e5
+ });
const editor = createEditor('editorEl', 'liquid');
const dataEditor = createEditor('dataEl', 'json');
const preview = createEditor('previewEl', 'html');
diff --git a/package-lock.json b/package-lock.json
index ab52a95091..d297d6ace8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "liquidjs",
- "version": "10.19.0",
+ "version": "10.19.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/package.json b/package.json
index fef6fcbcdd..616bb45f7c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,7 @@
{
"name": "liquidjs",
- "version": "10.19.0",
+ "version": "10.19.1",
+ "sideEffects": false,
"description": "A simple, expressive and safe Shopify / Github Pages compatible template engine in pure JavaScript.",
"main": "dist/liquid.node.js",
"module": "dist/liquid.node.mjs",
diff --git a/src/context/context.ts b/src/context/context.ts
index 5b0306dcc9..d23381b6a7 100644
--- a/src/context/context.ts
+++ b/src/context/context.ts
@@ -25,6 +25,8 @@ export class Context {
*/
public globals: Scope
public sync: boolean
+ public breakCalled = false
+ public continueCalled = false
/**
* The normalized liquid options object
*/
diff --git a/src/filters/array.ts b/src/filters/array.ts
index 4be9654866..8547fb80ec 100644
--- a/src/filters/array.ts
+++ b/src/filters/array.ts
@@ -1,4 +1,4 @@
-import { toArray, argumentsToValue, toValue, stringify, caseInsensitiveCompare, isArray, isNil, last as arrayLast } from '../util'
+import { toArray, argumentsToValue, toValue, stringify, caseInsensitiveCompare, isArray, isNil, last as arrayLast, isArrayLike } from '../util'
import { arrayIncludes, equals, evalToken, isTruthy } from '../render'
import { Value, FilterImpl } from '../template'
import { Tokenizer } from '../parser'
@@ -12,8 +12,8 @@ export const join = argumentsToValue(function (this: FilterImpl, v: any[], arg:
this.context.memoryLimit.use(complexity)
return array.join(sep)
})
-export const last = argumentsToValue((v: any) => isArray(v) ? arrayLast(v) : '')
-export const first = argumentsToValue((v: any) => isArray(v) ? v[0] : '')
+export const last = argumentsToValue((v: any) => isArrayLike(v) ? arrayLast(v) : '')
+export const first = argumentsToValue((v: any) => isArrayLike(v) ? v[0] : '')
export const reverse = argumentsToValue(function (this: FilterImpl, v: any[]) {
const array = toArray(v)
this.context.memoryLimit.use(array.length)
diff --git a/src/render/expression.ts b/src/render/expression.ts
index a05aca395e..62041ae174 100644
--- a/src/render/expression.ts
+++ b/src/render/expression.ts
@@ -67,6 +67,7 @@ export function evalQuotedToken (token: QuotedToken) {
function * evalRangeToken (token: RangeToken, ctx: Context) {
const low: number = yield evalToken(token.lhs, ctx)
const high: number = yield evalToken(token.rhs, ctx)
+ ctx.memoryLimit.use(high - low + 1)
return range(+low, +high + 1)
}
diff --git a/src/render/render.ts b/src/render/render.ts
index bebc8f366b..6cfc9a99ae 100644
--- a/src/render/render.ts
+++ b/src/render/render.ts
@@ -23,7 +23,7 @@ export class Render {
const html = yield tpl.render(ctx, emitter)
// if not, it'll return an `html`, write to the emitter for it
html && emitter.write(html)
- if (emitter['break'] || emitter['continue']) break
+ if (ctx.breakCalled || ctx.continueCalled) break
} catch (e) {
const err = LiquidError.is(e) ? e : new RenderError(e as Error, tpl)
if (ctx.opts.catchAllErrors) errors.push(err)
diff --git a/src/tags/break.ts b/src/tags/break.ts
index 0ed79388c4..4a68a18397 100644
--- a/src/tags/break.ts
+++ b/src/tags/break.ts
@@ -1,7 +1,7 @@
import { Context, Emitter, Tag } from '..'
export default class extends Tag {
- render (ctx: Context, emitter: Emitter) {
- emitter['break'] = true
+ render (ctx: Context, _emitter: Emitter) {
+ ctx.breakCalled = true
}
}
diff --git a/src/tags/continue.ts b/src/tags/continue.ts
index 914b8e3a00..0a886242af 100644
--- a/src/tags/continue.ts
+++ b/src/tags/continue.ts
@@ -1,7 +1,7 @@
import { Tag, Emitter, Context } from '..'
export default class extends Tag {
- render (ctx: Context, emitter: Emitter) {
- emitter['continue'] = true
+ render (ctx: Context, _emitter: Emitter) {
+ ctx.continueCalled = true
}
}
diff --git a/src/tags/for.ts b/src/tags/for.ts
index 3aca8d08e0..741f77bb17 100644
--- a/src/tags/for.ts
+++ b/src/tags/for.ts
@@ -69,12 +69,9 @@ export default class extends Tag {
ctx.push(scope)
for (const item of collection) {
scope[this.variable] = item
+ ctx.continueCalled = ctx.breakCalled = false
yield r.renderTemplates(this.templates, ctx, emitter)
- if (emitter['break']) {
- emitter['break'] = false
- break
- }
- emitter['continue'] = false
+ if (ctx.breakCalled) break
scope.forloop.next()
}
ctx.pop()
diff --git a/src/util/underscore.ts b/src/util/underscore.ts
index 2460fe0d43..adac97cc49 100644
--- a/src/util/underscore.ts
+++ b/src/util/underscore.ts
@@ -93,6 +93,10 @@ export function isArray (value: any): value is any[] {
return toString.call(value) === '[object Array]'
}
+export function isArrayLike (value: any): value is any[] {
+ return value && isNumber(value.length)
+}
+
export function isIterable (value: any): value is Iterable {
return isObject(value) && Symbol.iterator in value
}
diff --git a/test/e2e/issues.spec.ts b/test/e2e/issues.spec.ts
index 554d4c0b52..d437379fd8 100644
--- a/test/e2e/issues.spec.ts
+++ b/test/e2e/issues.spec.ts
@@ -524,4 +524,11 @@ describe('Issues', function () {
const result = engine.parseAndRenderSync(`\n{{ "foo" | pos }}`)
expect(result).toEqual('\n[2,12] foo')
})
+ it("memoryLimit doesn't work in for tag #776", () => {
+ const engine = new Liquid({
+ memoryLimit: 1e5
+ })
+ const tpl = `{% for i in (1..1000000000) %} {{'a'}} {% endfor %}`
+ expect(() => engine.parseAndRenderSync(tpl)).toThrow('memory alloc limit exceeded, line:1, col:1')
+ })
})
diff --git a/test/integration/filters/array.spec.ts b/test/integration/filters/array.spec.ts
index ed1e9a44e1..f633dc9316 100644
--- a/test/integration/filters/array.spec.ts
+++ b/test/integration/filters/array.spec.ts
@@ -27,13 +27,6 @@ describe('filters/array', function () {
return expect(render(src)).rejects.toThrow('expected ":" after filter name, line:1, col:83')
})
})
- describe('last', () => {
- it('should support last', function () {
- const src = '{{ arr | last }}'
- const scope = { arr: ['zebra', 'octopus', 'giraffe', 'tiger'] }
- return test(src, scope, 'tiger')
- })
- })
describe('split', () => {
it('should support split', function () {
const src = '{% assign my_array = "zebra, octopus, giraffe, tiger" | split: ", " %}' +
@@ -263,6 +256,7 @@ describe('filters/array', function () {
it('should return 0 for false', () => test('{{ false | size }}', '0'))
it('should return 0 for nil', () => test('{{ nil | size }}', '0'))
it('should return 0 for undefined', () => test('{{ foo | size }}', '0'))
+ it('should work for string', () => test('{{ "foo" | size }}', {}, '3'))
})
describe('first', function () {
it('should support first', () => test(
@@ -273,7 +267,7 @@ describe('filters/array', function () {
it('should return empty for nil', () => test('{{nil | first}}', ''))
it('should return empty for undefined', () => test('{{foo | first}}', ''))
it('should return empty for false', () => test('{{false | first}}', ''))
- it('should return empty for string', () => test('{{"zebra" | first}}', ''))
+ it('should work for string', () => test('{{ "foo" | first }}', 'f'))
})
describe('last', function () {
it('should support last', () => test(
@@ -284,7 +278,7 @@ describe('filters/array', function () {
it('should return empty for nil', () => test('{{nil | last}}', ''))
it('should return empty for undefined', () => test('{{foo | last}}', ''))
it('should return empty for false', () => test('{{false | last}}', ''))
- it('should return empty for string', () => test('{{"zebra" | last}}', ''))
+ it('should work for string', () => test('{{ "foo" | last }}', {}, 'o'))
})
describe('slice', function () {
it('should slice first char by 0', () => test('{{ "Liquid" | slice: 0 }}', 'L'))
diff --git a/test/integration/tags/for.spec.ts b/test/integration/tags/for.spec.ts
index fe16deb377..53a8678f1b 100644
--- a/test/integration/tags/for.spec.ts
+++ b/test/integration/tags/for.spec.ts
@@ -1,6 +1,7 @@
import { Liquid } from '../../../src/liquid'
import { Drop } from '../../../src/drop/drop'
import { Scope } from '../../../src/context/scope'
+import { mock, restore } from '../../stub/mockfs'
describe('tags/for', function () {
let liquid: Liquid, scope: Scope
@@ -139,6 +140,7 @@ describe('tags/for', function () {
})
describe('continue', function () {
+ afterEach(restore)
it('should support for with continue', async function () {
const src = '{% for i in (1..5) %}' +
'{% if i == 4 %}continue{% continue %}{% endif %}{{i}}' +
@@ -154,6 +156,28 @@ describe('tags/for', function () {
const html = await liquid.parseAndRender(src, scope)
return expect(html).toBe('123continue5')
})
+ it('should skip snippet for rendered continue', async function () {
+ mock({
+ 'snippet.liquid': ' before{% continue %}skipped'
+ })
+ const src = '{% for i in (1..2) %}' +
+ '{% render "snippet.liquid" %}' +
+ ' after' +
+ '{% endfor %}'
+ const html = await liquid.parseAndRender(src, scope)
+ return expect(html).toBe(' before after before after')
+ })
+ it('should skip `for` body for included continue', async function () {
+ mock({
+ 'snippet.liquid': ' before{% continue %}skipped'
+ })
+ const src = '{% for i in (1..2) %}' +
+ '{% include "snippet.liquid" %}' +
+ ' after' +
+ '{% endfor %}'
+ const html = await liquid.parseAndRender(src, scope)
+ return expect(html).toBe(' before before')
+ })
})
describe('break', function () {
it('should support break', async function () {