fix: report error for malformed else/elsif/endif/endfor, #713

This commit is contained in:
Harttle
2024-07-05 01:23:33 +08:00
parent d141c4bdd2
commit 22b5a12333
8 changed files with 41 additions and 47 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ Though we're trying to be compatible with the Ruby version, there are still some
* LiquidJS-defined filters: [json][json], group_by, group_by_exp, where_exp, jsonify, inspect, etc. * LiquidJS-defined filters: [json][json], group_by, group_by_exp, where_exp, jsonify, inspect, etc.
* Tags/filters that don't depend on Shopify platform are borrowed from [Shopify][shopify-tags]. * Tags/filters that don't depend on Shopify platform are borrowed from [Shopify][shopify-tags].
* Tags/filters that don't depend on Jekyll framework are borrowed from [Jekyll][jekyll-filters]. * Tags/filters that don't depend on Jekyll framework are borrowed from [Jekyll][jekyll-filters].
* Some tags/filters behave differently: [date][date] filter. * Some tags/filters behave differently: [date][date] filter, malformed tags (like duplicated `else`, extra args for `endif`) throw errors in LiquidJS.
[date]: https://liquidjs.com/filters/date.html [date]: https://liquidjs.com/filters/date.html
[layout]: ../tags/layout.html [layout]: ../tags/layout.html
+1 -1
View File
@@ -34,7 +34,7 @@ LiquidJS 一直很重视兼容于 Ruby 版本的 Liquid。Liquid 模板语言最
* LiquidJS 自己定义的过滤器:[json][json]。 * LiquidJS 自己定义的过滤器:[json][json]。
* 从 [Shopify][shopify-tags] 借来的不依赖 Shopify 平台的标签/过滤器。 * 从 [Shopify][shopify-tags] 借来的不依赖 Shopify 平台的标签/过滤器。
* 从 [Jekyll][jekyll-filters] 借来的不依赖 Jekyll 框架的标签/过滤器。 * 从 [Jekyll][jekyll-filters] 借来的不依赖 Jekyll 框架的标签/过滤器。
* 有些过滤器和标签表现不同:比如 [date][date]。 * 有些过滤器和标签表现不同:比如 [date][date],非法的标签(比如重复的 `else``endif` 的多余参数)在 LiquidJS 中会抛出异常
[layout]: ../tags/layout.html [layout]: ../tags/layout.html
[render]: ../tags/render.html [render]: ../tags/render.html
+6 -8
View File
@@ -1,10 +1,10 @@
import { Hash, ValueToken, Liquid, Tag, evalToken, Emitter, TagToken, TopLevelToken, Context, Template, ParseStream } from '..' import { Hash, ValueToken, Liquid, Tag, evalToken, Emitter, TagToken, TopLevelToken, Context, Template, ParseStream } from '..'
import { toEnumerable } from '../util' import { assertEmpty, toEnumerable } from '../util'
import { ForloopDrop } from '../drop/forloop-drop' import { ForloopDrop } from '../drop/forloop-drop'
const MODIFIERS = ['offset', 'limit', 'reversed'] const MODIFIERS = ['offset', 'limit', 'reversed']
type valueof<T> = T[keyof T] type valueOf<T> = T[keyof T]
export default class extends Tag { export default class extends Tag {
variable: string variable: string
@@ -31,12 +31,10 @@ export default class extends Tag {
let p let p
const stream: ParseStream = this.liquid.parser.parseStream(remainTokens) const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
.on('start', () => (p = this.templates)) .on('start', () => (p = this.templates))
.on('tag:else', () => (p = this.elseTemplates)) .on<TagToken>('tag:else', tag => { assertEmpty(tag.args); p = this.elseTemplates })
.on('tag:endfor', () => stream.stop()) .on<TagToken>('tag:endfor', tag => { assertEmpty(tag.args); stream.stop() })
.on('template', (tpl: Template) => p.push(tpl)) .on('template', (tpl: Template) => p.push(tpl))
.on('end', () => { .on('end', () => { throw new Error(`tag ${token.getText()} not closed`) })
throw new Error(`tag ${token.getText()} not closed`)
})
stream.start() stream.start()
} }
@@ -58,7 +56,7 @@ export default class extends Tag {
? Object.keys(hash).filter(x => MODIFIERS.includes(x)) ? Object.keys(hash).filter(x => MODIFIERS.includes(x))
: MODIFIERS.filter(x => hash[x] !== undefined) : MODIFIERS.filter(x => hash[x] !== undefined)
collection = modifiers.reduce((collection, modifier: valueof<typeof MODIFIERS>) => { collection = modifiers.reduce((collection, modifier: valueOf<typeof MODIFIERS>) => {
if (modifier === 'offset') return offset(collection, hash['offset']) if (modifier === 'offset') return offset(collection, hash['offset'])
if (modifier === 'limit') return limit(collection, hash['limit']) if (modifier === 'limit') return limit(collection, hash['limit'])
return reversed(collection) return reversed(collection)
+11 -17
View File
@@ -1,38 +1,32 @@
import { Liquid, Tag, Value, Emitter, isTruthy, TagToken, TopLevelToken, Context, Template } from '..' import { Liquid, Tag, Value, Emitter, isTruthy, TagToken, TopLevelToken, Context, Template } from '..'
import { assert, assertEmpty } from '../util'
export default class extends Tag { export default class extends Tag {
branches: { value: Value, templates: Template[] }[] = [] branches: { value: Value, templates: Template[] }[] = []
elseTemplates: Template[] = [] elseTemplates: Template[] | undefined
constructor (tagToken: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) { constructor (tagToken: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(tagToken, remainTokens, liquid) super(tagToken, remainTokens, liquid)
let p: Template[] = [] let p: Template[] = []
let elseCount = 0
liquid.parser.parseStream(remainTokens) liquid.parser.parseStream(remainTokens)
.on('start', () => this.branches.push({ .on('start', () => this.branches.push({
value: new Value(tagToken.args, this.liquid), value: new Value(tagToken.args, this.liquid),
templates: (p = []) templates: (p = [])
})) }))
.on('tag:elsif', (token: TagToken) => { .on('tag:elsif', (token: TagToken) => {
if (elseCount > 0) { assert(!this.elseTemplates, 'unexpected elsif after else')
p = []
return
}
this.branches.push({ this.branches.push({
value: new Value(token.args, this.liquid), value: new Value(token.args, this.liquid),
templates: (p = []) templates: (p = [])
}) })
}) })
.on('tag:else', () => { .on<TagToken>('tag:else', tag => {
elseCount++ assertEmpty(tag.args)
p = this.elseTemplates assert(!this.elseTemplates, 'duplicated else')
}) p = this.elseTemplates = []
.on('tag:endif', function () { this.stop() }) })
.on('template', (tpl: Template) => { .on<TagToken>('tag:endif', function (tag) { assertEmpty(tag.args); this.stop() })
if (p !== this.elseTemplates || elseCount === 1) { .on('template', (tpl: Template) => p.push(tpl))
p.push(tpl)
}
})
.on('end', () => { throw new Error(`tag ${tagToken.getText()} not closed`) }) .on('end', () => { throw new Error(`tag ${tagToken.getText()} not closed`) })
.start() .start()
} }
@@ -47,6 +41,6 @@ export default class extends Tag {
return return
} }
} }
yield r.renderTemplates(this.elseTemplates, ctx, emitter) yield r.renderTemplates(this.elseTemplates || [], ctx, emitter)
} }
} }
+4
View File
@@ -8,3 +8,7 @@ export function assert <T> (predicate: T | null | undefined, message?: string |
throw new AssertionError(msg) throw new AssertionError(msg)
} }
} }
export function assertEmpty<T> (predicate: T | null | undefined, message = `unexpected ${JSON.stringify(predicate)}`) {
assert(!predicate, message)
}
+2 -10
View File
@@ -471,19 +471,11 @@ describe('Issues', function () {
}) })
it('#670 Should not render anything after an else branch', () => { it('#670 Should not render anything after an else branch', () => {
const engine = new Liquid() const engine = new Liquid()
const result = engine.parseAndRenderSync('{% assign value = "this" %}' + expect(() => engine.parseAndRenderSync('{% assign value = "this" %}{% if false %}{% else %}{% else %}{% endif %}')).toThrow('duplicated else')
'{% if false %}don\'t show' +
'{% else %}show {{ value }}' +
'{% else %}don\'t show{% endif %}', {})
expect(result).toEqual('show this')
}) })
it('#672 Should not render an elseif after an else branch', () => { it('#672 Should not render an elseif after an else branch', () => {
const engine = new Liquid() const engine = new Liquid()
const result = engine.parseAndRenderSync('{% if false %}don\'t show' + expect(() => engine.parseAndRenderSync('{% if false %}{% else %}{% elsif true %}{% endif %}')).toThrow('unexpected elsif after else')
'{% else %}show' +
'{% elsif true %}don\'t show' +
'{% endif %}', {})
expect(result).toEqual('show')
}) })
it('#675 10.10.1 Operator: contains regression', () => { it('#675 10.10.1 Operator: contains regression', () => {
const engine = new Liquid() const engine = new Liquid()
+3 -3
View File
@@ -78,10 +78,10 @@ describe('tags/for', function () {
.rejects.toThrow('illegal tag: {%for c alpha%}, line:1, col:1') .rejects.toThrow('illegal tag: {%for c alpha%}, line:1, col:1')
}) })
it('should reject when inner templates rejected', function () { it('should throw for additional args', function () {
const src = '{%for c in alpha%}{%throwingTag%}{%endfor%}' const src = "{% for f in foo %} foo {% else foo = 'blah' %} {% endfor %}"
return expect(liquid.parseAndRender(src, scope)) return expect(liquid.parseAndRender(src, scope))
.rejects.toThrow(/intended render error/) .rejects.toThrow(`unexpected "foo = 'blah'", line:1, col:1`)
}) })
}) })
+13 -7
View File
@@ -26,6 +26,12 @@ describe('tags/if', function () {
return expect(html).toBe('') return expect(html).toBe('')
}) })
it('should throw for additional args', function () {
const src = "{% if foo %} foo {% else foo = 'blah' %} {% endif %}"
return expect(liquid.parseAndRender(src, scope))
.rejects.toThrow(`unexpected "foo = 'blah'", line:1, col:1`)
})
describe('single value as condition', function () { describe('single value as condition', function () {
it('should support boolean', async function () { it('should support boolean', async function () {
const src = '{% if false %}1{%elsif true%}2{%else%}3{%endif%}' const src = '{% if false %}1{%elsif true%}2{%else%}3{%endif%}'
@@ -155,12 +161,12 @@ describe('tags/if', function () {
const html = await liquid.parseAndRender(src, scope) const html = await liquid.parseAndRender(src, scope)
return expect(html).toBe('no') return expect(html).toBe('no')
}) })
it('should not render anything after an else branch even when first else branch is empty', () => { it('should throw for duplicated else', () => {
const engine = new Liquid() expect(() => liquid.parseAndRenderSync('{% if false %}{% else %}{% else %}{% endif %}'))
const result = engine.parseAndRenderSync('{% if false %}don\'t show' + .toThrow(`duplicated else`)
'{% else %}' + })
'{% else %}don\'t show' + it('should throw for unexpected elsif', () => {
'%{% endif %}', {}) expect(() => liquid.parseAndRenderSync('{% if false %}{% else %}{% elsif true %}{% endif %}'))
expect(result).toEqual('') .toThrow(`unexpected elsif after else`)
}) })
}) })