From e8fa8d32f589f27fe6bb2b1ff10b334bf9c5543b Mon Sep 17 00:00:00 2001 From: harttle Date: Mon, 30 Oct 2017 01:46:31 +0800 Subject: [PATCH] feature: separate options for value/tag trimming, working on #17 --- README.md | 8 ++- index.js | 15 ++++-- package.json | 4 +- src/parser.js | 12 ++--- src/render.js | 8 +-- src/tokenizer.js | 6 +-- src/whitespace-ctrl.js | 17 ++++++- tags/assign.js | 2 +- test/liquid.js | 104 ++------------------------------------- test/options/cache.js | 42 ++++++++++++++++ test/options/strict.js | 35 +++++++++++++ test/options/trimming.js | 84 +++++++++++++++++++++++++++++++ test/parser.js | 20 ++++---- test/render.js | 14 +++--- test/tokenizer.js | 12 ++--- 15 files changed, 236 insertions(+), 147 deletions(-) create mode 100644 test/options/cache.js create mode 100644 test/options/strict.js create mode 100644 test/options/trimming.js diff --git a/README.md b/README.md index e858e52e4..dc54431b3 100644 --- a/README.md +++ b/README.md @@ -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`. diff --git a/index.js b/index.js index 6ec377f5b..11305f6fb 100644 --- a/index.js +++ b/index.js @@ -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) diff --git a/package.json b/package.json index 05649e756..ae8545a0d 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/src/parser.js b/src/parser.js index 4bc9f9a70..0bb360cc0 100644 --- a/src/parser.js +++ b/src/parser.js @@ -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 } } diff --git a/src/render.js b/src/render.js index 9d7e1094d..c4b72125a 100644 --- a/src/render.js +++ b/src/render.js @@ -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)) diff --git a/src/tokenizer.js b/src/tokenizer.js index aaff401f7..811db8274 100644 --- a/src/tokenizer.js +++ b/src/tokenizer.js @@ -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) === '-}}', diff --git a/src/whitespace-ctrl.js b/src/whitespace-ctrl.js index 3450ba6b7..c500ae6a0 100644 --- a/src/whitespace-ctrl.js +++ b/src/whitespace-ctrl.js @@ -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, '') } diff --git a/tags/assign.js b/tags/assign.js index 74d6c4457..eb42cda59 100644 --- a/tags/assign.js +++ b/tags/assign.js @@ -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('') } }) diff --git a/test/liquid.js b/test/liquid.js index ad7952d84..df32c28a4 100644 --- a/test/liquid.js +++ b/test/liquid.js @@ -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) - }) - }) }) diff --git a/test/options/cache.js b/test/options/cache.js new file mode 100644 index 000000000..9e0f4d370 --- /dev/null +++ b/test/options/cache.js @@ -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')) + }) +}) diff --git a/test/options/strict.js b/test/options/strict.js new file mode 100644 index 000000000..30bdb8d3a --- /dev/null +++ b/test/options/strict.js @@ -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/) + }) +}) diff --git a/test/options/trimming.js b/test/options/trimming.js new file mode 100644 index 000000000..35a4656eb --- /dev/null +++ b/test/options/trimming.js @@ -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) + }) + }) +}) diff --git a/test/parser.js b/test/parser.js index 840554ddb..6c62e73be 100644 --- a/test/parser.js +++ b/test/parser.js @@ -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) }) diff --git a/test/render.js b/test/render.js index cce8eee67..a4ed65b6e 100644 --- a/test/render.js +++ b/test/render.js @@ -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') }) }) }) diff --git a/test/tokenizer.js b/test/tokenizer.js index 9d499a875..71fa5c533 100644 --- a/test/tokenizer.js +++ b/test/tokenizer.js @@ -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 = '

{{foo | date: "%Y-%m-%d"}}

' 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}}') }) })