feature: separate options for value/tag trimming, working on #17

This commit is contained in:
harttle
2017-10-30 01:46:31 +08:00
parent 502426f621
commit e8fa8d32f5
15 changed files with 236 additions and 147 deletions
+6 -2
View File
@@ -92,9 +92,13 @@ Defaults to `["."]`
If set to `false`, undefined variables will be rendered as empty string.
Otherwise, undefined variables will cause an exception. Defaults to `false`.
* `trim_right` is used to strip blank characters (including ` `, `\t`, and `\r`) from the right of tags (`{% %}`) until `\n` (inclusive). Defaults to `false`.
* `trim_tag_right` is used to strip blank characters (including ` `, `\t`, and `\r`) from the right of tags (`{% %}`) until `\n` (inclusive). Defaults to `false`.
* `trim_left` is similiar to `trim_right`, whereas the `\n` is exclusive. Defaults to `false`. See [Whitespace Control][whitespace control] for details.
* `trim_tag_left` is similiar to `trim_tag_right`, whereas the `\n` is exclusive. Defaults to `false`. See [Whitespace Control][whitespace control] for details.
* `trim_value_right` is used to strip blank characters (including ` `, `\t`, and `\r`) from the right of values (`{{ }}`) until `\n` (inclusive). Defaults to `false`.
* `trim_value_left` is similiar to `trim_value_right`, whereas the `\n` is exclusive. Defaults to `false`. See [Whitespace Control][whitespace control] for details.
* `greedy` is used to specify whether `trim_left`/`trim_right` is greedy. When set to `true`, all successive blank characters including `\n` will be trimed regardless of line breaks. Defaults to `true`.
+11 -4
View File
@@ -53,8 +53,12 @@ var _engine = {
.then(templates => this.render(templates, ctx, opts))
},
evalOutput: function (str, scope) {
var tpl = this.parser.parseOutput(str.trim())
return this.renderer.evalOutput(tpl, scope)
console.warn('[liquidjs:deprecated] use .evalValue() instead of .evalOutput')
return this.evalValue(str, scope)
},
evalValue: function (str, scope) {
var tpl = this.parser.parseValue(str.trim())
return this.renderer.evalValue(tpl, scope)
},
registerFilter: function (name, filter) {
return this.filter.register(name, filter)
@@ -111,8 +115,11 @@ function factory (options) {
root: ['.'],
cache: false,
extname: '.liquid',
trim_right: false,
trim_left: false,
trim_tag_right: false,
trim_tag_left: false,
trim_value_right: false,
trim_value_left: false,
greedy: true,
strict_filters: false,
strict_variables: false
}, options)
+2 -2
View File
@@ -5,11 +5,11 @@
"main": "index.js",
"scripts": {
"lint": "eslint src/ test/",
"test": "npm run lint && mocha --recursive",
"test": "mocha --recursive",
"coverage": "NODE_ENV=test istanbul cover --report html ./node_modules/mocha/bin/_mocha -- -R spec --recursive",
"lcov": "NODE_ENV=test istanbul cover --report lcovonly ./node_modules/mocha/bin/_mocha -- -R spec --recursive",
"dist": "make dist",
"preversion": "npm test",
"preversion": "npm run lint && npm test",
"version": "npm run dist && git add -A dist",
"postversion": "git push && git push --tags && npm publish"
},
+6 -6
View File
@@ -55,8 +55,8 @@ module.exports = function (Tag, Filter) {
var tpl = null
if (token.type === 'tag') {
tpl = parseTag(token, tokens)
} else if (token.type === 'output') {
tpl = parseOutput(token.value)
} else if (token.type === 'value') {
tpl = parseValue(token.value)
} else { // token.type === 'html'
tpl = token
}
@@ -72,9 +72,9 @@ module.exports = function (Tag, Filter) {
return Tag.construct(token, tokens)
}
function parseOutput (str) {
function parseValue (str) {
var match = lexical.matchValue(str)
assert(match, `illegal output string: ${str}`)
assert(match, `illegal value string: ${str}`)
var initial = match[0]
str = str.substr(match.index + match[0].length)
@@ -85,7 +85,7 @@ module.exports = function (Tag, Filter) {
}
return {
type: 'output',
type: 'value',
initial: initial,
filters: filters.map(str => Filter.construct(str))
}
@@ -100,6 +100,6 @@ module.exports = function (Tag, Filter) {
parse,
parseTag,
parseStream,
parseOutput
parseValue
}
}
+4 -4
View File
@@ -27,9 +27,9 @@ var render = {
if (template.type === 'tag') {
return this.renderTag(template, scope)
.then(partial => partial === undefined ? '' : partial)
} else if (template.type === 'output') {
} else if (template.type === 'value') {
return Promise.resolve()
.then(() => this.evalOutput(template, scope))
.then(() => this.evalValue(template, scope))
.then(partial => partial === undefined ? '' : stringify(partial))
} else { // template.type === 'html'
return Promise.resolve(template.value)
@@ -47,8 +47,8 @@ var render = {
return template.render(scope)
},
evalOutput: function (template, scope) {
assert(scope, 'unable to evalOutput: scope undefined')
evalValue: function (template, scope) {
assert(scope, 'unable to evalValue: scope undefined')
return template.filters.reduce(
(prev, filter) => filter.render(prev, scope),
Syntax.evalExp(template.initial, scope))
+3 -3
View File
@@ -19,7 +19,7 @@ function parse (input, file, options) {
}
tokens.push(match[1]
? parseTagToken(match[1], match[2].trim(), match.index)
: parseOutputToken(match[3], match[4].trim(), match.index))
: parseValueToken(match[3], match[4].trim(), match.index))
}
if (input.length > lastMatchEnd) {
tokens.push(parseHTMLToken(lastMatchEnd, input.length))
@@ -48,9 +48,9 @@ function parse (input, file, options) {
return token
}
function parseOutputToken (raw, value, pos) {
function parseValueToken (raw, value, pos) {
return {
type: 'output',
type: 'value',
line: lineNumber.get(pos),
trim_left: raw.slice(0, 3) === '{{-',
trim_right: raw.slice(-3) === '-}}',
+15 -2
View File
@@ -5,22 +5,35 @@ function whiteSpaceCtrl (tokens, options) {
var inRaw = false
tokens.forEach((token, i) => {
if (!inRaw && (token.trim_left || options.trim_left)) {
if (shouldTrimLeft(token, inRaw, options)) {
trimLeft(tokens[i - 1], options.greedy)
}
if (token.type === 'tag' && token.name === 'raw') inRaw = true
if (token.type === 'tag' && token.name === 'endraw') inRaw = false
if (!inRaw && (token.trim_right || options.trim_right)) {
if (shouldTrimRight(token, inRaw, options)) {
trimRight(tokens[i + 1], options.greedy)
}
})
}
function shouldTrimLeft (token, inRaw, options) {
if (inRaw) return false
if (token.type === 'tag') return token.trim_left || options.trim_tag_left
if (token.type === 'value') return token.trim_left || options.trim_value_left
}
function shouldTrimRight (token, inRaw, options) {
if (inRaw) return false
if (token.type === 'tag') return token.trim_right || options.trim_tag_right
if (token.type === 'value') return token.trim_right || options.trim_value_right
}
function trimLeft (token, greedy) {
if (!token || token.type !== 'html') return
console.log('trimming', token.value)
var rLeft = greedy ? /\s+$/g : /[\t\r ]*$/g
token.value = token.value.replace(rLeft, '')
}
+1 -1
View File
@@ -13,7 +13,7 @@ module.exports = function (liquid) {
this.value = match[2]
},
render: function (scope) {
scope.set(this.key, liquid.evalOutput(this.value, scope))
scope.set(this.key, liquid.evalValue(this.value, scope))
return Promise.resolve('')
}
})
+4 -100
View File
@@ -40,14 +40,14 @@ describe('liquid', function () {
expect(liquid.options.root).to.deep.equal([])
})
})
describe('{{output}}', function () {
it('should output object', function () {
describe('{{value}}', function () {
it('should value object', function () {
return expect(engine.parseAndRender('{{obj}}', ctx)).to.eventually.equal('{"foo":"bar"}')
})
it('should output array', function () {
it('should value array', function () {
return expect(engine.parseAndRender('{{arr}}', ctx)).to.eventually.equal('[-2,"a"]')
})
it('should output undefined to empty', function () {
it('should value undefined to empty', function () {
return expect(engine.parseAndRender('foo{{zzz}}bar', ctx)).to.eventually.equal('foobar')
})
it('should render as null when filter undefined', function () {
@@ -133,100 +133,4 @@ describe('liquid', function () {
.be.rejectedWith(/EACCES/)
})
})
describe('strict', function () {
it('should not throw when strict_variables false (default)', function () {
return expect(engine.parseAndRender('before{{notdefined}}after', ctx)).to
.eventually.equal('beforeafter')
})
it('should throw when strict_variables true', function () {
var tpl = engine.parse('before{{notdefined}}after')
var opts = {
strict_variables: true
}
return expect(engine.render(tpl, ctx, opts)).to
.be.rejectedWith(/undefined variable: notdefined/)
})
it('should pass strict_variables to render by parseAndRender', function () {
var html = 'before{{notdefined}}after'
var opts = {
strict_variables: true
}
return expect(engine.parseAndRender(html, ctx, opts)).to
.be.rejectedWith(/undefined variable: notdefined/)
})
})
describe('cache', function () {
it('should be disabled by default', function () {
return engine.renderFile('files/foo')
.then(x => expect(x).to.equal('foo'))
.then(() => mock({
'/root/files/foo.html': 'bar'
}))
.then(() => engine.renderFile('files/foo'))
.then(x => expect(x).to.equal('bar'))
})
it('should respect cache=true option', function () {
engine = Liquid({
root: '/root/',
extname: '.html',
cache: true
})
return engine.renderFile('files/foo')
.then(x => expect(x).to.equal('foo'))
.then(() => mock({
'/root/files/foo.html': 'bar'
}))
.then(() => engine.renderFile('files/foo'))
.then(x => expect(x).to.equal('foo'))
})
})
describe('trim_left, trim_right', function () {
it('should trim_left for tags when trim_left=true', function () {
engine = Liquid({
trim_left: true
})
return expect(engine.parseAndRender(' \n \t{%if true%}foo{%endif%} '))
.to.eventually.equal('foo ')
})
it('should trim_right for tags when trim_right=true', function () {
engine = Liquid({
trim_right: true
})
return expect(engine.parseAndRender('\t{%if true%}foo{%endif%} \n'))
.to.eventually.equal('\tfoo')
})
it('should trim all blanks before and after when greedy=true', function () {
engine = Liquid({
greedy: true
})
return expect(engine.parseAndRender('\t{%-if true%}foo{%endif-%} \n \n'))
.to.eventually.equal('foo')
})
it('should support trim using markup', function () {
engine = Liquid()
var src = [
'{%- assign username = "John G. Chalmers-Smith" -%}',
'{%- if username and username.length > 10 -%}',
' Wow, {{ username }}, you have a long name!',
'{%- else -%}',
' Hello there!',
'{%- endif -%}'
].join('\n')
var dst = 'Wow, John G. Chalmers-Smith, you have a long name!'
return expect(engine.parseAndRender(src)).to.eventually.equal(dst)
})
it('should not trim when not specified', function () {
engine = Liquid()
var src = [
'{% assign username = "John G. Chalmers-Smith" %}',
'{% if username and username.length > 10 %}',
' Wow, {{ username }}, you have a long name!',
'{% else %}',
' Hello there!',
'{% endif %}'
].join('\n')
var dst = '\n\n Wow, John G. Chalmers-Smith, you have a long name!\n'
return expect(engine.parseAndRender(src)).to.eventually.equal(dst)
})
})
})
+42
View File
@@ -0,0 +1,42 @@
const chai = require('chai')
const expect = chai.expect
const mock = require('mock-fs')
const Liquid = require('../..')
chai.use(require('chai-as-promised'))
describe('cache options', function () {
var engine
beforeEach(function () {
engine = Liquid({
root: '/root/',
extname: '.html'
})
mock({ '/root/files/foo.html': 'foo' })
})
afterEach(function () {
mock.restore()
})
it('should be disabled by default', function () {
return engine.renderFile('files/foo')
.then(x => expect(x).to.equal('foo'))
.then(() => mock({
'/root/files/foo.html': 'bar'
}))
.then(() => engine.renderFile('files/foo'))
.then(x => expect(x).to.equal('bar'))
})
it('should respect cache=true option', function () {
engine = Liquid({
root: '/root/',
extname: '.html',
cache: true
})
return engine.renderFile('files/foo')
.then(x => expect(x).to.equal('foo'))
.then(() => mock({
'/root/files/foo.html': 'bar'
}))
.then(() => engine.renderFile('files/foo'))
.then(x => expect(x).to.equal('foo'))
})
})
+35
View File
@@ -0,0 +1,35 @@
const chai = require('chai')
const expect = chai.expect
const Liquid = require('../..')
chai.use(require('chai-as-promised'))
describe('strict options', function () {
var engine
var ctx = {}
beforeEach(function () {
engine = Liquid({
root: '/root/',
extname: '.html'
})
})
it('should not throw when strict_variables false (default)', function () {
return expect(engine.parseAndRender('before{{notdefined}}after', ctx)).to
.eventually.equal('beforeafter')
})
it('should throw when strict_variables true', function () {
var tpl = engine.parse('before{{notdefined}}after')
var opts = {
strict_variables: true
}
return expect(engine.render(tpl, ctx, opts)).to
.be.rejectedWith(/undefined variable: notdefined/)
})
it('should pass strict_variables to render by parseAndRender', function () {
var html = 'before{{notdefined}}after'
var opts = {
strict_variables: true
}
return expect(engine.parseAndRender(html, ctx, opts)).to
.be.rejectedWith(/undefined variable: notdefined/)
})
})
+84
View File
@@ -0,0 +1,84 @@
const chai = require('chai')
const expect = chai.expect
const Liquid = require('../..')
chai.use(require('chai-as-promised'))
describe('trimming', function () {
var ctx = {name: 'harttle'}
describe('tag trimming', function () {
it('should respect trim_tag_left', function () {
var engine = Liquid({ trim_tag_left: true })
return expect(engine.parseAndRender(' \n \t{%if true%}foo{%endif%} '))
.to.eventually.equal('foo ')
})
it('should respect trim_tag_right', function () {
var engine = Liquid({ trim_tag_right: true })
return expect(engine.parseAndRender('\t{%if true%}foo{%endif%} \n'))
.to.eventually.equal('\tfoo')
})
it('should not trim value', function () {
var engine = Liquid({ trim_tag_left: true, trim_tag_right: true })
return expect(engine.parseAndRender('{%if true%}a {{name}} b{%endif%}', ctx))
.to.eventually.equal('a harttle b')
})
})
describe('value trimming', function () {
it('should respect trim_value_left', function () {
var engine = Liquid({ trim_value_left: true })
return expect(engine.parseAndRender(' \n \t{{name}} ', ctx))
.to.eventually.equal('harttle ')
})
it('should respect trim_value_right', function () {
var engine = Liquid({ trim_value_right: true })
return expect(engine.parseAndRender(' \n \t{{name}} ', ctx))
.to.eventually.equal(' \n \tharttle')
})
it('should respect not trim tag', function () {
var engine = Liquid({ trim_value_left: true, trim_value_right: true })
return expect(engine.parseAndRender('\t{% if true %} aha {%endif%}\t'))
.to.eventually.equal('\t aha \t')
})
})
describe('greedy', function () {
var src = '\n {%-if true-%}\n a \n{{-name-}}{%-endif-%}\n '
it('should enable greedy by default', function () {
var engine = Liquid()
return expect(engine.parseAndRender(src, ctx))
.to.eventually.equal('aharttle')
})
it('should respect to greedy:false by default', function () {
var engine = Liquid({greedy: false})
return expect(engine.parseAndRender(src, ctx))
.to.eventually.equal('\n a \nharttle ')
})
})
describe('markup', function () {
it('should support trim using markup', function () {
var engine = Liquid()
var src = [
'{%- assign username = "John G. Chalmers-Smith" -%}',
'{%- if username and username.length > 10 -%}',
' Wow, {{ username }}, you have a long name!',
'{%- else -%}',
' Hello there!',
'{%- endif -%}'
].join('\n')
var dst = 'Wow, John G. Chalmers-Smith, you have a long name!'
return expect(engine.parseAndRender(src)).to.eventually.equal(dst)
})
it('should not trim when not specified', function () {
var engine = Liquid()
var src = [
'{% assign username = "John G. Chalmers-Smith" %}',
'{% if username and username.length > 10 %}',
' Wow, {{ username }}, you have a long name!',
'{% else %}',
' Hello there!',
'{% endif %}'
].join('\n')
var dst = '\n\n Wow, John G. Chalmers-Smith, you have a long name!\n'
return expect(engine.parseAndRender(src)).to.eventually.equal(dst)
})
})
})
+10 -10
View File
@@ -19,28 +19,28 @@ describe('template', function () {
template = Template(tag, filter)
})
it('should throw when output string illegal', function () {
it('should throw when value string illegal', function () {
expect(function () {
template.parseOutput('/')
}).to.throw(/illegal output string/)
template.parseValue('/')
}).to.throw(/illegal value string/)
})
it('should parse output string', function () {
var tpl = template.parseOutput('foo')
expect(tpl.type).to.equal('output')
it('should parse value string', function () {
var tpl = template.parseValue('foo')
expect(tpl.type).to.equal('value')
expect(tpl.initial).to.equal('foo')
expect(tpl.filters).to.deep.equal([])
})
it('should parse output string with a simple filter', function () {
var tpl = template.parseOutput('foo | add: 3, "foo"')
it('should parse value string with a simple filter', function () {
var tpl = template.parseValue('foo | add: 3, "foo"')
expect(tpl.initial).to.equal('foo')
expect(tpl.filters.length).to.equal(1)
expect(tpl.filters[0].filter).to.equal(add)
})
it('should parse output string with filters', function () {
var tpl = template.parseOutput('foo | add: "|" | add')
it('should parse value string with filters', function () {
var tpl = template.parseValue('foo | add: "|" | add')
expect(tpl.initial).to.equal('foo')
expect(tpl.filters.length).to.equal(2)
})
+7 -7
View File
@@ -44,23 +44,23 @@ describe('render', function () {
var time = sinon.spy()
filter.register('date', date)
filter.register('time', time)
var tpl = Template.parseOutput('foo.bar[0] | date: "b" | time:2')
render.evalOutput(tpl, scope)
var tpl = Template.parseValue('foo.bar[0] | date: "b" | time:2')
render.evalValue(tpl, scope)
expect(date).to.have.been.calledWith('a', 'b')
expect(time).to.have.been.calledWith('y', 2)
})
describe('.evalOutput()', function () {
describe('.evalValue()', function () {
it('should throw when scope undefined', function () {
expect(function () {
render.evalOutput()
render.evalValue()
}).to.throw(/scope undefined/)
})
it('should eval output', function () {
it('should eval value', function () {
filter.register('date', (l, r) => l + r)
filter.register('time', (l, r) => l + 3 * r)
var tpl = Template.parseOutput('foo.bar[0] | date: "b" | time:2')
expect(render.evalOutput(tpl, scope)).to.equal('ab6')
var tpl = Template.parseValue('foo.bar[0] | date: "b" | time:2')
expect(render.evalValue(tpl, scope)).to.equal('ab6')
})
})
})
+6 -6
View File
@@ -25,20 +25,20 @@ describe('tokenizer', function () {
expect(tokens[1].type).to.equal('tag')
expect(tokens[1].value).to.equal('for p in a[1]')
})
it('should handle output syntax', function () {
it('should handle value syntax', function () {
var html = '<p>{{foo | date: "%Y-%m-%d"}}</p>'
var tokens = parse(html)
expect(tokens.length).to.equal(3)
expect(tokens[1].type).to.equal('output')
expect(tokens[1].type).to.equal('value')
expect(tokens[1].value).to.equal('foo | date: "%Y-%m-%d"')
})
it('should handle successive outputs and tags', function () {
it('should handle successive value and tags', function () {
var html = '{{foo}}{{bar}}{%foo%}{%bar%}'
var tokens = parse(html)
expect(tokens.length).to.equal(4)
expect(tokens[0].type).to.equal('output')
expect(tokens[0].type).to.equal('value')
expect(tokens[3].type).to.equal('tag')
expect(tokens[1].value).to.equal('bar')
@@ -61,11 +61,11 @@ describe('tokenizer', function () {
expect(tokens[0].args).to.equal('a:a\nb:1.23')
expect(tokens[0].raw).to.equal('{%foo\na:a\nb:1.23\n%}')
})
it('should handle multiple lines output', function () {
it('should handle multiple lines value', function () {
var html = '{{foo\n|date:\n"%Y-%m-%d"\n}}'
var tokens = parse(html)
expect(tokens.length).to.equal(1)
expect(tokens[0].type).to.equal('output')
expect(tokens[0].type).to.equal('value')
expect(tokens[0].raw).to.equal('{{foo\n|date:\n"%Y-%m-%d"\n}}')
})
})