diff --git a/README.md b/README.md index a13ca7785..09fb76256 100644 --- a/README.md +++ b/README.md @@ -53,37 +53,41 @@ Documentation: - [x] downcase [Document](https://shopify.github.io/liquid/filters/downcase) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] - [x] escape [Document](https://shopify.github.io/liquid/filters/escape) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] - [x] escape_once [Document](https://shopify.github.io/liquid/filters/escape_once) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] -- [ ] first [Document](https://shopify.github.io/liquid/filters/first) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] -- [ ] floor [Document](https://shopify.github.io/liquid/filters/floor) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] -- [ ] join [Document](https://shopify.github.io/liquid/filters/join) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] -- [ ] last [Document](https://shopify.github.io/liquid/filters/last) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] -- [ ] lstrip [Document](https://shopify.github.io/liquid/filters/lstrip) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] -- [ ] map [Document](https://shopify.github.io/liquid/filters/map) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] -- [ ] minus [Document](https://shopify.github.io/liquid/filters/minus) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] -- [ ] modulo [Document](https://shopify.github.io/liquid/filters/modulo) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] -- [ ] newline_to_br [Document](https://shopify.github.io/liquid/filters/newline_to_br) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] -- [ ] plus [Document](https://shopify.github.io/liquid/filters/plus) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] -- [ ] prepend [Document](https://shopify.github.io/liquid/filters/prepend) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] -- [ ] remove [Document](https://shopify.github.io/liquid/filters/remove) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] -- [ ] remove_first [Document](https://shopify.github.io/liquid/filters/remove_first) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] -- [ ] replace [Document](https://shopify.github.io/liquid/filters/replace) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] -- [ ] replace_first [Document](https://shopify.github.io/liquid/filters/replace_first) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] -- [ ] reverse [Document](https://shopify.github.io/liquid/filters/reverse) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] -- [ ] round [Document](https://shopify.github.io/liquid/filters/round) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] -- [ ] rstrip [Document](https://shopify.github.io/liquid/filters/rstrip) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] -- [ ] size [Document](https://shopify.github.io/liquid/filters/size) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] -- [ ] slice [Document](https://shopify.github.io/liquid/filters/slice) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] -- [ ] sort [Document](https://shopify.github.io/liquid/filters/sort) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] -- [ ] split [Document](https://shopify.github.io/liquid/filters/split) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] -- [ ] strip [Document](https://shopify.github.io/liquid/filters/strip) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] -- [ ] strip_html [Document](https://shopify.github.io/liquid/filters/strip_html) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] -- [ ] strip_newlines [Document](https://shopify.github.io/liquid/filters/strip_newlines) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] -- [ ] times [Document](https://shopify.github.io/liquid/filters/times) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] -- [ ] truncate [Document](https://shopify.github.io/liquid/filters/truncate) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] -- [ ] truncatewords [Document](https://shopify.github.io/liquid/filters/truncatewords) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] -- [ ] uniq [Document](https://shopify.github.io/liquid/filters/uniq) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] -- [ ] upcase [Document](https://shopify.github.io/liquid/filters/upcase) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] -- [ ] url_encode [Document](https://shopify.github.io/liquid/filters/url_encode) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] first [Document](https://shopify.github.io/liquid/filters/first) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] floor [Document](https://shopify.github.io/liquid/filters/floor) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] join [Document](https://shopify.github.io/liquid/filters/join) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] last [Document](https://shopify.github.io/liquid/filters/last) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] lstrip [Document](https://shopify.github.io/liquid/filters/lstrip) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] map [Document](https://shopify.github.io/liquid/filters/map) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] minus [Document](https://shopify.github.io/liquid/filters/minus) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] modulo [Document](https://shopify.github.io/liquid/filters/modulo) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] newline_to_br [Document](https://shopify.github.io/liquid/filters/newline_to_br) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] plus [Document](https://shopify.github.io/liquid/filters/plus) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] prepend [Document](https://shopify.github.io/liquid/filters/prepend) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] remove [Document](https://shopify.github.io/liquid/filters/remove) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] remove_first [Document](https://shopify.github.io/liquid/filters/remove_first) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] replace [Document](https://shopify.github.io/liquid/filters/replace) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] replace_first [Document](https://shopify.github.io/liquid/filters/replace_first) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] reverse [Document](https://shopify.github.io/liquid/filters/reverse) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] round [Document](https://shopify.github.io/liquid/filters/round) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] rstrip [Document](https://shopify.github.io/liquid/filters/rstrip) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] size [Document](https://shopify.github.io/liquid/filters/size) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] slice [Document](https://shopify.github.io/liquid/filters/slice) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] sort [Document](https://shopify.github.io/liquid/filters/sort) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] split [Document](https://shopify.github.io/liquid/filters/split) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] strip [Document](https://shopify.github.io/liquid/filters/strip) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] strip_html [Document](https://shopify.github.io/liquid/filters/strip_html) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] strip_newlines [Document](https://shopify.github.io/liquid/filters/strip_newlines) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] times [Document](https://shopify.github.io/liquid/filters/times) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] truncate [Document](https://shopify.github.io/liquid/filters/truncate) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] truncatewords [Document](https://shopify.github.io/liquid/filters/truncatewords) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] uniq [Document](https://shopify.github.io/liquid/filters/uniq) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] upcase [Document](https://shopify.github.io/liquid/filters/upcase) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] +- [x] url_encode [Document](https://shopify.github.io/liquid/filters/url_encode) [Source](https://github.com/harttle/shopify-liquid/blob/master/filters.js) [Test][ft] + +## Differences from Shopify Liquid + +* `url_encode` is based on `encodeURIComponent`, ` ` will be converted to `%20`. ## Async Support diff --git a/expression.js b/expression.js index 06c9b4787..30aa96792 100644 --- a/expression.js +++ b/expression.js @@ -5,12 +5,12 @@ const _ = require('lodash'); function evalExp(exp, scope) { if (!scope) throw new Error('unable to evalExp: scope undefined'); - var operatorREs = lexical.operators; + var operatorREs = lexical.operators, + match; for (var i = 0; i < operatorREs.length; i++) { var operatorRE = operatorREs[i]; var expRE = new RegExp(`^(${lexical.quoteBalanced.source})(${operatorRE.source})(${lexical.quoteBalanced.source})$`); - var match = exp.match(expRE); - if (match) { + if (match = exp.match(expRE)) { var l = evalExp(match[1], scope); var op = syntax.operators[match[2].trim()]; var r = evalExp(match[3], scope); diff --git a/filter.js b/filter.js index 29dc78a3f..4d9aa721f 100644 --- a/filter.js +++ b/filter.js @@ -2,33 +2,42 @@ const lexical = require('./lexical.js'); const _ = require('lodash'); const Exp = require('./expression.js'); -var _filterInstance = { - render: function(output, scope) { - var args = this.args.map(arg => Exp.evalValue(arg, scope)); - args.unshift(output); - return this.filter.apply(null, args); - } -}; +var valueRE = new RegExp(`${lexical.value.source}`, 'g'); module.exports = function() { var filters = {}; + var _filterInstance = { + render: function(output, scope) { + var args = this.args.map(arg => Exp.evalValue(arg, scope)); + args.unshift(output); + return this.filter.apply(null, args); + }, + parse: function(str) { + var match = lexical.filterLine.exec(str); + if (!match) throw new Error('illegal filter: ' + str); + + var name = match[1], argList = match[2] || '', filter = filters[name]; + if (typeof filter !== 'function'){ + throw new Error(`filter "${name}" not found`); + } + + var args = []; + while(match = valueRE.exec(argList.trim())){ + args.push(match[0]); + } + + this.name = name; + this.filter = filter; + this.args = args; + + return this; + } + }; + function construct(str) { - var match = lexical.filterLine.exec(str.trim()); - if (!match) error('illegal filter: ' + str); - - var k = match[1], v = match[2]; - return factory(k, [v]); - } - - function factory(name, args) { - var filter = filters[name]; - if (typeof filter !== 'function') - throw new Error(`filter ${name} not found`); - var instance = Object.create(_filterInstance); - instance.args = args; - instance.filter = filter; + instance.parse(str); return instance; } diff --git a/filters.js b/filters.js index 0b383490e..05552acd9 100644 --- a/filters.js +++ b/filters.js @@ -8,8 +8,66 @@ module.exports = function(liquid) { liquid.registerFilter('ceil', v => Math.ceil(v)); liquid.registerFilter('date', (v, arg) => strftime(arg, v)); liquid.registerFilter('default', (v, arg) => arg || v); - liquid.registerFilter('divided_by', (v, arg) => Math.floor(v/arg)); + liquid.registerFilter('divided_by', (v, arg) => Math.floor(v / arg)); liquid.registerFilter('downcase', v => v.toLowerCase()); liquid.registerFilter('escape', v => _.escape(v)); liquid.registerFilter('escape_once', v => _.escape(_.unescape(v))); + liquid.registerFilter('first', v => v[0]); + liquid.registerFilter('floor', v => Math.floor(v)); + liquid.registerFilter('join', _.join); + liquid.registerFilter('last', v => v[v.length - 1]); + liquid.registerFilter('lstrip', v => _.trimStart(v)); + liquid.registerFilter('map', (v, arg) => _.map(v, arg)); + liquid.registerFilter('minus', bindFixed((v, arg) => v - arg)); + liquid.registerFilter('modulo', bindFixed((v, arg) => v % arg)); + liquid.registerFilter('newline_to_br', v => v.replace(/\n/g, '
')); + liquid.registerFilter('plus', bindFixed((v, arg) => v + arg)); + liquid.registerFilter('prepend', (v, arg) => arg + v); + liquid.registerFilter('remove', (v, arg) => v.split(arg).join('')); + liquid.registerFilter('remove_first', (v, l) => v.replace(l, '')); + liquid.registerFilter('replace', (v, pattern, replacement) => + _.replace(v, new RegExp(_.escapeRegExp(pattern), 'g'), replacement)); + liquid.registerFilter('replace_first', _.replace); + liquid.registerFilter('reverse', _.reverse); + liquid.registerFilter('round', _.round); + liquid.registerFilter('rstrip', _.trimEnd); + liquid.registerFilter('size', _.size); + liquid.registerFilter('slice', (v, begin, length) => + v.substr(begin, length === undefined ? 1 : length)); + liquid.registerFilter('sort', _.sortBy); + liquid.registerFilter('split', _.split); + liquid.registerFilter('strip', _.trim); + liquid.registerFilter('strip_html', v => _.replace(v, /<\/?\s*\w+\s*\/?>/g, '')); + liquid.registerFilter('strip_newlines', v => _.replace(v, /\n/g, '')); + liquid.registerFilter('times', (v, arg) => v * arg); + liquid.registerFilter('truncate', (v, l, o) => _.truncate(v, { + length: l, + omission: o === undefined ? '...' : o + })); + liquid.registerFilter('truncatewords', (v, l, o) => { + if (o === undefined) o = '...'; + var arr = v.split(' '); + var ret = arr.slice(0, l).join(' '); + if (arr.length > l) ret += o; + return ret; + }); + liquid.registerFilter('uniq', _.uniq); + liquid.registerFilter('upcase', _.toUpper); + liquid.registerFilter('url_encode', encodeURIComponent); }; + +function getFixed(v) { + var p = (v + "").split("."); + return (p.length > 1) ? p[1].length : 0; +} + +function getMaxFixed(l, r) { + return Math.max(getFixed(l), getFixed(r)); +} + +function bindFixed(cb) { + return (l, r) => { + var f = getMaxFixed(l, r); + return cb(l, r).toFixed(f); + }; +} diff --git a/index.js b/index.js index 2f3f81c56..118e40410 100644 --- a/index.js +++ b/index.js @@ -28,21 +28,22 @@ function factory(){ engine.filter = Filter(); engine.tokenize = tokenizer.parse; - var template = Template(engine.tag); - engine.parse = template.parse; - engine.parseTag = template.parseTag; - engine.parseStream = template.parseStream; + engine.template = Template(engine.tag, engine.filter); + engine.parseStream = engine.template.parseStream; var renderer = Render(engine.filter, engine.tag); - engine.evalFilter = renderer.evalFilter; engine.renderTemplates = renderer.renderTemplates; engine.render = function(html, ctx) { var tokens = engine.tokenize(html); - var templates = engine.parse(tokens); + var templates = engine.template.parse(tokens); engine.register = {}; return engine.renderTemplates(templates, scope.factory(ctx)); - }, + }; + engine.evalOutput = function(str, scope) { + var template = engine.template.parseOutput(str.trim()); + return renderer.evalOutput(template, scope); + }; fs.readdirSync(tagsPath).map(function(f){ var match = /^(\w+)\.js$/.exec(f); diff --git a/lexical.js b/lexical.js index 39c9028b5..4900915a3 100644 --- a/lexical.js +++ b/lexical.js @@ -1,33 +1,43 @@ const _ = require('lodash'); +// quote related var singleQuoted = /'[^']*'/; var doubleQuoted = /"[^"]*"/; +var quoteBalanced = new RegExp(`(?:${singleQuoted.source}|${doubleQuoted.source}|[^'"])*`); -var number = /-?\d+\.?\d*|\.?\d+/; +var number = /(?:-?\d+\.?\d*|\.?\d+)/; var bool = /true|false/i; var identifier = /[a-zA-Z_$][a-zA-Z_$0-9]*/; var subscript = /\[\d+\]/; -var quoted = new RegExp(`${singleQuoted.source}|${doubleQuoted.source}`); -var literal = new RegExp(`${quoted.source}|${bool.source}|${number.source}`, 'i'); +var quoted = new RegExp(`(?:${singleQuoted.source}|${doubleQuoted.source})`); +var literal = new RegExp(`(?:${quoted.source}|${bool.source}|${number.source})`, 'i'); var variable = new RegExp(`${identifier.source}(?:\\.${identifier.source}|${subscript.source})*`); -var value = new RegExp(`${literal.source}|${variable.source}`, 'i'); -var range = new RegExp(`\\((?:${value.source})\\.\\.(?:${value.source})\\)`); -var rangeCapture = new RegExp(`\\((${value.source})\\.\\.(${value.source})\\)`); -var variableOrRange = new RegExp(`${variable.source}|${range.source}`); + +// range related +var rangeLimit = new RegExp(`(?:${variable.source}|${number.source})`); +var range = new RegExp(`\\(${rangeLimit.source}\\.\\.${rangeLimit.source}\\)`); +var rangeCapture = new RegExp(`\\((${rangeLimit.source})\\.\\.(${rangeLimit.source})\\)`); + +var value = new RegExp(`(?:${literal.source}|${variable.source}|${range.source})`, 'i'); + +// hash related var hash = new RegExp(`(?:${identifier.source})\\s*:\\s*(?:${value.source})`, 'g'); var hashCapture = new RegExp(`(${identifier.source})\\s*:\\s*(${value.source})`, 'g'); -var filter = new RegExp(`(${identifier.source})(?:\\s*:\\s*(${value.source}))?`); -var quoteBalanced = new RegExp(`(?:${singleQuoted.source}|${doubleQuoted.source}|[^'"])*`); var tagLine = new RegExp(`^\\s*(${identifier.source})\\s*(.*)\\s*$`); -var literalLine = new RegExp(`^(?:${literal.source})$`, 'i'); -var variableLine = new RegExp(`^(?:${variable.source})$`); -var numberLine = new RegExp(`^(?:${number.source})$`); -var boolLine = new RegExp(`^(?:${bool.source})$`, 'i'); -var quotedLine = new RegExp(`^(?:${quoted.source})$`); -var rangeLine = new RegExp(`^(?:${rangeCapture.source})$`); -var filterLine = new RegExp(`^(?:${filter.source})$`); +var literalLine = new RegExp(`^${literal.source}$`, 'i'); +var variableLine = new RegExp(`^${variable.source}$`); +var numberLine = new RegExp(`^${number.source}$`); +var boolLine = new RegExp(`^${bool.source}$`, 'i'); +var quotedLine = new RegExp(`^${quoted.source}$`); +var rangeLine = new RegExp(`^${rangeCapture.source}$`); + +// filter related +var valueList = new RegExp(`${value.source}(\\s*,\\s*${value.source})*`); +var filter = new RegExp(`${identifier.source}(?:\\s*:\\s*${valueList.source})?`, 'g'); +var filterCapture = new RegExp(`(${identifier.source})(?:\\s*:\\s*(${valueList.source}))?`); +var filterLine = new RegExp(`^${filterCapture.source}$`); var operators = [ /\s+or\s+/, @@ -35,7 +45,6 @@ var operators = [ /==|!=|<=|>=|<|>|\s+contains\s+/ ]; - function isLiteral(str) { return literalLine.test(str); } @@ -64,7 +73,7 @@ function parseLiteral(str) { module.exports = { quoted, number, bool, literal, filter, hash, hashCapture, - range, rangeCapture, variableOrRange, + range, rangeCapture, identifier, value, quoteBalanced, operators, quotedLine, numberLine, boolLine, rangeLine, literalLine, filterLine, tagLine, isLiteral, isVariable, parseLiteral, isRange diff --git a/package.json b/package.json index 73ca0cc83..45efed133 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shopify-liquid", - "version": "1.0.3", + "version": "1.1.0", "description": "A Shopify Liquid Implementation in Node.js", "main": "index.js", "scripts": { diff --git a/parse-stream.js b/parse-stream.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/render.js b/render.js index 9f09214a9..bfb7f7b6e 100644 --- a/render.js +++ b/render.js @@ -1,6 +1,6 @@ -const lexical = require('./lexical.js'); const error = require('./error.js'); const Exp = require('./expression.js'); +const assert = require('assert'); function stringify(val) { if (typeof val === 'string') return val; @@ -8,43 +8,51 @@ function stringify(val) { } function factory(Filter, Tag) { + function renderTemplates(templates, scope) { - if (!scope) throw new Error('unable to evalTemplates: scope undefined'); - var html = ''; + assert(scope, 'unable to evalTemplates: scope undefined'); + var html = '', partial; templates.some(template => { if (scope.get('forloop.skip')) return true; - if (template.type === 'tag') { - if (template.name === 'continue') { - scope.set('forloop.skip', true); - return true; - } - if (template.name === 'break') { - scope.set('forloop.stop', true); - scope.set('forloop.skip', true); - return true; - } - html += template.render(scope, this.register); - } else if (template.type === 'html') { - html += template.value; - } else if (template.type === 'output') { - var val = evalFilter(template.value, scope); - html += stringify(val); + switch (template.type) { + case 'tag': + partial = renderTag(template, scope, this.register); + if(partial === undefined) return true; + html += partial; + break; + case 'html': + html += template.value; + break; + case 'output': + var val = evalOutput(template, scope); + html += stringify(val); } }); return html; } - function evalFilter(str, scope) { - if (!scope) throw new Error('unable to evalFilter: scope undefined'); - var filters = str.split('|'); - var val = Exp.evalValue(filters.shift(), scope); - return filters - .map(str => Filter.construct(str)) + function renderTag(template, scope, register) { + if (template.name === 'continue') { + scope.set('forloop.skip', true); + return; + } + if (template.name === 'break') { + scope.set('forloop.stop', true); + scope.set('forloop.skip', true); + return; + } + return template.render(scope, register); + } + + function evalOutput(template, scope) { + assert(scope, 'unable to evalOutput: scope undefined'); + var val = Exp.evalExp(template.initial, scope); + return template.filters .reduce((v, filter) => filter.render(v, scope), val); } return { - renderTemplates, evalFilter + renderTemplates, evalOutput, renderTag }; } diff --git a/tags/assign.js b/tags/assign.js index d03e97a08..effc31ac4 100644 --- a/tags/assign.js +++ b/tags/assign.js @@ -12,7 +12,7 @@ module.exports = function(liquid) { this.value = match[2]; }, render: function(scope, hash) { - scope.set(this.key, Liquid.evalValue(this.value, scope)); + scope.set(this.key, liquid.evalOutput(this.value, scope)); } }); diff --git a/tags/for.js b/tags/for.js index 475bde4be..32f6298ce 100644 --- a/tags/for.js +++ b/tags/for.js @@ -1,7 +1,7 @@ var Liquid = require('..'); var lexical = Liquid.lexical; var re = new RegExp(`^(${lexical.identifier.source})\\s+in\\s+` + - `(${lexical.variableOrRange.source})` + + `(${lexical.value.source})` + `(?:\\s+${lexical.hash.source})*` + `(?:\\s+(reversed))?$`); diff --git a/tags/tablerow.js b/tags/tablerow.js index 1204559bf..bae0b6cec 100644 --- a/tags/tablerow.js +++ b/tags/tablerow.js @@ -1,7 +1,7 @@ var Liquid = require('..'); var lexical = Liquid.lexical; var re = new RegExp(`^(${lexical.identifier.source})\\s+in\\s+` + - `(${lexical.variableOrRange.source})` + + `(${lexical.value.source})` + `(?:\\s+${lexical.hash.source})*$`); module.exports = function(liquid) { @@ -14,11 +14,9 @@ module.exports = function(liquid) { this.collection = match[2]; this.templates = []; - this.elseTemplates = []; var p, stream = liquid.parseStream(remainTokens) .onStart(x => p = this.templates) - .onTag('else', token => p = this.elseTemplates) .onTag('endtablerow', token => stream.stop()) .onTemplate(tpl => p.push(tpl)) .onEnd(x => { @@ -29,10 +27,7 @@ module.exports = function(liquid) { }, render: function(scope, hash) { - var collection = Liquid.evalExp(this.collection, scope); - if (Liquid.isFalsy(collection)) { - return liquid.renderTemplates(this.elseTemplates, scope); - } + var collection = Liquid.evalExp(this.collection, scope) || []; var html = '', ctx = {}, @@ -40,12 +35,12 @@ module.exports = function(liquid) { var offset = hash.offset || 0; var limit = (hash.limit === undefined) ? collection.length : hash.limit; - var cols = hash.cols; + var cols = hash.cols, row, col; if (!cols) throw new Error(`illegal cols: ${cols}`); collection.slice(offset, offset + limit).some((item, i) => { - var row = Math.floor(i / cols) + 1, - col = (i % cols) + 1; + row = Math.floor(i / cols) + 1; + col = (i % cols) + 1; if(col === 1){ if(row !== 1){ html += ''; @@ -60,7 +55,8 @@ module.exports = function(liquid) { html += ''; scope.pop(ctx); }); - html += '
'; + if(row > 0) html += ''; + html += ''; return html; } }); diff --git a/template.js b/template.js index 3d0611fc9..05574ef9d 100644 --- a/template.js +++ b/template.js @@ -2,7 +2,7 @@ const lexical = require('./lexical.js'); const error = require('./error.js'); const ParseError = require('./error.js').ParseError; -module.exports = function(Tag) { +module.exports = function(Tag, Filter) { var stream = { init: function(tokens) { @@ -24,17 +24,11 @@ module.exports = function(Tag) { start: function() { this.trigger('start'); while (!this.stopRequested && (token = this.tokens.shift())) { - var template; - if (token.type == 'tag') { - if (this.trigger(`tag:${token.name}`, token)) continue; - if (token.name === 'continue' || token.name === 'break') { - template = token; - } else { - template = parseTag(token, this.tokens); - } - } else { - template = token; + if (token.type == 'tag' && + this.trigger(`tag:${token.name}`, token)) { + continue; } + var template = parseToken(token, this.tokens); this.trigger('template', template); } if (!this.stopRequested) this.trigger('end'); @@ -59,32 +53,58 @@ module.exports = function(Tag) { }; function parse(tokens) { - var templates = []; - var token; + var token, templates = []; while (token = tokens.shift()) { - if (token.type === 'tag') { - var tagInstance = parseTag(token, tokens); - templates.push(tagInstance); - } else templates.push(token); + templates.push(parseToken(token, tokens)); } return templates; } - function parseTag(token, tokens) { + function parseToken(token, tokens) { try { - return Tag.construct(token).parse(tokens); + switch (token.type) { + case 'tag': + return parseTag(token, tokens); + case 'output': + return parseOutput(token.value); + case 'html': + return token; + } } catch (e) { - throw new ParseError(e.message, - token.input, token.line, e.stack); + throw new ParseError(e.message, token.input, token.line, e.stack); } } + function parseTag(token, tokens) { + if (token.name === 'continue' || token.name === 'break') return token; + return Tag.construct(token).parse(tokens); + } + + function parseOutput(str) { + var match = lexical.value.exec(str); + if(!match) throw new Error(`illegal output string: ${str}`); + + var initial = match[0]; + str = str.substr(match.index + match[0].length); + + var filters = []; + while(match = lexical.filter.exec(str)){ + filters.push([match[0].trim()]); + } + + return { + type: 'output', + initial: initial, + filters: filters.map(str => Filter.construct(str)) + }; + } + function parseStream(tokens) { var s = Object.create(stream); return s.init(tokens); } return { - parse, parseTag, parseStream + parse, parseTag, parseStream, parseOutput }; }; diff --git a/test/error.js b/test/error.js index 1baa667fb..92f7e376a 100644 --- a/test/error.js +++ b/test/error.js @@ -38,6 +38,16 @@ describe('error', function() { }); }); + it('should throw ParseError when filter not exist', function() { + test(function(){ + liquid.render('{{ a | xz }}', {}); + }, function(err){ + expect(err.name).to.equal('ParseError'); + expect(err.message).to.equal('filter "xz" not found'); + expect(err.input).to.equal('{{ a | xz }}'); + expect(err.line).to.equal(1); + }); + }); it('should throw ParseError when tag not exist', function() { test(function(){ liquid.render('{% a %}', {}); diff --git a/test/filter.js b/test/filter.js index b8fbaf296..2891fedfc 100644 --- a/test/filter.js +++ b/test/filter.js @@ -17,12 +17,30 @@ describe('filter', function() { it('should throw when not registered', function() { expect(function() { filter.construct('foo'); - }).to.throw(/filter foo not found/); + }).to.throw(/filter "foo" not found/); + }); + + it('should parse argument syntax', function(){ + filter.register('foo', x => x); + var f = filter.construct('foo: a, "b"'); + + expect(f.name).to.equal('foo'); + expect(f.args).to.deep.equal(['a', '"b"']); }); it('should register a simple filter', function(){ - filter.register('foo', x => x.toUpperCase()); - expect(filter.construct('foo').render('foo', scope)).to.equal('FOO'); + filter.register('upcase', x => x.toUpperCase()); + expect(filter.construct('upcase').render('foo', scope)).to.equal('FOO'); + }); + + it('should register a argumented filter', function(){ + filter.register('add', (a, b) => a + b); + expect(filter.construct('add: 2').render(3, scope)).to.equal(5); + }); + + it('should register a multi-argumented filter', function(){ + filter.register('add', (a, b, c) => a + b + c); + expect(filter.construct('add: 2, "c"').render(3, scope)).to.equal("5c"); }); it('should call filter with corrct arguments', function(){ diff --git a/test/filters.js b/test/filters.js index ff6c22b58..8dacb41fa 100644 --- a/test/filters.js +++ b/test/filters.js @@ -1,7 +1,8 @@ const chai = require("chai"); const expect = chai.expect; -var liquid = require('..')(), ctx; +var liquid = require('..')(), + ctx; function test(src, dst) { ctx = { @@ -10,16 +11,21 @@ function test(src, dst) { arr: [-2, 'a'], obj: { foo: 'bar' - } + }, + posts: [{ + category: 'foo' + }, { + category: 'bar' + }] }; expect(liquid.render(src, ctx)).to.equal(dst); } describe('filters', function() { - it('should output object', function(){ + it('should output object', function() { test('{{obj}}', '{"foo":"bar"}'); }); - it('should output array', function(){ + it('should output array', function() { test('{{arr}}', '[-2,"a"]'); }); it('should support abs', function() { @@ -67,9 +73,206 @@ describe('filters', function() { 'Have you read 'James & the Giant Peach'?'); test('{{ "Tetsuro Takara" | escape }}', 'Tetsuro Takara'); }); + it('should support escape_once', function() { test('{{ "1 < 2 & 3" | escape_once }}', '1 < 2 & 3'); test('{{ "1 < 2 & 3" | escape_once }}', '1 < 2 & 3'); }); -}); + it('should support split/first', function() { + src = '{% assign my_array = "apples, oranges, peaches, plums" | split: ", " %}' + + '{{ my_array | first }}'; + test(src, 'apples'); + }); + + it('should support floor', function() { + test('{{ 1.2 | floor }}', '1'); + test('{{ 2.0 | floor }}', '2'); + test('{{ 183.357 | floor }}', '183'); + test('{{ "3.5" | floor }}', '3'); + }); + + it('should support join', function() { + src = '{% assign beatles = "John, Paul, George, Ringo" | split: ", " %}' + + '{{ beatles | join: " and " }}'; + test(src, 'John and Paul and George and Ringo'); + }); + + it('should support split/last', function() { + src = '{% assign my_array = "zebra, octopus, giraffe, tiger" | split: ", " %}' + + '{{ my_array|last }}'; + test(src, 'tiger'); + }); + + it('should support lstrip', function() { + src = '{{ " So much room for activities! " | lstrip }}'; + test(src, 'So much room for activities! '); + }); + + it('should support map', function() { + test('{{posts | map: "category"}}', '["foo","bar"]'); + }); + + it('should support minus', function() { + test('{{ 4 | minus: 2 }}', '2'); + test('{{ 16 | minus: 4 }}', '12'); + test('{{ 183.357 | minus: 12 }}', '171.357'); + }); + + it('should support modulo', function() { + test('{{ 3 | modulo: 2 }}', '1'); + test('{{ 24 | modulo: 7 }}', '3'); + test('{{ 183.357 | modulo: 12 }}', '3.357'); + }); + + it('should support string_with_newlines', function() { + src = '{% capture string_with_newlines %}\n' + + 'Hello\n' + + 'there\n' + + '{% endcapture %}' + + '{{ string_with_newlines | newline_to_br }}'; + dst = '
' + + 'Hello
' + + 'there
'; + test(src, dst); + }); + + it('should support plus', function() { + test('{{ 4 | plus: 2 }}', '6'); + test('{{ 16 | plus: 4 }}', '20'); + test('{{ 183.357 | plus: 12 }}', '195.357'); + }); + + it('should support prepend', function() { + test('{% assign url = "liquidmarkup.com" %}' + + '{{ "/index.html" | prepend: url }}', + 'liquidmarkup.com/index.html'); + }); + + it('should support remove', function() { + test('{{ "I strained to see the train through the rain" | remove: "rain" }}', + 'I sted to see the t through the '); + }); + + it('should support remove_first', function() { + test('{{ "I strained to see the train through the rain" | remove_first: "rain" }}', + 'I sted to see the train through the rain'); + }); + + it('should support replace', function() { + test('{{ "Take my protein pills and put my helmet on" | replace: "my", "your" }}', + 'Take your protein pills and put your helmet on'); + }); + + it('should support replace_first', function() { + test('{% assign my_string = "Take my protein pills and put my helmet on" %}\n' + + '{{ my_string | replace_first: "my", "your" }}', + '\nTake your protein pills and put my helmet on'); + }); + + it('should support reverse', function() { + test('{{ "Ground control to Major Tom." | split: "" | reverse | join: "" }}', + '.moT rojaM ot lortnoc dnuorG'); + }); + + it('should support round', function() { + test('{{1.2|round}}', '1'); + test('{{2.7|round}}', '3'); + test('{{183.357|round: 2}}', '183.36'); + }); + + it('should support rstrip', function() { + test('{{ " So much room for activities! " | rstrip }}', + ' So much room for activities!'); + }); + + it('should support size', function() { + test('{{ "Ground control to Major Tom." | size }}', '28'); + test('{% assign my_array = "apples, oranges, peaches, plums"' + + ' | split: ", " %}{{ my_array | size }}', + '4'); + }); + + it('should support slice', function() { + test('{{ "Liquid" | slice: 0 }}', 'L'); + test('{{ "Liquid" | slice: 2 }}', 'q'); + test('{{ "Liquid" | slice: 2, 5 }}', 'quid'); + test('{{ "Liquid" | slice: -3, 2 }}', 'ui'); + }); + + it('should support sort', function() { + test('{% assign my_array = "zebra, octopus, giraffe, Sally Snake"' + + ' | split: ", " %}' + + '{{ my_array | sort | join: ", " }}', + 'Sally Snake, giraffe, octopus, zebra'); + }); + + it('should support split', function() { + test('{% assign beatles = "John, Paul, George, Ringo" | split: ", " %}' + + '{% for member in beatles %}' + + '{{ member }} ' + + '{% endfor %}', + 'John Paul George Ringo '); + }); + + it('should support strip', function() { + test('{{ " So much room for activities! " | strip }}', + 'So much room for activities!'); + }); + + it('should support strip_tml', function() { + test('{{ "Have you read Ulysses?" | strip_html }}', + 'Have you read Ulysses?'); + test('{{"

< p >

" | strip_html }}', ''); + }); + + it('should support strip_newlines', function() { + test('{% capture string_with_newlines %}\n' + + 'Hello\nthere\n{% endcapture %}' + + '{{ string_with_newlines | strip_newlines }}', + 'Hellothere'); + }); + + it('should support times', function() { + test('{{ 3 | times: 2 }}', '6'); + test('{{ 24 | times: 7 }}', '168'); + test('{{ 183.357 | times: 12 }}', '2200.284'); + }); + + it('should support truncate', function() { + test('{{ "Ground control to Major Tom." | truncate: 20 }}', + 'Ground control to...'); + test('{{ "Ground control to Major Tom." | truncate: 80 }}', + 'Ground control to Major Tom.'); + test('{{ "Ground control to Major Tom." | truncate: 25,", and so on" }}', + 'Ground control, and so on'); + test('{{ "Ground control to Major Tom." | truncate: 20, "" }}', + 'Ground control to Ma'); + }); + + it('should support truncatewords', function() { + test('{{ "Ground control to Major Tom." | truncatewords: 3 }}', + 'Ground control to...'); + test('{{ "Ground control to Major Tom." | truncatewords: 8 }}', + 'Ground control to Major Tom.'); + test('{{ "Ground control to Major Tom." | truncatewords: 3, "--" }}', + 'Ground control to--'); + test('{{ "Ground control to Major Tom." | truncatewords: 3, "" }}', + 'Ground control to'); + }); + + it('should support uniq', function() { + test('{% assign my_array = "ants, bugs, bees, bugs, ants" | split: ", " %}' + + '{{ my_array | uniq | join: ", " }}', + 'ants, bugs, bees'); + }); + + it('should support upcase', function() { + test('{{ "Parker Moore" | upcase }}', 'PARKER MOORE'); + }); + + it('should support url_encode', function() { + test('{{ "john@liquid.com" | url_encode }}', 'john%40liquid.com'); + test('{{ "Tetsuro Takara" | url_encode }}', 'Tetsuro%20Takara'); + }); +}); diff --git a/test/lexical.js b/test/lexical.js index 8f1a74733..5ef7e2d37 100644 --- a/test/lexical.js +++ b/test/lexical.js @@ -1,70 +1,79 @@ var chai = require("chai"); var should = chai.should(); +var expect = chai.expect; -var identifier = require('../lexical.js'); +var lexical = require('../lexical.js'); -describe('identifier', function() { +describe('lexical', function() { + it('should test filter syntax', function(){ + lexical.filterLine.test('abs').should.equal(true); + lexical.filterLine.test('plus:1').should.equal(true); + lexical.filterLine.test('replace: "a", b').should.equal(true); + lexical.filterLine.test('foo: a, "b"').should.equal(true); + lexical.filterLine.test('abs | another').should.equal(false); + lexical.filterLine.test('join: "," | another').should.equal(false); + }); it('should test boolean literal', function() { - identifier.isLiteral('true').should.equal(true); - identifier.isLiteral('TrUE').should.equal(true); - identifier.isLiteral('false').should.equal(true); + lexical.isLiteral('true').should.equal(true); + lexical.isLiteral('TrUE').should.equal(true); + lexical.isLiteral('false').should.equal(true); }); it('should test number literal', function() { - identifier.isLiteral('2.3').should.equal(true); - identifier.isLiteral('.3').should.equal(true); - identifier.isLiteral('-3.').should.equal(true); - identifier.isLiteral('23').should.equal(true); + lexical.isLiteral('2.3').should.equal(true); + lexical.isLiteral('.3').should.equal(true); + lexical.isLiteral('-3.').should.equal(true); + lexical.isLiteral('23').should.equal(true); }); it("should test range literal", function() { - identifier.isRange("(12..32)").should.equal(true); - identifier.isRange("(12..foo)").should.equal(true); - identifier.isRange("(foo.bar..foo)").should.equal(true); + lexical.isRange("(12..32)").should.equal(true); + lexical.isRange("(12..foo)").should.equal(true); + lexical.isRange("(foo.bar..foo)").should.equal(true); }); it('should test string literal', function() { - identifier.isLiteral('""').should.equal(true); - identifier.isLiteral('"a\'b"').should.equal(true); - identifier.isLiteral("''").should.equal(true); - identifier.isLiteral("'a bcd'").should.equal(true); + lexical.isLiteral('""').should.equal(true); + lexical.isLiteral('"a\'b"').should.equal(true); + lexical.isLiteral("''").should.equal(true); + lexical.isLiteral("'a bcd'").should.equal(true); }); it("should test variable", function() { - identifier.isVariable("foo").should.equal(true); - identifier.isVariable("foo.bar.foo").should.equal(true); - identifier.isVariable("foo[0].b").should.equal(true); + lexical.isVariable("foo").should.equal(true); + lexical.isVariable("foo.bar.foo").should.equal(true); + lexical.isVariable("foo[0].b").should.equal(true); }); it('should test none literal', function() { - identifier.isLiteral('2a').should.equal(false); - identifier.isLiteral('"x').should.equal(false); - identifier.isLiteral('a2').should.equal(false); + lexical.isLiteral('2a').should.equal(false); + lexical.isLiteral('"x').should.equal(false); + lexical.isLiteral('a2').should.equal(false); }); it('should test none variable', function() { - identifier.isVariable("a.").should.equal(false); - identifier.isVariable(".b").should.equal(false); - identifier.isVariable(".").should.equal(false); - identifier.isVariable("0a").should.equal(false); - identifier.isVariable("[0][12].bar[0]").should.equal(false); + lexical.isVariable("a.").should.equal(false); + lexical.isVariable(".b").should.equal(false); + lexical.isVariable(".").should.equal(false); + lexical.isVariable("0a").should.equal(false); + lexical.isVariable("[0][12].bar[0]").should.equal(false); }); it('should parse boolean literal', function() { - identifier.parseLiteral('true').should.equal(true); - identifier.parseLiteral('TrUE').should.equal(true); - identifier.parseLiteral('false').should.equal(false); + lexical.parseLiteral('true').should.equal(true); + lexical.parseLiteral('TrUE').should.equal(true); + lexical.parseLiteral('false').should.equal(false); }); it('should parse number literal', function() { - identifier.parseLiteral('2.3').should.equal(2.3); - identifier.parseLiteral('.32').should.equal(0.32); - identifier.parseLiteral('-23.').should.equal(-23); - identifier.parseLiteral('23').should.equal(23); + lexical.parseLiteral('2.3').should.equal(2.3); + lexical.parseLiteral('.32').should.equal(0.32); + lexical.parseLiteral('-23.').should.equal(-23); + lexical.parseLiteral('23').should.equal(23); }); it('should parse string literal', function() { - identifier.parseLiteral('"ab\'c"').should.equal("ab\'c"); + lexical.parseLiteral('"ab\'c"').should.equal("ab\'c"); }); }); diff --git a/test/render.js b/test/render.js index eb0be6a2d..6639a4ad4 100644 --- a/test/render.js +++ b/test/render.js @@ -8,14 +8,14 @@ chai.use(sinonChai); var tag = require('../tag.js')(); var Scope = require('../scope.js'); var filter = require('../filter')(); -var Render = require('../render.js')(filter, tag); -var render = Render.renderTemplates; -var evalFilter = Render.evalFilter; +var Render = require('../render.js'); +var Template = require('../template.js')(tag, filter); describe('render', function() { - var scope; + var scope, render; beforeEach(function() { + render = Render(filter, tag); scope = Scope.factory({ foo: { bar: ['a', 2] @@ -25,8 +25,18 @@ describe('render', function() { tag.clear(); }); + it('should stringify object', function() { + expect(Render.stringify({ + foo: 'bar' + })).to.equal('{"foo":"bar"}'); + }); + + it('should stringify object', function() { + expect(Render.stringify([1, 2, 3])).to.equal('[1,2,3]'); + }); + it('should render html', function() { - expect(render([{ + expect(render.renderTemplates([{ type: 'html', value: '

' }], scope)).to.equal('

'); @@ -37,14 +47,16 @@ describe('render', function() { var time = sinon.spy(); filter.register('date', date); filter.register('time', time); - evalFilter('foo.bar[0] | date: "b" | time:2', scope); + var tpl = Template.parseOutput('foo.bar[0] | date: "b" | time:2'); + render.evalOutput(tpl, scope); expect(date).to.have.been.calledWith('a', 'b'); expect(time).to.have.been.calledWith('y', 2); }); - it('should eval filter', function() { + it('should eval output', function() { filter.register('date', (l, r) => l + r); filter.register('time', (l, r) => l + 3 * r); - expect(evalFilter('foo.bar[0] | date: "b" | time:2', scope)).to.equal('ab6'); + var tpl = Template.parseOutput('foo.bar[0] | date: "b" | time:2'); + expect(render.evalOutput(tpl, scope)).to.equal('ab6'); }); }); diff --git a/test/tags.js b/test/tags.js index 2ecfc7cbd..eec807fbd 100644 --- a/test/tags.js +++ b/test/tags.js @@ -28,7 +28,9 @@ describe('tags', function() { }; }); it('should support assign', function() { - test('{% assign foo="bar"%}{{foo}}', 'bar'); + test('{% assign foo="bar" %}{{foo}}', 'bar'); + test('{% assign foo=(1..3) %}{{foo}}', '[1,2,3]'); + test('{% assign foo="a b" | capitalize | split: " " | first %}{{foo}}', 'A'); }); it('should support case', function() { testThrow('{% case "foo"%}', /{% case "foo"%} not closed/); @@ -146,6 +148,12 @@ describe('tags', function() { test(src, dst); }); + it('should support empty tablerow', function() { + src = '{% tablerow i in (1..0) cols:2 %}{{ i }}{% endtablerow %}'; + dst = '
'; + test(src, dst); + }); + it('should support tablerow with range', function() { src = '{% tablerow i in (1..5) cols:2 %}{{ i }}{% endtablerow %}'; dst = '' + diff --git a/test/template.js b/test/template.js new file mode 100644 index 000000000..eb9123d58 --- /dev/null +++ b/test/template.js @@ -0,0 +1,43 @@ +const chai = require("chai"); +const sinon = require("sinon"); +const sinonChai = require("sinon-chai"); +const should = chai.should(); +const expect = chai.expect; + +chai.use(sinonChai); + +var filter = require('../filter.js')(); +var tag = require('../tag.js')(); +var Template = require('../template.js'); + +describe('template', function() { + var scope, template, add = (l, r) => l + r; + + beforeEach(function(){ + filter.clear(); + filter.register('add', add); + + tag.clear(); + template = Template(tag, filter); + }); + + it('should parse output string', function() { + var tpl = template.parseOutput('foo'); + expect(tpl.type).to.equal('output'); + 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"'); + 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'); + expect(tpl.initial).to.equal('foo'); + expect(tpl.filters.length).to.equal(2); + }); +}); diff --git a/tokenizer.js b/tokenizer.js index d60f8bdf5..2723c38f5 100644 --- a/tokenizer.js +++ b/tokenizer.js @@ -12,6 +12,7 @@ function parse(html) { var _count = 0; while ((result = syntax.exec(html)) !== null) { + // passed html fragments if (result.index > idx) { htmlFragment = html.slice(idx, result.index); tokens.push({ @@ -19,25 +20,29 @@ function parse(html) { value: htmlFragment }); } - + // tag appeared if (result[1]) { token = factory('tag', 1, result); + var match = token.value.match(lexical.tagLine); if (!match) { throw new TokenizationError(`illegal tag: ${token.raw}`, token.input, token.line); } - token.name = match[1]; token.args = match[2]; + tokens.push(token); - } else { + } + // output + else { token = factory('output', 3, result); tokens.push(token); } idx = syntax.lastIndex; } + // remaining html if (html.length > idx) { htmlFragment = html.slice(idx, html.length); tokens.push({ @@ -48,25 +53,28 @@ function parse(html) { return tokens; function factory(type, offset, match) { - - var lines = match.input.slice(_idx + 1, match.index).split('\n'); - var idx1 = match.input.lastIndexOf('\n', match.index); - var idx2 = match.input.indexOf('\n', match.index); - if(idx2 === -1) idx2 = match.input.length; - var input = match.input.slice(idx1 + 1, idx2); - _count += lines.length - 1; - _idx = match.index; - - var token = { + return { type: type, raw: match[offset], value: match[offset + 1].trim(), - line: _count + 1, - input: input + line: getLineNum(match), + input: getLineContent(match) }; - return token; } + function getLineContent(match) { + var idx1 = match.input.lastIndexOf('\n', match.index); + var idx2 = match.input.indexOf('\n', match.index); + if (idx2 === -1) idx2 = match.input.length; + return match.input.slice(idx1 + 1, idx2); + } + + function getLineNum(match) { + var lines = match.input.slice(_idx + 1, match.index).split('\n'); + _count += lines.length - 1; + _idx = match.index; + return _count + 1; + } } exports.parse = parse;