Skip to content

Commit 22b5a12

Browse files
committed
fix: report error for malformed else/elsif/endif/endfor, #713
1 parent d141c4b commit 22b5a12

File tree

8 files changed

+41
-47
lines changed

8 files changed

+41
-47
lines changed

docs/source/tutorials/differences.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Though we're trying to be compatible with the Ruby version, there are still some
3434
* LiquidJS-defined filters: [json][json], group_by, group_by_exp, where_exp, jsonify, inspect, etc.
3535
* Tags/filters that don't depend on Shopify platform are borrowed from [Shopify][shopify-tags].
3636
* Tags/filters that don't depend on Jekyll framework are borrowed from [Jekyll][jekyll-filters].
37-
* Some tags/filters behave differently: [date][date] filter.
37+
* Some tags/filters behave differently: [date][date] filter, malformed tags (like duplicated `else`, extra args for `endif`) throw errors in LiquidJS.
3838

3939
[date]: https://liquidjs.com/filters/date.html
4040
[layout]: ../tags/layout.html

docs/source/zh-cn/tutorials/differences.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ LiquidJS 一直很重视兼容于 Ruby 版本的 Liquid。Liquid 模板语言最
3434
* LiquidJS 自己定义的过滤器:[json][json]
3535
*[Shopify][shopify-tags] 借来的不依赖 Shopify 平台的标签/过滤器。
3636
*[Jekyll][jekyll-filters] 借来的不依赖 Jekyll 框架的标签/过滤器。
37-
* 有些过滤器和标签表现不同:比如 [date][date]
37+
* 有些过滤器和标签表现不同:比如 [date][date],非法的标签(比如重复的 `else``endif` 的多余参数)在 LiquidJS 中会抛出异常
3838

3939
[layout]: ../tags/layout.html
4040
[render]: ../tags/render.html

src/tags/for.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { Hash, ValueToken, Liquid, Tag, evalToken, Emitter, TagToken, TopLevelToken, Context, Template, ParseStream } from '..'
2-
import { toEnumerable } from '../util'
2+
import { assertEmpty, toEnumerable } from '../util'
33
import { ForloopDrop } from '../drop/forloop-drop'
44

55
const MODIFIERS = ['offset', 'limit', 'reversed']
66

7-
type valueof<T> = T[keyof T]
7+
type valueOf<T> = T[keyof T]
88

99
export default class extends Tag {
1010
variable: string
@@ -31,12 +31,10 @@ export default class extends Tag {
3131
let p
3232
const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
3333
.on('start', () => (p = this.templates))
34-
.on('tag:else', () => (p = this.elseTemplates))
35-
.on('tag:endfor', () => stream.stop())
34+
.on<TagToken>('tag:else', tag => { assertEmpty(tag.args); p = this.elseTemplates })
35+
.on<TagToken>('tag:endfor', tag => { assertEmpty(tag.args); stream.stop() })
3636
.on('template', (tpl: Template) => p.push(tpl))
37-
.on('end', () => {
38-
throw new Error(`tag ${token.getText()} not closed`)
39-
})
37+
.on('end', () => { throw new Error(`tag ${token.getText()} not closed`) })
4038

4139
stream.start()
4240
}
@@ -58,7 +56,7 @@ export default class extends Tag {
5856
? Object.keys(hash).filter(x => MODIFIERS.includes(x))
5957
: MODIFIERS.filter(x => hash[x] !== undefined)
6058

61-
collection = modifiers.reduce((collection, modifier: valueof<typeof MODIFIERS>) => {
59+
collection = modifiers.reduce((collection, modifier: valueOf<typeof MODIFIERS>) => {
6260
if (modifier === 'offset') return offset(collection, hash['offset'])
6361
if (modifier === 'limit') return limit(collection, hash['limit'])
6462
return reversed(collection)

src/tags/if.ts

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,32 @@
11
import { Liquid, Tag, Value, Emitter, isTruthy, TagToken, TopLevelToken, Context, Template } from '..'
2+
import { assert, assertEmpty } from '../util'
23

34
export default class extends Tag {
45
branches: { value: Value, templates: Template[] }[] = []
5-
elseTemplates: Template[] = []
6+
elseTemplates: Template[] | undefined
67

78
constructor (tagToken: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
89
super(tagToken, remainTokens, liquid)
910
let p: Template[] = []
10-
let elseCount = 0
1111
liquid.parser.parseStream(remainTokens)
1212
.on('start', () => this.branches.push({
1313
value: new Value(tagToken.args, this.liquid),
1414
templates: (p = [])
1515
}))
1616
.on('tag:elsif', (token: TagToken) => {
17-
if (elseCount > 0) {
18-
p = []
19-
return
20-
}
17+
assert(!this.elseTemplates, 'unexpected elsif after else')
2118
this.branches.push({
2219
value: new Value(token.args, this.liquid),
2320
templates: (p = [])
2421
})
2522
})
26-
.on('tag:else', () => {
27-
elseCount++
28-
p = this.elseTemplates
29-
})
30-
.on('tag:endif', function () { this.stop() })
31-
.on('template', (tpl: Template) => {
32-
if (p !== this.elseTemplates || elseCount === 1) {
33-
p.push(tpl)
34-
}
35-
})
23+
.on<TagToken>('tag:else', tag => {
24+
assertEmpty(tag.args)
25+
assert(!this.elseTemplates, 'duplicated else')
26+
p = this.elseTemplates = []
27+
})
28+
.on<TagToken>('tag:endif', function (tag) { assertEmpty(tag.args); this.stop() })
29+
.on('template', (tpl: Template) => p.push(tpl))
3630
.on('end', () => { throw new Error(`tag ${tagToken.getText()} not closed`) })
3731
.start()
3832
}
@@ -47,6 +41,6 @@ export default class extends Tag {
4741
return
4842
}
4943
}
50-
yield r.renderTemplates(this.elseTemplates, ctx, emitter)
44+
yield r.renderTemplates(this.elseTemplates || [], ctx, emitter)
5145
}
5246
}

src/util/assert.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,7 @@ export function assert <T> (predicate: T | null | undefined, message?: string |
88
throw new AssertionError(msg)
99
}
1010
}
11+
12+
export function assertEmpty<T> (predicate: T | null | undefined, message = `unexpected ${JSON.stringify(predicate)}`) {
13+
assert(!predicate, message)
14+
}

test/e2e/issues.spec.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -471,19 +471,11 @@ describe('Issues', function () {
471471
})
472472
it('#670 Should not render anything after an else branch', () => {
473473
const engine = new Liquid()
474-
const result = engine.parseAndRenderSync('{% assign value = "this" %}' +
475-
'{% if false %}don\'t show' +
476-
'{% else %}show {{ value }}' +
477-
'{% else %}don\'t show{% endif %}', {})
478-
expect(result).toEqual('show this')
474+
expect(() => engine.parseAndRenderSync('{% assign value = "this" %}{% if false %}{% else %}{% else %}{% endif %}')).toThrow('duplicated else')
479475
})
480476
it('#672 Should not render an elseif after an else branch', () => {
481477
const engine = new Liquid()
482-
const result = engine.parseAndRenderSync('{% if false %}don\'t show' +
483-
'{% else %}show' +
484-
'{% elsif true %}don\'t show' +
485-
'{% endif %}', {})
486-
expect(result).toEqual('show')
478+
expect(() => engine.parseAndRenderSync('{% if false %}{% else %}{% elsif true %}{% endif %}')).toThrow('unexpected elsif after else')
487479
})
488480
it('#675 10.10.1 Operator: contains regression', () => {
489481
const engine = new Liquid()

test/integration/tags/for.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,10 @@ describe('tags/for', function () {
7878
.rejects.toThrow('illegal tag: {%for c alpha%}, line:1, col:1')
7979
})
8080

81-
it('should reject when inner templates rejected', function () {
82-
const src = '{%for c in alpha%}{%throwingTag%}{%endfor%}'
81+
it('should throw for additional args', function () {
82+
const src = "{% for f in foo %} foo {% else foo = 'blah' %} {% endfor %}"
8383
return expect(liquid.parseAndRender(src, scope))
84-
.rejects.toThrow(/intended render error/)
84+
.rejects.toThrow(`unexpected "foo = 'blah'", line:1, col:1`)
8585
})
8686
})
8787

test/integration/tags/if.spec.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ describe('tags/if', function () {
2626
return expect(html).toBe('')
2727
})
2828

29+
it('should throw for additional args', function () {
30+
const src = "{% if foo %} foo {% else foo = 'blah' %} {% endif %}"
31+
return expect(liquid.parseAndRender(src, scope))
32+
.rejects.toThrow(`unexpected "foo = 'blah'", line:1, col:1`)
33+
})
34+
2935
describe('single value as condition', function () {
3036
it('should support boolean', async function () {
3137
const src = '{% if false %}1{%elsif true%}2{%else%}3{%endif%}'
@@ -155,12 +161,12 @@ describe('tags/if', function () {
155161
const html = await liquid.parseAndRender(src, scope)
156162
return expect(html).toBe('no')
157163
})
158-
it('should not render anything after an else branch even when first else branch is empty', () => {
159-
const engine = new Liquid()
160-
const result = engine.parseAndRenderSync('{% if false %}don\'t show' +
161-
'{% else %}' +
162-
'{% else %}don\'t show' +
163-
'%{% endif %}', {})
164-
expect(result).toEqual('')
164+
it('should throw for duplicated else', () => {
165+
expect(() => liquid.parseAndRenderSync('{% if false %}{% else %}{% else %}{% endif %}'))
166+
.toThrow(`duplicated else`)
167+
})
168+
it('should throw for unexpected elsif', () => {
169+
expect(() => liquid.parseAndRenderSync('{% if false %}{% else %}{% elsif true %}{% endif %}'))
170+
.toThrow(`unexpected elsif after else`)
165171
})
166172
})

0 commit comments

Comments
 (0)