diff --git a/src/tags/render.ts b/src/tags/render.ts index 750bb0274..8ad99902d 100644 --- a/src/tags/render.ts +++ b/src/tags/render.ts @@ -181,7 +181,9 @@ export function * renderFilePath (file: ParsedFileName, ctx: Context, liquid: Li export function pushPartialStack (ctx: Context, filepath: string, tag: 'render' | 'include') { const stack: string[] = ctx.getRegister('partialStack', []) - if (stack.includes(filepath)) throw new Error(`${tag} tag cannot be nested`) + if (ctx.renderLimit.isUnlimited() && stack.includes(filepath)) { + throw new Error(`${tag} tag cannot be nested`) + } stack.push(filepath) } diff --git a/src/util/limiter.ts b/src/util/limiter.ts index f241f5e53..e8a0a649b 100644 --- a/src/util/limiter.ts +++ b/src/util/limiter.ts @@ -19,4 +19,7 @@ export class Limiter { assert(+count <= this.limit, this.message) } } + isUnlimited () { + return !Number.isFinite(this.limit) + } } diff --git a/test/e2e/parse-and-render.spec.ts b/test/e2e/parse-and-render.spec.ts index 694b94174..59336e4e6 100644 --- a/test/e2e/parse-and-render.spec.ts +++ b/test/e2e/parse-and-render.spec.ts @@ -133,5 +133,9 @@ describe('.parseAndRender()', function () { rmSync(root, { recursive: true, force: true }) } }) + it('should allow self-referential {% render %} when renderLimit is finite', async function () { + const liquid = new Liquid({ templates: { self: '{% render "self" %}' }, renderLimit: 0.01 }) + await expect(liquid.parseAndRender('{% render "self" %}')).rejects.toThrow(/template render limit exceeded/) + }) }) }) diff --git a/test/integration/tags/include.spec.ts b/test/integration/tags/include.spec.ts index 31ac23352..5b890e0d9 100644 --- a/test/integration/tags/include.spec.ts +++ b/test/integration/tags/include.spec.ts @@ -311,6 +311,13 @@ describe('tags/include', function () { }) return expect(liquid.renderFile('/a.html')).rejects.toThrow(/include tag cannot be nested/) }) + it('should allow self-referential {% include %} when renderLimit is finite', function () { + mock({ + '/self.html': 'A{% include "self.html" %}B' + }) + const limited = new Liquid({ root: '/', renderLimit: 0.01 }) + return expect(limited.renderFile('/self.html')).rejects.toThrow(/template render limit exceeded/) + }) it('should allow legitimate nested {% include %} chain', async function () { mock({ '/a.html': 'A{% include "b.html" %}', diff --git a/test/integration/tags/render.spec.ts b/test/integration/tags/render.spec.ts index d1efddb2b..a4c8ea97b 100644 --- a/test/integration/tags/render.spec.ts +++ b/test/integration/tags/render.spec.ts @@ -407,6 +407,10 @@ describe('tags/render', function () { }) await expect(liquid.renderFile('/a.html')).rejects.toThrow(/render tag cannot be nested/) }) + it('should allow self-referential {% render %} when renderLimit is finite', async function () { + const liquid = new Liquid({ templates: { self: '{% render "self" %}' }, renderLimit: 0.01 }) + await expect(liquid.parseAndRender('{% render "self" %}')).rejects.toThrow(/template render limit exceeded/) + }) it('should allow legitimate nested {% render %} chain', async function () { mock({ '/a.html': 'A{% render "b.html" %}',