From 365a0477361440d771c5250ac770358fcbe3ef83 Mon Sep 17 00:00:00 2001
From: harttle
'));
- 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) =>
- stringify(v).split(pattern).join(replacement));
- liquid.registerFilter('replace_first', (v, arg1, arg2) => stringify(v).replace(arg1, arg2));
- liquid.registerFilter('reverse', v => v.reverse());
- liquid.registerFilter('round', (v, arg) => {
+ 'escape_once': str => escape(unescape(str)),
+ 'first': v => v[0],
+ 'floor': v => Math.floor(v),
+ 'join': (v, arg) => v.join(arg),
+ 'last': v => v[v.length - 1],
+ 'lstrip': v => stringify(v).replace(/^\s+/, ''),
+ 'map': (arr, arg) => arr.map(v => v[arg]),
+ 'minus': bindFixed((v, arg) => v - arg),
+ 'modulo': bindFixed((v, arg) => v % arg),
+ 'newline_to_br': v => v.replace(/\n/g, '
'),
+ 'plus': bindFixed((v, arg) => v + arg),
+ 'prepend': (v, arg) => arg + v,
+ 'remove': (v, arg) => v.split(arg).join(''),
+ 'remove_first': (v, l) => v.replace(l, ''),
+ 'replace': (v, pattern, replacement) =>
+ stringify(v).split(pattern).join(replacement),
+ 'replace_first': (v, arg1, arg2) => stringify(v).replace(arg1, arg2),
+ 'reverse': v => v.reverse(),
+ 'round': (v, arg) => {
var amp = Math.pow(10, arg || 0);
return Math.round(v * amp, arg) / amp;
- });
- liquid.registerFilter('rstrip', str => stringify(str).replace(/\s+$/, ''));
- liquid.registerFilter('size', v => v.length);
- liquid.registerFilter('slice', (v, begin, length) =>
- v.substr(begin, length === undefined ? 1 : length));
- liquid.registerFilter('sort', (v, arg) => v.sort(arg));
- liquid.registerFilter('split', (v, arg) => stringify(v).split(arg));
- liquid.registerFilter('strip', (v) => stringify(v).trim());
- liquid.registerFilter('strip_html', v => stringify(v).replace(/<\/?\s*\w+\s*\/?>/g, ''));
- liquid.registerFilter('strip_newlines', v => stringify(v).replace(/\n/g, ''));
- liquid.registerFilter('times', (v, arg) => v * arg);
- liquid.registerFilter('truncate', (v, l, o) => {
+ },
+ 'rstrip': str => stringify(str).replace(/\s+$/, ''),
+ 'size': v => v.length,
+ 'slice': (v, begin, length) =>
+ v.substr(begin, length === undefined ? 1 : length),
+ 'sort': (v, arg) => v.sort(arg),
+ 'split': (v, arg) => stringify(v).split(arg),
+ 'strip': (v) => stringify(v).trim(),
+ 'strip_html': v => stringify(v).replace(/<\/?\s*\w+\s*\/?>/g, ''),
+ 'strip_newlines': v => stringify(v).replace(/\n/g, ''),
+ 'times': (v, arg) => v * arg,
+ 'truncate': (v, l, o) => {
v = stringify(v);
o = (o === undefined) ? '...' : o;
l = l || 16;
if (v.length <= l) return v;
return v.substr(0, l - o.length) + o;
- });
- liquid.registerFilter('truncatewords', (v, l, o) => {
+ },
+ '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', function(arr) {
+ },
+ 'uniq': function(arr) {
var u = {};
return (arr || []).filter(val => {
if (u.hasOwnProperty(val)) {
@@ -96,11 +85,19 @@ module.exports = function(liquid) {
u[val] = true;
return true;
});
- });
- liquid.registerFilter('upcase', str => stringify(str).toUpperCase());
- liquid.registerFilter('url_encode', encodeURIComponent);
+ },
+ 'upcase': str => stringify(str).toUpperCase(),
+ 'url_encode': encodeURIComponent
};
+function escape(str) {
+ return stringify(str).replace(/&|<|>|"|'/g, m => escapeMap[m]);
+}
+
+function unescape(str) {
+ return stringify(str).replace(/&(amp|lt|gt|#34|#39);/g, m => unescapeMap[m]);
+}
+
function getFixed(v) {
var p = (v + "").split(".");
return (p.length > 1) ? p[1].length : 0;
@@ -121,3 +118,10 @@ function bindFixed(cb) {
return cb(l, r).toFixed(f);
};
}
+
+function registerAll(liquid) {
+ return _.forOwn(filters, (func, name) => liquid.registerFilter(name, func));
+}
+
+registerAll.filters = filters;
+module.exports = registerAll;
diff --git a/src/lexical.js b/src/lexical.js
index c91cc5639..a80b51a3c 100644
--- a/src/lexical.js
+++ b/src/lexical.js
@@ -1,14 +1,16 @@
// quote related
var singleQuoted = /'[^']*'/;
var doubleQuoted = /"[^"]*"/;
-var quoteBalanced = new RegExp(`(?:${singleQuoted.source}|${doubleQuoted.source}|[^'"])*`);
+var quoted = new RegExp(`${singleQuoted.source}|${doubleQuoted.source}`);
+var quoteBalanced = new RegExp(`(?:${quoted.source}|[^'"])*`);
-var number = /(?:-?\d+\.?\d*|\.?\d+)/;
+// values
+var integer = /-?\d+/;
+var number = /-?\d+\.?\d*|\.?\d+/;
var bool = /true|false/;
-var identifier = /[a-zA-Z_$][a-zA-Z_$0-9]*/;
-var subscript = /\[\d+\]/;
+var identifier = /[\w-]+/;
+var subscript = new RegExp(`\\[(?:${quoted.source}|[\\w-\\.]+)\\]`);
-var quoted = new RegExp(`(?:${singleQuoted.source}|${doubleQuoted.source})`);
var literal = new RegExp(`(?:${quoted.source}|${bool.source}|${number.source})`);
var variable = new RegExp(`${identifier.source}(?:\\.${identifier.source}|${subscript.source})*`);
@@ -69,7 +71,7 @@ function parseLiteral(str) {
}
module.exports = {
- quoted, number, bool, literal, filter,
+ quoted, number, bool, literal, filter, integer,
hash, hashCapture,
range, rangeCapture,
identifier, value, quoteBalanced, operators,
diff --git a/src/util/underscore.js b/src/util/underscore.js
index d558e25f3..8bfa1ca80 100644
--- a/src/util/underscore.js
+++ b/src/util/underscore.js
@@ -1,5 +1,29 @@
-function isString(value){
+/*
+ * Checks if value is classified as a String primitive or object.
+ * @param {any} value The value to check.
+ * @return {Boolean} Returns true if value is a string, else false.
+ */
+function isString(value) {
return value instanceof String || typeof value === 'string';
}
+/*
+ * Iterates over own enumerable string keyed properties of an object and invokes iteratee for each property.
+ * The iteratee is invoked with three arguments: (value, key, object).
+ * Iteratee functions may exit iteration early by explicitly returning false.
+ * @param {Object} object The object to iterate over.
+ * @param {Function} iteratee The function invoked per iteration.
+ * @return {Object} Returs object.
+ */
+function forOwn(object, iteratee) {
+ object = object || {};
+ for (var k in object) {
+ if (object.hasOwnProperty(k)) {
+ if (iteratee(object[k], k, object) === false) break;
+ }
+ }
+ return object;
+}
+
exports.isString = isString;
+exports.forOwn = forOwn;
diff --git a/test/error.js b/test/error.js
index e4530f893..363203c65 100644
--- a/test/error.js
+++ b/test/error.js
@@ -16,10 +16,10 @@ function test(promise, cb){
describe('error', function() {
it('should throw TokenizationError when tag illegal', function() {
- return test(engine.parseAndRender('{% -a %}', {}), function(err){
+ return test(engine.parseAndRender('{% . a %}', {}), function(err){
expect(err.name).to.equal('TokenizationError');
- expect(err.message).to.equal('illegal tag: {% -a %}');
- expect(err.input).to.equal('{% -a %}');
+ expect(err.message).to.equal('illegal tag: {% . a %}');
+ expect(err.input).to.equal('{% . a %}');
expect(err.line).to.equal(1);
});
});
diff --git a/test/filters.js b/test/filters.js
index 370c16355..9b699ac90 100644
--- a/test/filters.js
+++ b/test/filters.js
@@ -1,9 +1,8 @@
const chai = require("chai");
-const expect = chai.expect;
-chai.use(require("chai-as-promised"));
-
+const chaiAsPromised = require("chai-as-promised");
var liquid = require('..')(),
ctx;
+chai.use(chaiAsPromised);
function test(src, dst) {
ctx = {
@@ -20,57 +19,74 @@ function test(src, dst) {
category: 'bar'
}]
};
- return expect(liquid.parseAndRender(src, ctx)).to.eventually.equal(dst);
+ return liquid.parseAndRender(src, ctx).should.eventually.equal(dst);
}
describe('filters', function() {
- it('should support abs 1', () => test('{{ -3 | abs }}', '3'));
- it('should support abs 2', () => test('{{ arr[0] | abs }}', '2'));
+ describe('abs', function() {
+ it('should return 3 for -3', () => test('{{ -3 | abs }}', '3'));
+ it('should return 2 for arr[0]', () => test('{{ arr[0] | abs }}', '2'));
+ });
- it('should support append 1', () => test('{{ -3 | append: "abc" }}', '-3abc'));
- it('should support append 2', () => test('{{ "a" | append: foo }}', 'abar'));
+ describe('append', function() {
+ it('should return "-3abc" for -3, "abc"',
+ () => test('{{ -3 | append: "abc" }}', '-3abc'));
+ it('should return "abar" for "a",foo', () => test('{{ "a" | append: foo }}', 'abar'));
+ });
it('should support capitalize', () => test('{{ "i am good" | capitalize }}', 'I am good'));
- it('should support ceil 1', () => test('{{ 1.2 | ceil }}', '2'));
- it('should support ceil 2', () => test('{{ 2.0 | ceil }}', '2'));
- it('should support ceil 3', () => test('{{ "3.5" | ceil }}', '4'));
- it('should support ceil 4', () => test('{{ 183.357 | ceil }}', '184'));
+ describe('ceil', function() {
+ it('should return "2" for 1.2', () => test('{{ 1.2 | ceil }}', '2'));
+ it('should return "2" for 2.0', () => test('{{ 2.0 | ceil }}', '2'));
+ it('should return "4" for 3.5', () => test('{{ "3.5" | ceil }}', '4'));
+ it('should return "184" for 183.357', () => test('{{ 183.357 | ceil }}', '184'));
+ });
- describe('date', function(){
- it('should support %a %b %d %Y', function() {
+ describe('date', function() {
+ it('should support date: %a %b %d %Y', function() {
var str = ctx.date.toDateString();
return test('{{ date | date:"%a %b %d %Y"}}', str);
});
- it('should support "now"', function() {
- var year = (new Date()).getFullYear();
- var src = '{{ "now" | date: "%Y"}}';
- return expect(liquid.parseAndRender(src)).to.eventually.match(/\d{4}/);
+ it('should create a new Date when given "now"', function() {
+ return test('{{ "now" | date: "%Y"}}', (new Date).getFullYear().toString());
});
});
it('should support default', () => test('{{false |default: "a"}}', 'a'));
- it('should support divided_by 1', () => test('{{4 | divided_by: 2}}', '2'));
- it('should support divided_by 2', () => test('{{16 | divided_by: 4}}', '4'));
- it('should support divided_by 3', () => test('{{5 | divided_by: 3}}', '1'));
-
- it('should support downcase 1', () => test('{{ "Parker Moore" | downcase }}', 'parker moore'));
- it('should support downcase 2', () => test('{{ "apple" | downcase }}', 'apple'));
-
- it('should support escape 1', function() {
- return test('{{ "Have you read \'James & the Giant Peach\'?" | escape }}',
- 'Have you read 'James & the Giant Peach'?');
- });
- it('should support escape 2', function() {
- return test('{{ "Tetsuro Takara" | escape }}', 'Tetsuro Takara');
- });
- it('should support excape function', function() {
- return test('{{ func | escape }}', 'function () {}');
+ describe('divided_by', function() {
+ it('should return 2 for 4,2', () => test('{{4 | divided_by: 2}}', '2'));
+ it('should return 4 for 16,4', () => test('{{16 | divided_by: 4}}', '4'));
+ it('should return 1 for 5,3', () => test('{{5 | divided_by: 3}}', '1'));
});
- it('should support escape_once 1', () => test('{{ "1 < 2 & 3" | escape_once }}', '1 < 2 & 3'));
- it('should support escape_once 2', () => test('{{ "1 < 2 & 3" | escape_once }}', '1 < 2 & 3'));
+ describe('downcase', function() {
+ it('should return "parker moore" for "Parker Moore"',
+ () => test('{{ "Parker Moore" | downcase }}', 'parker moore'));
+ it('should return "apple" for "apple"',
+ () => test('{{ "apple" | downcase }}', 'apple'));
+ });
+
+ describe('escape', function() {
+ it('should escape \' and &', function() {
+ return test('{{ "Have you read \'James & the Giant Peach\'?" | escape }}',
+ 'Have you read 'James & the Giant Peach'?');
+ });
+ it('should escape normal string', function() {
+ return test('{{ "Tetsuro Takara" | escape }}', 'Tetsuro Takara');
+ });
+ it('should escape function', function() {
+ return test('{{ func | escape }}', 'function () {}');
+ });
+ });
+
+ describe('escape_once', function() {
+ it('should do escape', () =>
+ test('{{ "1 < 2 & 3" | escape_once }}', '1 < 2 & 3'));
+ it('should not escape twice',
+ () => test('{{ "1 < 2 & 3" | escape_once }}', '1 < 2 & 3'));
+ });
it('should support split/first', function() {
var src = '{% assign my_array = "apples, oranges, peaches, plums" | split: ", " %}' +
@@ -78,10 +94,12 @@ describe('filters', function() {
return test(src, 'apples');
});
- it('should support floor 1', () => test('{{ 1.2 | floor }}', '1'));
- it('should support floor 2', () => test('{{ 2.0 | floor }}', '2'));
- it('should support floor 3', () => test('{{ 183.357 | floor }}', '183'));
- it('should support floor 4', () => test('{{ "3.5" | floor }}', '3'));
+ describe('floor', function() {
+ it('should return "1" for 1.2', () => test('{{ 1.2 | floor }}', '1'));
+ it('should return "2" for 2.0', () => test('{{ 2.0 | floor }}', '2'));
+ it('should return "183" for 183.357', () => test('{{ 183.357 | floor }}', '183'));
+ it('should return "3" for 3.5', () => test('{{ "3.5" | floor }}', '3'));
+ });
it('should support join', function() {
var src = '{% assign beatles = "John, Paul, George, Ringo" | split: ", " %}' +
@@ -104,13 +122,19 @@ describe('filters', function() {
return test('{{posts | map: "category"}}', '["foo","bar"]');
});
- it('should support minus 1', () => test('{{ 4 | minus: 2 }}', '2'));
- it('should support minus 2', () => test('{{ 16 | minus: 4 }}', '12'));
- it('should support minus 3', () => test('{{ 183.357 | minus: 12 }}', '171.357'));
+ describe('minus', function() {
+ it('should return "2" for 4,2', () => test('{{ 4 | minus: 2 }}', '2'));
+ it('should return "12" for 16,4', () => test('{{ 16 | minus: 4 }}', '12'));
+ it('should return "171.357" for 183.357,12',
+ () => test('{{ 183.357 | minus: 12 }}', '171.357'));
+ });
- it('should support modulo 1', () => test('{{ 3 | modulo: 2 }}', '1'));
- it('should support modulo 2', () => test('{{ 24 | modulo: 7 }}', '3'));
- it('should support modulo 3', () => test('{{ 183.357 | modulo: 12 }}', '3.357'));
+ describe('modulo', function() {
+ it('should return "1" for 3,2', () => test('{{ 3 | modulo: 2 }}', '1'));
+ it('should return "3" for 24,7', () => test('{{ 24 | modulo: 7 }}', '3'));
+ it('should return "3.357" for 183.357,12',
+ () => test('{{ 183.357 | modulo: 12 }}', '3.357'));
+ });
it('should support string_with_newlines', function() {
var src = '{% capture string_with_newlines %}\n' +
@@ -124,9 +148,12 @@ describe('filters', function() {
return test(src, dst);
});
- it('should support plus 1', () => test('{{ 4 | plus: 2 }}', '6'));
- it('should support plus 2', () => test('{{ 16 | plus: 4 }}', '20'));
- it('should support plus 3', () => test('{{ 183.357 | plus: 12 }}', '195.357'));
+ describe('plus', function() {
+ it('should return "6" for 4,2', () => test('{{ 4 | plus: 2 }}', '6'));
+ it('should return "20" for 16,4', () => test('{{ 16 | plus: 4 }}', '20'));
+ it('should return "195.357" for 183.357,12',
+ () => test('{{ 183.357 | plus: 12 }}', '195.357'));
+ });
it('should support prepend', function() {
return test('{% assign url = "liquidmarkup.com" %}' +
@@ -160,26 +187,34 @@ describe('filters', function() {
'.moT rojaM ot lortnoc dnuorG');
});
- it('should support round 1', () => test('{{1.2|round}}', '1'));
- it('should support round 2', () => test('{{2.7|round}}', '3'));
- it('should support round 3', () => test('{{183.357|round: 2}}', '183.36'));
+ describe('round', function() {
+ it('should return "1" for 1.2', () => test('{{1.2|round}}', '1'));
+ it('should return "3" for 2.7', () => test('{{2.7|round}}', '3'));
+ it('should return "183.36" for 183.357,2',
+ () => test('{{183.357|round: 2}}', '183.36'));
+ });
it('should support rstrip', function() {
return test('{{ " So much room for activities! " | rstrip }}',
' So much room for activities!');
});
- it('should support size 1', () => test('{{ "Ground control to Major Tom." | size }}', '28'));
- it('should support size 2', function() {
- return test('{% assign my_array = "apples, oranges, peaches, plums"' +
- ' | split: ", " %}{{ my_array | size }}',
- '4');
+ describe('size', function() {
+ it('should return string length',
+ () => test('{{ "Ground control to Major Tom." | size }}', '28'));
+ it('should return array size', function() {
+ return test('{% assign my_array = "apples, oranges, peaches, plums"' +
+ ' | split: ", " %}{{ my_array | size }}',
+ '4');
+ });
});
- it('should support slice 1', () => test('{{ "Liquid" | slice: 0 }}', 'L'));
- it('should support slice 2', () => test('{{ "Liquid" | slice: 2 }}', 'q'));
- it('should support slice 3', () => test('{{ "Liquid" | slice: 2, 5 }}', 'quid'));
- it('should support slice 4', () => test('{{ "Liquid" | slice: -3, 2 }}', 'ui'));
+ describe('slice', function() {
+ it('should slice first char by 0', () => test('{{ "Liquid" | slice: 0 }}', 'L'));
+ it('should slice third char by 2', () => test('{{ "Liquid" | slice: 2 }}', 'q'));
+ it('should slice substr by 2,5', () => test('{{ "Liquid" | slice: 2, 5 }}', 'quid'));
+ it('should slice substr by -3,2', () => test('{{ "Liquid" | slice: -3, 2 }}', 'ui'));
+ });
it('should support sort', function() {
return test('{% assign my_array = "zebra, octopus, giraffe, Sally Snake"' +
@@ -201,12 +236,14 @@ describe('filters', function() {
'So much room for activities!');
});
- it('should support strip_tml 1', function() {
- return test('{{ "Have you read Ulysses?" | strip_html }}',
- 'Have you read Ulysses?');
- });
- it('should support strip_tml 2', function() {
- return test('{{"
< p >
{{arr | join: "_"}}
'); return engine.render(template, ctx).should.eventually.equal('-2_a
'); }); + it('should render accessive filters', function() { + var src = '{% assign my_array = "apples, oranges, peaches, plums" | split: ", " %}' + + '{{ my_array | first }}'; + return expect(engine.parseAndRender(src)).to.eventually.equal('apples'); + }); describe('#renderFile()', function() { it('should render file', function() { return engine.renderFile('/root/files/foo.html', ctx).should.eventually.equal('foo'); @@ -83,17 +88,17 @@ describe('liquid', function() { it('should use default extname', function() { return engine.renderFile('files/name', ctx).should.eventually.equal('My name is harttle.'); }); - it('should accept root with no trailing slash', function(){ + it('should accept root with no trailing slash', function() { engine = Liquid({ root: '/root', extname: '.html' }); return expect(engine.renderFile('files/foo.html')).to.eventually.equal('foo'); }); - it('should accept dot path', function(){ + it('should accept dot path', function() { return expect(engine.renderFile('./files/foo.html')).to.eventually.equal('foo'); }); - it('should accept double-dot path', function(){ + it('should accept double-dot path', function() { return expect(engine.renderFile('files/foo/../foo.html')).to.eventually.equal('foo'); }); }); diff --git a/test/scope.js b/test/scope.js index b3a11acdc..c15ee7e3e 100644 --- a/test/scope.js +++ b/test/scope.js @@ -1,27 +1,25 @@ -var chai = require("chai"); -var should = chai.should(); -var expect = chai.expect; +const chai = require("chai"); +const expect = chai.expect; var Scope = require('../src/scope.js'); describe('scope', function() { var scope, ctx; - beforeEach(function(){ + beforeEach(function() { ctx = { - foo: 'bar', - bar: ['a', {b: [1, 2]}] + foo: 'bar' }; scope = Scope.factory(ctx); }); - it('should get property', function() { - scope.get('foo').should.equal('bar'); + it('should get direct property', function() { + expect(scope.get('foo')).equal('bar'); }); it('should get undefined property', function() { - function fn(){ - scope.get('notdefined'); - } + function fn() { + scope.get('notdefined'); + } expect(fn).to.not.throw(); expect(scope.get('notdefined')).to.equal(undefined); expect(scope.get('')).to.equal(undefined); @@ -29,47 +27,46 @@ describe('scope', function() { }); it('should throw undefined in strict mode', function() { - scope = Scope.factory(ctx, { - strict: true - }); - function fn(){ - scope.get('notdefined'); - } - expect(fn).to.throw(/undefined variable: notdefined/); + scope = Scope.factory(ctx, { + strict: true + }); + + function fn() { + scope.get('notdefined'); + } + expect(fn).to.throw(/undefined variable: notdefined/); }); - it('should get all property', function() { - scope.get().should.deep.equal(ctx); + it('should get all properties when arguments empty', function() { + expect(scope.get()).deep.equal(ctx); }); - it('should set property', function() { - scope.set('foo', 'FOO'); - scope.get('foo').should.equal('FOO'); - }); - - it('should set child property', function() { + it('should access child property via dot syntax', function() { scope.set('oo.bar', 'FOO'); - scope.get('oo.bar').should.equal('FOO'); + expect(scope.get('oo.bar')).to.equal('FOO'); }); - it('should get desendent property', function() { - scope.get('bar[0]').should.equal('a'); - scope.get('bar[1].b').should.deep.equal([1, 2]); - scope.get('bar[1].b[1]').should.equal(2); + it('should access child property via [