feat: renderSync, parseAndRenderSync and renderFileSync, see #48

This commit is contained in:
harttle
2019-08-26 10:15:43 -05:00
committed by Jun Yang
parent 8028f82499
commit 7fb01ad69a
66 changed files with 896 additions and 272 deletions
+11
View File
@@ -0,0 +1,11 @@
import { expect } from 'chai'
import { Liquid } from '../..'
describe('#evalValueSync()', function () {
var engine: Liquid
beforeEach(() => { engine = new Liquid() })
it('should throw when scope undefined', async function () {
return expect(() => engine.evalValueSync('{{"foo"}}', null as any)).to.throw(/context not defined/)
})
})
+1 -1
View File
@@ -6,6 +6,6 @@ describe('.evalValue()', function () {
beforeEach(() => { engine = new Liquid() })
it('should throw when scope undefined', async function () {
return expect(() => engine.evalValue('{{"foo"}}', null as any)).to.throw(/context not defined/)
return expect(engine.evalValue('{{"foo"}}', null as any)).to.be.rejectedWith(/context not defined/)
})
})
+5
View File
@@ -81,4 +81,9 @@ describe('tags/assign', function () {
return expect(html).to.equal('12 2')
})
})
it('should support sync', function () {
const src = '{% assign foo="bar" %}{{foo}}'
const html = liquid.parseAndRenderSync(src)
return expect(html).to.equal('bar')
})
})
+5
View File
@@ -32,4 +32,9 @@ describe('tags/capture', function () {
return expect(liquid.parseAndRender(src))
.to.be.rejectedWith(/tag .* not closed/)
})
it('should support sync', function () {
const src = '{% capture f %}{{"a" | capitalize}}{%endcapture%}{{f}}'
const html = liquid.parseAndRenderSync(src)
return expect(html).to.equal('A')
})
})
+16
View File
@@ -48,4 +48,20 @@ describe('tags/case', function () {
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('d')
})
describe('sync support', function () {
it('should hit the specified case', function () {
const src = '{% case "foo"%}' +
'{% when "foo" %}foo{% when "bar"%}bar' +
'{%endcase%}'
const html = liquid.parseAndRenderSync(src)
return expect(html).to.equal('foo')
})
it('should support else branch', function () {
const src = '{% case "a" %}' +
'{% when "b" %}b{% when "c"%}c{%else %}d' +
'{%endcase%}'
const html = liquid.parseAndRenderSync(src)
return expect(html).to.equal('d')
})
})
})
+7
View File
@@ -31,4 +31,11 @@ describe('tags/comment', function () {
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('')
})
describe('sync support', function () {
it('should ignore plain string', function () {
const src = 'My name is {% comment %}super{% endcomment %} Shopify.'
const html = liquid.parseAndRenderSync(src)
return expect(html).to.equal('My name is Shopify.')
})
})
})
+5
View File
@@ -35,4 +35,9 @@ describe('tags/cycle', function () {
const html = await liquid.parseAndRender(src, ctx)
return expect(html).to.equal('121')
})
it('should support sync', function () {
const src = "{% cycle '1', '2', '3' %}"
const html = liquid.parseAndRenderSync(src + src + src + src)
return expect(html).to.equal('1231')
})
})
+57 -30
View File
@@ -6,13 +6,13 @@ import { Scope } from '../../../../src/context/scope'
use(chaiAsPromised)
describe('tags/for', function () {
let liquid: Liquid, ctx: Scope
let liquid: Liquid, scope: Scope
before(function () {
liquid = new Liquid()
liquid.registerTag('throwingTag', {
render: function () { throw new Error('intended render error') }
})
ctx = {
scope = {
one: 1,
// eslint-disable-next-line
strObj: new String(''),
@@ -25,30 +25,30 @@ describe('tags/for', function () {
})
it('should support array', async function () {
const src = '{%for c in alpha%}{{c}}{%endfor%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('abc')
})
it('should support object', async function () {
const src = '{%for item in obj%}{{item[0]}},{{item[1]}}-{%else%}b{%endfor%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('foo,bar-coo,haa-')
})
it('should output forloop', async function () {
const src = '{%for i in (1..1)%}{{forloop}}{%endfor%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('{"i":0,"length":1}')
})
describe('illegal', function () {
it('should reject when for not closed', function () {
const src = '{%for c in alpha%}{{c}}'
return expect(liquid.parseAndRender(src, ctx))
return expect(liquid.parseAndRender(src, scope))
.to.be.rejectedWith(/tag .* not closed/)
})
it('should reject when inner templates rejected', function () {
const src = '{%for c in alpha%}{%throwingTag%}{%endfor%}'
return expect(liquid.parseAndRender(src, ctx))
return expect(liquid.parseAndRender(src, scope))
.to.be.rejectedWith(/intended render error/)
})
})
@@ -56,38 +56,38 @@ describe('tags/for', function () {
describe('else', function () {
it('should goto else for empty array', async function () {
const src = '{%for c in emptyArray%}a{%else%}b{%endfor%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('b')
})
it('should treat non-empty string as one single element', async function () {
const src = '{%for c in "abc"%}x{{c}}{%else%}y{%endfor%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('xabc')
})
it('should goto else for empty string', async function () {
const src = '{%for c in ""%}a{%else%}b{%endfor%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('b')
})
it('should goto else for empty string object', async function () {
// it should be false although `new String` is none-conform
const src = '{%for c in strObj%}a{%else%}b{%endfor%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('b')
})
it('should goto else for empty object', async function () {
const src = '{%for c in emptyObj%}a{%else%}b{%endfor%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('b')
})
it('should goto else for null-prototyped object', async function () {
const src = '{%for c in nullProtoObj%}a{%else%}b{%endfor%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('b')
})
})
@@ -102,7 +102,7 @@ describe('tags/for', function () {
const dst = 'true.1.0.false.3.3.2a\n' +
'false.2.1.false.3.2.1b\n' +
'false.3.2.true.3.1.0c\n'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal(dst)
})
@@ -111,7 +111,7 @@ describe('tags/for', function () {
const src = '{% for i in (1..5) %}' +
'{% if i == 4 %}continue{% continue %}{% endif %}{{i}}' +
'{% endfor %}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('123continue5')
})
it('should output contents before continue', async function () {
@@ -119,7 +119,7 @@ describe('tags/for', function () {
'{% if i == 4 %}continue{% continue %}{% endif %}' +
'{{ i }}' +
'{% endfor %}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('123continue5')
})
})
@@ -129,7 +129,7 @@ describe('tags/for', function () {
'{% if i == 4 %}{% break %}{% endif %}' +
'{{ i }}' +
'{% endfor %}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('123')
})
it('should output contents before break', async function () {
@@ -137,7 +137,7 @@ describe('tags/for', function () {
'{% if i == 4 %}breaking{% break %}{% endif %}' +
'{{ i }}' +
'{% endfor %}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('123breaking')
})
})
@@ -145,22 +145,22 @@ describe('tags/for', function () {
describe('limit', function () {
it('should support for with limit', async function () {
const src = '{% for i in (1..5) limit:2 %}{{ i }}{% endfor %}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('12')
})
it('should set forloop.last properly', async function () {
const src = '{%for i in (1..10) limit:2 offset:3%}{{forloop.last}} {%endfor%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('false true ')
})
it('should set forloop.first properly', async function () {
const src = '{%for i in (1..10) limit:2 offset:3%}{{forloop.first}} {%endfor%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('true false ')
})
it('should set forloop.length properly', function () {
const src = '{%for i in (1..10) limit:2 offset:3%}{{forloop.length}} {%endfor%}'
return expect(liquid.parseAndRender(src, ctx))
return expect(liquid.parseAndRender(src, scope))
.to.eventually.equal('2 2 ')
})
})
@@ -168,27 +168,27 @@ describe('tags/for', function () {
describe('offset', function () {
it('should support offset with limit', async function () {
const src = '{% for i in (1..10) limit:2 offset:5%}{{ i }}{% endfor %}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('67')
})
it('should set index properly', async function () {
const src = '{%for i in (1..10) limit:2 offset:3%}{{forloop.index}} {%endfor%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('1 2 ')
})
it('should set index0 properly', async function () {
const src = '{%for i in (1..10) limit:2 offset:3%}{{forloop.index0}} {%endfor%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('0 1 ')
})
it('should set rindex properly', async function () {
const src = '{%for i in (1..10) limit:2 offset:3%}{{forloop.rindex}} {%endfor%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('2 1 ')
})
it('should set rindex0 properly', async function () {
const src = '{%for i in (1..10) limit:2 offset:3%}{{forloop.rindex0}} {%endfor%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('1 0 ')
})
})
@@ -196,20 +196,47 @@ describe('tags/for', function () {
describe('reverse', function () {
it('should support for reversed in the last position', async function () {
const src = '{% for i in (1..5) limit:2 reversed %}{{ i }}{% endfor %}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('21')
})
it('should support for reversed in the first position', async function () {
const src = '{% for i in (1..5) reversed limit:2 %}{{ i }}{% endfor %}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('21')
})
it('should support for reversed in the middle position', async function () {
const src = '{% for i in (1..5) offset:2 reversed limit:4 %}{{ i }}{% endfor %}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('543')
})
})
describe('sync', function () {
it('should support sync', function () {
const src = '{% for i in (1..5) %}{{i}}{%endfor%}'
const html = liquid.parseAndRenderSync(src)
return expect(html).to.equal('12345')
})
it('should output contents before break', function () {
const src = '{% for i in (1..5) %}' +
'{% if i == 4 %}breaking{% break %}{% endif %}' +
'{{ i }}' +
'{% endfor %}'
const html = liquid.parseAndRenderSync(src, scope)
return expect(html).to.equal('123breaking')
})
it('should support for with continue', function () {
const src = '{% for i in (1..5) %}' +
'{% if i == 4 %}continue{% continue %}{% endif %}{{i}}' +
'{% endfor %}'
const html = liquid.parseAndRenderSync(src, scope)
return expect(html).to.equal('123continue5')
})
it('should goto else for empty array', async function () {
const src = '{%for c in emptyArray%}a{%else%}b{%endfor%}'
const html = await liquid.parseAndRenderSync(src, scope)
return expect(html).to.equal('b')
})
})
})
+24 -19
View File
@@ -3,7 +3,7 @@ import { expect } from 'chai'
describe('tags/if', function () {
const liquid = new Liquid()
const ctx = {
const scope = {
one: 1,
two: 2,
emptyString: '',
@@ -12,52 +12,52 @@ describe('tags/if', function () {
it('should throw if not closed', function () {
const src = '{% if false%}yes'
return expect(liquid.parseAndRender(src, ctx))
return expect(liquid.parseAndRender(src, scope))
.to.be.rejectedWith(/tag {% if false%} not closed/)
})
it('should support nested', async function () {
const src = '{%if false%}{%if true%}{%else%}a{%endif%}{%endif%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('')
})
describe('single value as condition', function () {
it('should support boolean', async function () {
const src = '{% if false %}1{%elsif true%}2{%else%}3{%endif%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('2')
})
it('should treat Array truthy', async function () {
const src = '{%if emptyArray%}a{%endif%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('a')
})
it('should return true if empty string', async function () {
const src = '{%if emptyString%}a{%endif%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('a')
})
})
describe('expression as condition', function () {
it('should support ==', async function () {
const src = '{% if 2 == 3 %}yes{%else%}no{%endif%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('no')
})
it('should support >=', async function () {
const src = '{% if 1 >= 2 and one<two %}a{%endif%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('')
})
it('should support !=', async function () {
const src = '{% if one != two %}yes{%else%}no{%endif%}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('yes')
})
it('should support value and expression', async function () {
const src = `X{%if version and version != '' %}x{{version}}y{%endif%}Y`
const ctx = { 'version': '' }
const html = await liquid.parseAndRender(src, ctx)
const scope = { 'version': '' }
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('XY')
})
it('should evaluate right to left', async function () {
@@ -69,50 +69,55 @@ describe('tags/if', function () {
describe('comparasion to null', function () {
it('should evaluate false for null < 10', async function () {
const src = '{% if null < 10 %}yes{% else %}no{% endif %}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('no')
})
it('should evaluate false for null > 10', async function () {
const src = '{% if null > 10 %}yes{% else %}no{% endif %}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('no')
})
it('should evaluate false for null <= 10', async function () {
const src = '{% if null <= 10 %}yes{% else %}no{% endif %}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('no')
})
it('should evaluate false for null >= 10', async function () {
const src = '{% if null >= 10 %}yes{% else %}no{% endif %}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('no')
})
it('should evaluate false for 10 < null', async function () {
const src = '{% if 10 < null %}yes{% else %}no{% endif %}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('no')
})
it('should evaluate false for 10 > null', async function () {
const src = '{% if 10 > null %}yes{% else %}no{% endif %}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('no')
})
it('should evaluate false for 10 <= null', async function () {
const src = '{% if 10 <= null %}yes{% else %}no{% endif %}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('no')
})
it('should evaluate false for 10 >= null', async function () {
const src = '{% if 10 >= null %}yes{% else %}no{% endif %}'
const html = await liquid.parseAndRender(src, ctx)
const html = await liquid.parseAndRender(src, scope)
return expect(html).to.equal('no')
})
})
it('should support sync', function () {
const src = '{%if true%}true{%else%}false{%endif%}'
const html = liquid.parseAndRenderSync(src, scope)
return expect(html).to.equal('true')
})
})
+35
View File
@@ -171,4 +171,39 @@ describe('tags/include', function () {
return expect(html).to.equal('Xchild with redY')
})
})
describe('sync support', function () {
it('should support quoted string', function () {
mock({
'/current.html': 'bar{% include "bar/foo.html" %}bar',
'/bar/foo.html': 'foo'
})
const html = liquid.renderFileSync('/current.html')
return expect(html).to.equal('barfoobar')
})
it('should support template string', function () {
mock({
'/current.html': 'bar{% include name" %}bar',
'/bar/foo.html': 'foo'
})
const html = liquid.renderFileSync('/current.html', { name: '/bar/foo.html' })
return expect(html).to.equal('barfoobar')
})
it('should support include: with', function () {
mock({
'/with.html': '{% include "color" with "red", shape: "rect" %}',
'/color.html': 'color:{{color}}, shape:{{shape}}'
})
const html = liquid.renderFileSync('with.html')
return expect(html).to.equal('color:red, shape:rect')
})
it('should support filename with extention', function () {
mock({
'/parent.html': 'X{% include child.html color:"red" %}Y',
'/child.html': 'child with {{color}}'
})
const staticLiquid = new Liquid({ dynamicPartials: false, root: '/' })
const html = staticLiquid.renderFileSync('parent.html')
return expect(html).to.equal('Xchild with redY')
})
})
})
+9
View File
@@ -133,4 +133,13 @@ describe('tags/layout', function () {
return expect(html).to.equal('blackA')
})
})
it('should support sync', function () {
mock({
'/grand.html': 'X{%block a%}G{%endblock%}Y',
'/parent.html': '{%layout "grand" %}{%block a%}P{%endblock%}',
'/main.html': '{%layout "parent"%}{%block a%}A{%endblock%}'
})
const html = liquid.renderFileSync('/main.html')
return expect(html).to.equal('XAY')
})
})
+7 -3
View File
@@ -3,20 +3,24 @@ import { expect } from 'chai'
describe('tags/raw', function () {
const liquid = new Liquid()
it('should support raw 1', async function () {
it('should throw when not closed', async function () {
const p = liquid.parseAndRender('{% raw%}')
return expect(p).be.rejectedWith(/{% raw%} not closed/)
})
it('should support raw 2', async function () {
it('should output filters as it is', async function () {
const src = '{% raw %}{{ 5 | plus: 6 }}{% endraw %} is equal to 11.'
const dst = '{{ 5 | plus: 6 }} is equal to 11.'
const html = await liquid.parseAndRender(src)
return expect(html).to.equal(dst)
})
it('should support raw 3', async function () {
it('should preserve blank characters', async function () {
const src = '{% raw %}\n{{ foo}} \n{% endraw %}'
const dst = '\n{{ foo}} \n'
const html = await liquid.parseAndRender(src)
return expect(html).to.equal(dst)
})
it('should support sync', function () {
const html = liquid.parseAndRenderSync('{% raw %}{{foo}}{% endraw %}')
return expect(html).to.equal('{{foo}}')
})
})
+14
View File
@@ -107,4 +107,18 @@ describe('tags/tablerow', function () {
return expect(html).to.equal(dst)
})
})
describe('sync support', function () {
it('should support tablerow', function () {
const src = '{% tablerow i in (1..3)%}{{ i }}{% endtablerow %}'
const dst = '<tr class="row1"><td class="col1">1</td><td class="col2">2</td><td class="col3">3</td></tr>'
const html = liquid.parseAndRenderSync(src)
expect(html).to.equal(dst)
})
it('should support empty tablerow', function () {
const src = '{% tablerow i in "" cols:2 %}{{ i }}{% endtablerow %}'
const dst = ''
const html = liquid.parseAndRenderSync(src)
return expect(html).to.equal(dst)
})
})
})
+12
View File
@@ -34,4 +34,16 @@ describe('tags/unless', function () {
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('')
})
describe('sync support', function () {
it('should render else when predicate yields true', function () {
const src = '{% unless 0 %}yes{%else%}no{%endunless%}'
const html = liquid.parseAndRenderSync(src)
expect(html).to.equal('no')
})
it('should render unless when predicate yields false', function () {
const src = '{% unless false %}yes{%else%}no{%endunless%}'
const html = liquid.parseAndRenderSync(src)
expect(html).to.equal('yes')
})
})
})
+4
View File
@@ -49,6 +49,10 @@ describe('drop/drop', function () {
const html = await liquid.parseAndRender(`{{obj.name}}`, { obj: new PromiseDrop() })
expect(html).to.equal('NAME')
})
it('should resolve before calling filters', async function () {
const html = await liquid.parseAndRender(`{{obj.name | downcase}}`, { obj: new PromiseDrop() })
expect(html).to.equal('name')
})
it('should support promise returned by liquidMethodMissing', async function () {
const html = await liquid.parseAndRender(`{{obj.foo}}`, { obj: new PromiseDrop() })
expect(html).to.equal('FOO')
+80 -29
View File
@@ -3,36 +3,87 @@ import { Liquid } from '../../../src/liquid'
import { mock, restore } from '../../stub/mockfs'
describe('LiquidOptions#cache', function () {
let engine: Liquid
beforeEach(function () {
engine = new Liquid({
root: '/root/',
extname: '.html'
})
mock({ '/root/files/foo.html': 'foo' })
})
afterEach(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 = new Liquid({
root: '/root/',
extname: '.html',
cache: true
describe('#renderFile', function () {
it('should be disabled by default', async function () {
const engine = new Liquid({
root: '/root/',
extname: '.html'
})
mock({ '/root/files/foo.html': 'foo' })
const x = await engine.renderFile('files/foo')
expect(x).to.equal('foo')
mock({ '/root/files/foo.html': 'bar' })
const y = await engine.renderFile('files/foo')
expect(y).to.equal('bar')
})
it('should respect cache=true option', async function () {
const engine = new Liquid({
root: '/root/',
extname: '.html',
cache: true
})
mock({ '/root/files/foo.html': 'foo' })
const x = await engine.renderFile('files/foo')
expect(x).to.equal('foo')
mock({ '/root/files/foo.html': 'bar' })
const y = await engine.renderFile('files/foo')
expect(y).to.equal('foo')
})
it('should not cache not exist file', async function () {
const engine = new Liquid({
root: '/root/',
extname: '.html',
cache: true
})
try { await engine.renderFile('foo') } catch (err) {}
mock({ '/root/foo.html': 'foo' })
const y = await engine.renderFile('foo')
expect(y).to.equal('foo')
})
})
describe('#renderFileSync', function () {
it('should be disabled by default', function () {
const engine = new Liquid({
root: '/root/',
extname: '.html'
})
mock({ '/root/foo.html': 'foo' })
const x = engine.renderFileSync('foo')
expect(x).to.equal('foo')
mock({ '/root/foo.html': 'bar' })
const y = engine.renderFileSync('foo')
expect(y).to.equal('bar')
})
it('should respect cache=true option', function () {
const engine = new Liquid({
root: '/root/',
extname: '.html',
cache: true
})
mock({ '/root/foo.html': 'foo' })
const x = engine.renderFileSync('foo')
expect(x).to.equal('foo')
mock({ '/root/foo.html': 'bar' })
const y = engine.renderFileSync('foo')
expect(y).to.equal('foo')
})
it('should not cache not exist file', async function () {
const engine = new Liquid({
root: '/root/',
extname: '.html',
cache: true
})
try { engine.renderFileSync('foo') } catch (err) {}
mock({ '/root/foo.html': 'foo' })
const y = await engine.renderFile('foo')
expect(y).to.equal('foo')
})
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'))
})
})
+2 -2
View File
@@ -53,14 +53,14 @@ describe('Liquid', function () {
})
after(restore)
it('should render single template', function (done) {
render.call({ root: '.' }, 'foo', null as any, (err: Error | null, result: string | undefined) => {
render.call({ root: '/root' }, 'foo', null as any, (err: Error | null, result: string | undefined) => {
if (err) return done(err)
expect(result).to.equal('foo')
done()
})
})
it('should render single template with Array-typed root', function (done) {
render.call({ root: ['.'] }, 'foo', null as any, (err: Error | null, result: string | undefined) => {
render.call({ root: ['/root'] }, 'foo', null as any, (err: Error | null, result: string | undefined) => {
if (err) return done(err)
expect(result).to.equal('foo')
done()
+18 -11
View File
@@ -1,17 +1,24 @@
import { test, liquid } from '../../stub/render'
import { expect } from 'chai'
import { Liquid } from '../../../src/liquid'
describe('liquid#registerFilter()', function () {
const liquid = new Liquid()
describe('object arguments', function () {
liquid.registerFilter('obj_test', function () {
return JSON.stringify(arguments)
liquid.registerFilter('obj_test', function (...args) {
return JSON.stringify(args)
})
it('should support object', async () => {
const src = `{{ "a" | obj_test: k1: "v1", k2: foo }}`,
const dst = '["a",["k1","v1"],["k2","bar"]]'
const html = await liquid.parseAndRender(src, { foo: 'bar' })
return expect(html).to.equal(dst)
})
it('should support mixed object', async () => {
const src = `{{ "a" | obj_test: "something", k1: "v1", k2: foo }}`,
const dst = '["a","something",["k1","v1"],["k2","bar"]]'
const html = await liquid.parseAndRender(src, { foo: 'bar' })
return expect(html).to.equal(dst)
})
it('should support object', () => test(
`{{ "a" | obj_test: k1: "v1", k2: foo }}`,
'{"0":"a","1":["k1","v1"],"2":["k2","bar"]}'
))
it('should support mixed object', () => test(
`{{ "a" | obj_test: "something", k1: "v1", k2: foo }}`,
'{"0":"a","1":"something","2":["k1","v1"],"3":["k2","bar"]}'
))
})
})
+43
View File
@@ -1,4 +1,5 @@
import { expect } from 'chai'
import { RenderError } from '../../../src/util/error'
import { Liquid } from '../../../src/liquid'
import * as path from 'path'
import { mock, restore } from '../../stub/mockfs'
@@ -264,4 +265,46 @@ describe('error', function () {
expect(err.stack).to.match(/at .*:\d+:\d+\)/)
})
})
describe('sync support', function () {
let engine
beforeEach(function () {
engine = new Liquid({
root: '/'
})
engine.registerTag('throwingTag', {
render: function () {
throw new Error('intended render error')
}
})
})
it('should throw RenderError when tag throws', function () {
const src = '{%throwingTag%}'
expect(() => engine.parseAndRenderSync(src))
.to.throw(RenderError, /intended render error/)
})
it('should contain original error info for {% include %}', function () {
const origin = ['1st', '2nd', '3rd', 'X{%throwingTag%} Y', '5th', '6th', '7th']
mock({
'/throwing-tag.html': origin.join('\n')
})
const html = '{%include "throwing-tag.html"%}'
const message = [
' 2| 2nd',
' 3| 3rd',
'>> 4| X{%throwingTag%} Y',
' 5| 5th',
' 6| 6th',
' 7| 7th',
'RenderError'
]
try {
engine.parseAndRenderSync(html)
throw new Error('expected throw')
} catch (err) {
expect(err.message).to.equal(`intended render error, file:${path.resolve('/throwing-tag.html')}, line:4, col:2`)
expect(err.stack).to.contain(message.join('\n'))
expect(err.name).to.equal('RenderError')
}
})
})
})
+9 -3
View File
@@ -8,8 +8,7 @@ interface FileDescriptor {
}
let files: { [path: string]: FileDescriptor } = {}
const readFile = fs.readFile
const exists = fs.exists
const { readFile, exists, readFileSync, existsSync } = fs
export function mock (options: { [path: string]: (string | FileDescriptor) }) {
forOwn(options, (val, key) => {
@@ -18,19 +17,26 @@ export function mock (options: { [path: string]: (string | FileDescriptor) }) {
: val as FileDescriptor
})
fs.readFile = async function (path) {
return fs.readFileSync(path)
}
fs.readFileSync = function (path) {
const file = files[path]
if (file === undefined) throw new Error('ENOENT')
if (file.mode === '0000') throw new Error('EACCES')
return file.content
}
fs.exists = async function (path: string) {
return fs.existsSync(path)
}
fs.existsSync = function (path: string) {
return !!files[path]
}
}
export function restore () {
files = {}
fs.readFileSync = readFileSync
fs.existsSync = existsSync
fs.readFile = readFile
fs.exists = exists
}
+1 -1
View File
@@ -8,7 +8,7 @@ export const ctx = {
foo: 'bar',
arr: [-2, 'a'],
obj: { foo: 'bar' },
func: function () {},
func: function () {}, // eslint-disable-line
posts: [{ category: 'foo' }, { category: 'bar' }],
products: [
{ title: 'Vacuum', type: 'living room' },
+31
View File
@@ -59,6 +59,12 @@ describe('fs/browser', function () {
})
})
describe('#existsSync()', () => {
it('should always return true', function () {
expect(fs.existsSync('/foo/bar')).to.equal(true)
})
})
describe('#readFile()', () => {
let server: sinon.SinonFakeServer
beforeEach(() => {
@@ -87,4 +93,29 @@ describe('fs/browser', function () {
return result
})
})
describe('#readFileSync()', () => {
let server: sinon.SinonFakeServer
beforeEach(() => {
server = sinon.fakeServer.create()
server.autoRespond = true
server.respondWith(
'GET', 'https://example.com/views/hello.html',
[200, { 'Content-Type': 'text/plain' }, 'hello {{name}}']
);
(global as any).XMLHttpRequest = sinon.useFakeXMLHttpRequest()
})
afterEach(() => {
server.restore()
delete (global as any).XMLHttpRequest
})
it('should get corresponding text', function () {
const html = fs.readFileSync('https://example.com/views/hello.html')
return expect(html).to.equal('hello {{name}}')
})
it('should throw 404', () => {
return expect(() => fs.readFileSync('https://example.com/not/exist.html'))
.to.throw('Not Found')
})
})
})
+23 -6
View File
@@ -6,7 +6,7 @@ import * as chaiAsPromised from 'chai-as-promised'
use(chaiAsPromised)
describe('fs', function () {
describe('#resolve()', function () {
describe('.resolve()', function () {
it('should resolve based on root', async function () {
const filepath = fs.resolve('/foo', 'bar.html', '.liquid')
const expected = path.resolve('/foo/bar.html')
@@ -18,23 +18,40 @@ describe('fs', function () {
return expect(filepath).to.equal(expected)
})
})
describe('#exists', () => {
describe('.existsSync', () => {
it('should resolve as false if not exists', () => {
expect(fs.existsSync('/foo/bar')).to.be.false
})
it('should resolve as true if exists', () => {
expect(fs.existsSync(__filename)).to.be.true
})
})
describe('.exists', () => {
it('should resolve as false if not exists', async () => {
const result = await fs.exists('/foo/bar')
return expect(result).to.be.false
expect(result).to.be.false
})
it('should resolve as true if exists', async () => {
const result = await fs.exists(__filename)
return expect(result).to.be.true
expect(result).to.be.true
})
})
describe('#readFile', function () {
describe('.readFileSync', function () {
it('should throw when not exist', function () {
return expect(() => fs.readFileSync('/foo/bar')).to.throw('ENOENT')
})
it('should read content if exists', function () {
const content = fs.readFileSync(__filename)
expect(content).to.contain('should read content if exists')
})
})
describe('.readFile', function () {
it('should throw when not exist', function () {
return expect(fs.readFile('/foo/bar')).to.rejectedWith('ENOENT')
})
it('should read content if exists', async function () {
const content = await fs.readFile(__filename)
return expect(content).to.contain('should read content if exists')
expect(content).to.contain('should read content if exists')
})
})
})
+29 -24
View File
@@ -17,51 +17,56 @@ describe('Expression', function () {
})
it('should throw when context not defined', async function () {
return expect(() => new Expression().value()).to.throw(/context not defined/)
return expect(new Expression().value()).to.be.rejectedWith(/context not defined/)
})
it('should eval simple expression', async function () {
expect(new Expression('1 < 2').value(ctx)).to.equal(true)
expect(new Expression('2 <= 2').value(ctx)).to.equal(true)
expect(new Expression('one <= two').value(ctx)).to.equal(true)
expect(new Expression('x contains "x"').value(ctx)).to.equal(false)
expect(new Expression('x contains "X"').value(ctx)).to.equal(true)
expect(new Expression('1 contains "x"').value(ctx)).to.equal(false)
expect(new Expression('y contains "x"').value(ctx)).to.equal(false)
expect(new Expression('z contains "x"').value(ctx)).to.equal(false)
expect(new Expression('(1..5) contains 3').value(ctx)).to.equal(true)
expect(new Expression('(1..5) contains 6').value(ctx)).to.equal(false)
expect(new Expression('"<=" == "<="').value(ctx)).to.equal(true)
expect(await new Expression('1 < 2').value(ctx)).to.equal(true)
expect(await new Expression('1 < 2').value(ctx)).to.equal(true)
expect(await new Expression('2 <= 2').value(ctx)).to.equal(true)
expect(await new Expression('one <= two').value(ctx)).to.equal(true)
expect(await new Expression('x contains "x"').value(ctx)).to.equal(false)
expect(await new Expression('x contains "X"').value(ctx)).to.equal(true)
expect(await new Expression('1 contains "x"').value(ctx)).to.equal(false)
expect(await new Expression('y contains "x"').value(ctx)).to.equal(false)
expect(await new Expression('z contains "x"').value(ctx)).to.equal(false)
expect(await new Expression('(1..5) contains 3').value(ctx)).to.equal(true)
expect(await new Expression('(1..5) contains 6').value(ctx)).to.equal(false)
expect(await new Expression('"<=" == "<="').value(ctx)).to.equal(true)
})
describe('complex expression', function () {
it('should support value or value', async function () {
expect(new Expression('false or true').value(ctx)).to.equal(true)
expect(await new Expression('false or true').value(ctx)).to.equal(true)
})
it('should support < and contains', async function () {
expect(new Expression('1 < 2 and x contains "x"').value(ctx)).to.equal(false)
expect(await new Expression('1 < 2 and x contains "x"').value(ctx)).to.equal(false)
})
it('should support < or contains', async function () {
expect(new Expression('1 < 2 or x contains "x"').value(ctx)).to.equal(true)
expect(await new Expression('1 < 2 or x contains "x"').value(ctx)).to.equal(true)
})
it('should support value and !=', async function () {
expect(new Expression('empty and empty != ""').value(ctx)).to.equal(false)
expect(await new Expression('empty and empty != ""').value(ctx)).to.equal(false)
})
it('should recognize quoted value', async function () {
expect(new Expression('">"').value(ctx)).to.equal('>')
expect(await new Expression('">"').value(ctx)).to.equal('>')
})
it('should evaluate from right to left', function () {
expect(new Expression('true or false and false').value(ctx)).to.equal(true)
expect(new Expression('true and false and false or true').value(ctx)).to.equal(false)
it('should evaluate from right to left', async function () {
expect(await new Expression('true or false and false').value(ctx)).to.equal(true)
expect(await new Expression('true and false and false or true').value(ctx)).to.equal(false)
})
it('should recognize property access', function () {
it('should recognize property access', async function () {
const ctx = new Context({ obj: { foo: true } })
expect(new Expression('obj["foo"] and true').value(ctx)).to.equal(true)
expect(await new Expression('obj["foo"] and true').value(ctx)).to.equal(true)
})
})
it('should eval range expression', async function () {
expect(new Expression('(2..4)').value(ctx)).to.deep.equal([2, 3, 4])
expect(new Expression('(two..4)').value(ctx)).to.deep.equal([2, 3, 4])
expect(await new Expression('(2..4)').value(ctx)).to.deep.equal([2, 3, 4])
expect(await new Expression('(two..4)').value(ctx)).to.deep.equal([2, 3, 4])
})
it('should support sync', function () {
expect(new Expression('empty and empty != ""').valueSync(ctx)).to.equal(false)
})
})
+5 -5
View File
@@ -5,20 +5,20 @@ import { expect } from 'chai'
describe('Value', function () {
it('should eval number variable', async function () {
const ctx = new Context({ one: 1 })
expect(new Value('one').value(ctx)).to.equal(1)
expect(new Value('one').valueSync(ctx)).to.equal(1)
})
it('question mark should be valid variable name', async function () {
const ctx = new Context({ 'has_value?': true })
expect(new Value('has_value?').value(ctx)).to.equal(true)
expect(new Value('has_value?').valueSync(ctx)).to.equal(true)
})
it('should eval string variable', async function () {
const ctx = new Context({ x: 'XXX' })
expect(new Value('x').value(ctx)).to.equal('XXX')
expect(new Value('x').valueSync(ctx)).to.equal('XXX')
})
it('should eval null literal', async function () {
expect(new Value('null').value({})).to.be.null
expect(new Value('null').valueSync({})).to.be.null
})
it('should eval nil literal', async function () {
expect(new Value('nil').value({})).to.be.null
expect(new Value('nil').valueSync({})).to.be.null
})
})
+10
View File
@@ -60,4 +60,14 @@ describe('filter', function () {
new Filter('/', [], false)
}).to.not.throw()
})
it('should support sync', function () {
Filter.register('add', (a, b) => a + b)
expect(new Filter('add', ['2'], false).renderSync(3, ctx)).to.equal(5)
})
it('should support key value pairs', function () {
Filter.register('add', (a, b) => b[0] + ':' + (a + b[1]))
expect(new Filter('add', [['num', '2']], false).renderSync(3, ctx)).to.equal('num:5')
})
})
+20
View File
@@ -0,0 +1,20 @@
import * as chai from 'chai'
import { Hash } from '../../../src/template/tag/hash'
import { Context } from '../../../src/context/context'
const expect = chai.expect
describe('Hash', function () {
it('should parse variable', async function () {
const hash = await Hash.create('num:foo', new Context({ foo: 3 }))
expect(hash.num).to.equal(3)
})
it('should parse literals', async function () {
const hash = await Hash.create('num:3', new Context())
expect(hash.num).to.equal(3)
})
it('should support sync', function () {
const hash = Hash.createSync('num:3', new Context())
expect(hash.num).to.equal(3)
})
})
+1 -1
View File
@@ -10,7 +10,7 @@ chai.use(sinonChai)
const expect = chai.expect
const liquid = new Liquid()
describe('tag', function () {
describe('Tag', function () {
let ctx: Context
const emitter = { write: (html: string) => (emitter.html += html), html: '' }
before(function () {