Skip to content

Commit e2311df

Browse files
authored
fix: nested block for layout (#883)
1 parent 2def22c commit e2311df

11 files changed

Lines changed: 89 additions & 11 deletions

File tree

.all-contributorsrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"login": "harttle",
1515
"name": "Jun Yang",
1616
"avatar_url": "https://avatars3.githubusercontent.com/u/4427974?v=4",
17-
"profile": "https://harttle.land",
17+
"profile": "https://github.com/harttle",
1818
"contributions": [
1919
"maintenance",
2020
"code"

.cursor/rules/testing.mdc

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
description: Testing conventions — e2e uses built dist, integration uses src
3+
globs: test/**/*.ts
4+
alwaysApply: false
5+
---
6+
7+
# Testing
8+
9+
## End-to-end tests (`test/e2e`)
10+
11+
- **Use the built package**, not TypeScript sources under `src/`.
12+
- Import the public API from the package root (for example `import { Liquid } from '../..'`), which resolves through `package.json` to **`dist/`** (`main`, `module`, etc.).
13+
- **Avoid** `import … from '../../src/liquid'` (or other `src/` paths) in `test/e2e/**` so e2e matches what consumers get from npm and you do not depend on an unbuilt tree.
14+
15+
## Integration and unit tests
16+
17+
- Tests under `test/integration/`, `src/**/*.spec.ts`, and similar may import from **`src/`** when the suite is meant to run against the current TypeScript sources (typical for this repo’s Jest setup).

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ Want to contribute? see [Contribution Guidelines][contribution]. Thanks goes to
108108
<table>
109109
<tbody>
110110
<tr>
111-
<td align="center" valign="top" width="14.28%"><a href="https://harttle.land"><img src="https://avatars3.githubusercontent.com/u/4427974?v=4?s=100" width="100px;" alt="Jun Yang"/><br /><sub><b>Jun Yang</b></sub></a><br /><a href="#maintenance-harttle" title="Maintenance">🚧</a> <a href="https://github.com/harttle/liquidjs/commits?author=harttle" title="Code">💻</a></td>
111+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/harttle"><img src="https://avatars3.githubusercontent.com/u/4427974?v=4?s=100" width="100px;" alt="Jun Yang"/><br /><sub><b>Jun Yang</b></sub></a><br /><a href="#maintenance-harttle" title="Maintenance">🚧</a> <a href="https://github.com/harttle/liquidjs/commits?author=harttle" title="Code">💻</a></td>
112112
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chenos"><img src="https://avatars0.githubusercontent.com/u/2993310?v=4?s=100" width="100px;" alt="chenos"/><br /><sub><b>chenos</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=chenos" title="Code">💻</a></td>
113113
<td align="center" valign="top" width="14.28%"><a href="https://zachleat.com/"><img src="https://avatars2.githubusercontent.com/u/39355?v=4?s=100" width="100px;" alt="Zach Leatherman"/><br /><sub><b>Zach Leatherman</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/issues?q=author%3Azachleat" title="Bug reports">🐛</a></td>
114114
<td align="center" valign="top" width="14.28%"><a href="https://github.com/thardy"><img src="https://avatars3.githubusercontent.com/u/120636?v=4?s=100" width="100px;" alt="Tim Hardy"/><br /><sub><b>Tim Hardy</b></sub></a><br /><a href="https://github.com/harttle/liquidjs/commits?author=thardy" title="Code">💻</a></td>

src/context/context.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ export class Context {
4848
this.memoryLimit = memoryLimit ?? new Limiter('memory alloc', renderOptions.memoryLimit ?? opts.memoryLimit)
4949
this.renderLimit = renderLimit ?? new Limiter('template render', getPerformance().now() + (renderOptions.renderLimit ?? opts.renderLimit))
5050
}
51-
public getRegister (key: string) {
52-
return (this.registers[key] = this.registers[key] || {})
51+
public getRegister<T> (key: string, defaultValue: T = undefined as T): T {
52+
return (this.registers[key] = this.registers[key] || defaultValue)
5353
}
5454
public setRegister (key: string, value: any) {
5555
return (this.registers[key] = value)

src/tags/block.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,25 @@ export default class extends Tag {
2323
* render (ctx: Context, emitter: Emitter) {
2424
const blockRender = this.getBlockRender(ctx)
2525
if (ctx.getRegister('blockMode') === BlockMode.STORE) {
26-
ctx.getRegister('blocks')[this.block] = blockRender
26+
ctx.getRegister('blocks', {} as Record<string, any>)[this.block] = blockRender
2727
} else {
2828
yield blockRender(new BlockDrop(), emitter)
2929
}
3030
}
3131

3232
private getBlockRender (ctx: Context) {
33+
const self = this as Tag
3334
const { liquid, templates } = this
34-
const renderChild = ctx.getRegister('blocks')[this.block]
35+
const renderChild = ctx.getRegister('blocks', {} as Record<string, any>)[this.block]
3536
const renderCurrent = function * (superBlock: BlockDrop, emitter: Emitter) {
36-
// add {{ block.super }} support when rendering
37+
const stack: Tag[] = ctx.getRegister('blockStack', [])
38+
if (stack.includes(self)) throw new Error('block tag cannot be nested')
39+
40+
stack.push(self)
3741
ctx.push({ block: superBlock })
3842
yield liquid.renderer.renderTemplates(templates, ctx, emitter)
3943
ctx.pop()
44+
stack.pop()
4045
}
4146
return renderChild
4247
? (superBlock: BlockDrop, emitter: Emitter) => renderChild(

src/tags/cycle.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export default class extends Tag {
2727
* render (ctx: Context, emitter: Emitter): Generator<unknown, unknown, unknown> {
2828
const group = (yield evalToken(this.group, ctx)) as ValueToken
2929
const fingerprint = `cycle:${group}:` + this.candidates.join(',')
30-
const groups = ctx.getRegister('cycle')
30+
const groups = ctx.getRegister('cycle', {} as Record<string, number>)
3131
let idx = groups[fingerprint]
3232

3333
if (idx === undefined) {

src/tags/for.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export default class extends Tag {
5050
}
5151

5252
const continueKey = 'continue-' + this.variable + '-' + this.collection.getText()
53-
ctx.push({ continue: ctx.getRegister(continueKey) })
53+
ctx.push({ continue: ctx.getRegister(continueKey, {}) })
5454
const hash = yield this.hash.render(ctx)
5555
ctx.pop()
5656

src/tags/layout.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export default class extends Tag {
3232
// render remaining contents and store rendered results
3333
ctx.setRegister('blockMode', BlockMode.STORE)
3434
const html = yield renderer.renderTemplates(this.templates, ctx)
35-
const blocks = ctx.getRegister('blocks')
35+
const blocks = ctx.getRegister('blocks', {} as Record<string, any>)
3636

3737
// set whole content to anonymous block if anonymous doesn't specified
3838
if (blocks[''] === undefined) blocks[''] = (parent: BlankDrop, emitter: Emitter) => emitter.write(html)

test/e2e/parse-and-render.spec.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,39 @@ describe('.parseAndRender()', function () {
8282
expect(() => e.parseAndRenderSync('{% render "link" %}')).toThrow(/ENOENT|Failed to lookup/)
8383
})
8484
})
85+
describe('layout: nested {% block %} regression', function () {
86+
let root: string
87+
beforeEach(function () {
88+
root = mkdtempSync(join(tmpdir(), 'liquid-e2e-layout-nested-'))
89+
})
90+
afterEach(function () {
91+
rmSync(root, { recursive: true, force: true })
92+
})
93+
it('should reject same-name {% block %} nested in child template (no hang / OOM)', async function () {
94+
writeFileSync(
95+
join(root, 'layout.html'),
96+
'<header>{% block a %}default-a{% endblock %}</header>' +
97+
'<main>{% block b %}default-b{% endblock %}</main>' +
98+
'<footer>{% block c %}default-c{% endblock %}</footer>'
99+
)
100+
writeFileSync(
101+
join(root, 'template.html'),
102+
'{% layout "layout" %}' +
103+
'{% block a %}outer-a {% block a %}inner-a{% endblock %}{% endblock %}' +
104+
'{% block b %}content-b{% endblock %}' +
105+
'{% block c %}content-c{% endblock %}'
106+
)
107+
const liquid = new Liquid({ root, extname: '.html' })
108+
await expect(liquid.renderFile('template')).rejects.toThrow(/block tag cannot be nested/)
109+
})
110+
it('should reject nested anonymous {% block %} in child template (no hang / OOM)', async function () {
111+
writeFileSync(join(root, 'parent.html'), 'X{%block%}{%endblock%}Y')
112+
writeFileSync(
113+
join(root, 'template.html'),
114+
'{% layout "parent" %}{%block%}A{%block%}B{%endblock%}{%endblock%}'
115+
)
116+
const liquid = new Liquid({ root, extname: '.html' })
117+
await expect(liquid.renderFile('template')).rejects.toThrow(/block tag cannot be nested/)
118+
})
119+
})
85120
})

test/integration/tags/include.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ describe('tags/include', function () {
5050
})
5151
return liquid.renderFile('/parent.html').catch(function (e) {
5252
expect(e.name).toBe('TokenizationError')
53-
expect(e.message).toMatch('illegal file path, file:/parent.html, line:1, col:11')
53+
expect(e.message).toMatch(/illegal file path, file:.*parent.html, line:1, col:11/)
5454
})
5555
})
5656

0 commit comments

Comments
 (0)