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
Koen

💻 Matthieu Bacconnier
Matthieu Bacconnier

📖 + + Tim van Dam
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 () {