refactor: strictly typed

This commit is contained in:
harttle
2019-02-23 00:49:54 +08:00
parent 51f7e66f60
commit 5b6100d12b
86 changed files with 2630 additions and 2590 deletions
+2032 -1920
View File
File diff suppressed because it is too large Load Diff
+8 -1
View File
@@ -12,7 +12,7 @@
"test": "npm run unit && npm run e2e", "test": "npm run unit && npm run e2e",
"coverage-html": "nyc npm run unit && nyc report --reporter=html", "coverage-html": "nyc npm run unit && nyc report --reporter=html",
"coverage-coveralls": "nyc npm run unit && nyc report --reporter=text-lcov | coveralls", "coverage-coveralls": "nyc npm run unit && nyc report --reporter=text-lcov | coveralls",
"build": "rollup -c && ls -lh dist", "build": "rollup -c rollup.config.ts && ls -lh dist",
"version": "npm run build && git add -A dist" "version": "npm run build && git add -A dist"
}, },
"repository": { "repository": {
@@ -46,8 +46,14 @@
"@semantic-release/git": "^7.0.8", "@semantic-release/git": "^7.0.8",
"@semantic-release/npm": "^5.1.4", "@semantic-release/npm": "^5.1.4",
"@semantic-release/release-notes-generator": "^7.1.4", "@semantic-release/release-notes-generator": "^7.1.4",
"@types/chai": "^4.1.7",
"@types/chai-as-promised": "^7.1.0",
"@types/express": "^4.16.1",
"@types/jsdom": "^12.2.2", "@types/jsdom": "^12.2.2",
"@types/mocha": "^5.2.6", "@types/mocha": "^5.2.6",
"@types/sinon": "^7.0.6",
"@types/sinon-chai": "^3.2.2",
"@types/supertest": "^2.0.7",
"@typescript-eslint/eslint-plugin": "^1.3.0", "@typescript-eslint/eslint-plugin": "^1.3.0",
"chai": "^4.2.0", "chai": "^4.2.0",
"chai-as-promised": "^7.1.1", "chai-as-promised": "^7.1.1",
@@ -75,6 +81,7 @@
"supertest": "^3.4.2", "supertest": "^3.4.2",
"ts-node": "^8.0.2", "ts-node": "^8.0.2",
"tsconfig-paths": "^3.8.0", "tsconfig-paths": "^3.8.0",
"tslib": "^1.9.3",
"typescript": "^3.3.3" "typescript": "^3.3.3"
}, },
"release": { "release": {
+12 -13
View File
@@ -1,24 +1,23 @@
import { last } from 'src/util/underscore' import { last } from 'src/util/underscore'
export default { export default {
'join': (v, arg) => v.join(arg === undefined ? ' ' : arg), 'join': (v: any[], arg: string) => v.join(arg === undefined ? ' ' : arg),
'last': v => last(v), 'last': <T>(v: T[]): T => last(v),
'first': v => v[0], 'first': <T>(v: T[]): T => v[0],
'map': (arr, arg) => arr.map(v => v[arg]), 'map': <T1, T2>(arr: {[key: string]: T1}[], arg: string): T1[] => arr.map(v => v[arg]),
'reverse': v => v.reverse(), 'reverse': (v: any[]) => v.reverse(),
'sort': (v, arg) => v.sort(arg), 'sort': <T>(v: T[], arg: (lhs: T, rhs: T) => number) => v.sort(arg),
'size': v => v.length, 'size': (v: string | any[]) => v.length,
'slice': (v, begin, length) => { 'concat': <T1, T2>(v: T1[], arg: T2[] | T2): Array<T1 | T2> => Array.prototype.concat.call(v, arg),
'slice': <T>(v: T[], begin: number, length: number): T[] => {
if (length === undefined) length = 1 if (length === undefined) length = 1
return v.slice(begin, begin + length) return v.slice(begin, begin + length)
}, },
'uniq': function (arr) { 'uniq': function<T> (arr: T[]): T[] {
const u = {} const u = {}
return (arr || []).filter(val => { return (arr || []).filter(val => {
if (u.hasOwnProperty(val)) { if (u.hasOwnProperty(String(val))) return false
return false u[String(val)] = true
}
u[val] = true
return true return true
}) })
} }
+2 -2
View File
@@ -2,7 +2,7 @@ import strftime from 'src/util/strftime'
import { isString } from 'src/util/underscore' import { isString } from 'src/util/underscore'
export default { export default {
'date': (v, arg) => { 'date': (v: string | Date, arg: string) => {
let date = v let date = v
if (v === 'now') { if (v === 'now') {
date = new Date() date = new Date()
@@ -13,6 +13,6 @@ export default {
} }
} }
function isValidDate (date) { function isValidDate (date: any): date is Date {
return date instanceof Date && !isNaN(date.getTime()) return date instanceof Date && !isNaN(date.getTime())
} }
+5 -5
View File
@@ -13,17 +13,17 @@ const unescapeMap = {
'&#39;': "'" '&#39;': "'"
} }
function escape (str) { function escape (str: string) {
return String(str).replace(/&|<|>|"|'/g, m => escapeMap[m]) return String(str).replace(/&|<|>|"|'/g, m => escapeMap[m])
} }
function unescape (str) { function unescape (str: string) {
return String(str).replace(/&(amp|lt|gt|#34|#39);/g, m => unescapeMap[m]) return String(str).replace(/&(amp|lt|gt|#34|#39);/g, m => unescapeMap[m])
} }
export default { export default {
'escape': escape, 'escape': escape,
'escape_once': str => escape(unescape(str)), 'escape_once': (str: string) => escape(unescape(str)),
'newline_to_br': v => v.replace(/\n/g, '<br />'), 'newline_to_br': (v: string) => v.replace(/\n/g, '<br />'),
'strip_html': v => String(v).replace(/<script.*?<\/script>|<!--.*?-->|<style.*?<\/style>|<.*?>/g, '') 'strip_html': (v: string) => v.replace(/<script.*?<\/script>|<!--.*?-->|<style.*?<\/style>|<.*?>/g, '')
} }
+13 -13
View File
@@ -1,25 +1,25 @@
export default { export default {
'abs': v => Math.abs(v), 'abs': (v: number) => Math.abs(v),
'ceil': v => Math.ceil(v), 'ceil': (v: number) => Math.ceil(v),
'divided_by': (v, arg) => v / arg, 'divided_by': (v: number, arg: number) => v / arg,
'floor': v => Math.floor(v), 'floor': (v: number) => Math.floor(v),
'minus': bindFixed((v, arg) => v - arg), 'minus': bindFixed((v: number, arg: number) => v - arg),
'modulo': bindFixed((v, arg) => v % arg), 'modulo': bindFixed((v: number, arg: number) => v % arg),
'round': (v, arg) => { 'round': (v: number, arg: number = 0) => {
const amp = Math.pow(10, arg || 0) const amp = Math.pow(10, arg)
return Math.round(v * amp) / amp return Math.round(v * amp) / amp
}, },
'plus': bindFixed((v, arg) => Number(v) + Number(arg)), 'plus': bindFixed((v: number, arg: number) => Number(v) + Number(arg)),
'times': (v, arg) => v * arg 'times': (v: number, arg: number) => v * arg
} }
function getFixed (v) { function getFixed (v: number) {
const p = String(v).split('.') const p = String(v).split('.')
return (p.length > 1) ? p[1].length : 0 return (p.length > 1) ? p[1].length : 0
} }
function bindFixed (cb) { function bindFixed (cb: (v: number, arg: number) => number) {
return (l, r) => { return (l: number, r: number) => {
const f = Math.max(getFixed(l), getFixed(r)) const f = Math.max(getFixed(l), getFixed(r))
return cb(l, r).toFixed(f) return cb(l, r).toFixed(f)
} }
+1 -1
View File
@@ -1,5 +1,5 @@
import { isTruthy } from 'src/render/syntax' import { isTruthy } from 'src/render/syntax'
export default { export default {
'default': (v, arg) => isTruthy(v) ? v : arg 'default': <T1, T2>(v: T1, arg: T2): T1 | T2 => isTruthy(v) ? v : arg
} }
+19 -21
View File
@@ -1,32 +1,30 @@
import FilterImpl from "src/template/filter/filter-impl";
export default { export default {
'append': (v, arg) => v + arg, 'append': (v: string, arg: string) => v + arg,
'prepend': (v, arg) => arg + v, 'prepend': (v: string, arg: string) => arg + v,
'capitalize': str => String(str).charAt(0).toUpperCase() + str.slice(1), 'capitalize': (str: string) => String(str).charAt(0).toUpperCase() + str.slice(1),
'concat': (v, arg) => Array.prototype.concat.call(v, arg), 'lstrip': (v: string) => String(v).replace(/^\s+/, ''),
'lstrip': v => String(v).replace(/^\s+/, ''), 'downcase': (v: string) => v.toLowerCase(),
'downcase': v => v.toLowerCase(), 'upcase': (str: string) => String(str).toUpperCase(),
'upcase': str => String(str).toUpperCase(), 'remove': (v: string, arg: string) => v.split(arg).join(''),
'remove': (v, arg) => v.split(arg).join(''), 'remove_first': (v: string, l: string) => v.replace(l, ''),
'remove_first': (v, l) => v.replace(l, ''), 'replace': (v: string, pattern: string, replacement: string) =>
'replace': (v, pattern, replacement) =>
String(v).split(pattern).join(replacement), String(v).split(pattern).join(replacement),
'replace_first': (v, arg1, arg2) => String(v).replace(arg1, arg2), 'replace_first': (v: string, arg1: string, arg2: string) => String(v).replace(arg1, arg2),
'rstrip': str => String(str).replace(/\s+$/, ''), 'rstrip': (str: string) => String(str).replace(/\s+$/, ''),
'split': (v, arg) => String(v).split(arg), 'split': (v: string, arg: string) => String(v).split(arg),
'strip': (v) => String(v).trim(), 'strip': (v: string) => String(v).trim(),
'strip_newlines': v => String(v).replace(/\n/g, ''), 'strip_newlines': (v: string) => String(v).replace(/\n/g, ''),
'truncate': (v, l, o) => { 'truncate': (v: string, l: number = 16, o: string = '...') => {
v = String(v) v = String(v)
o = (o === undefined) ? '...' : o
l = l || 16
if (v.length <= l) return v if (v.length <= l) return v
return v.substr(0, l - o.length) + o return v.substr(0, l - o.length) + o
}, },
'truncatewords': (v, l, o) => { 'truncatewords': (v: string, l: number = v.length, o: string = '...') => {
if (o === undefined) o = '...'
const arr = v.split(' ') const arr = v.split(' ')
let ret = arr.slice(0, l).join(' ') let ret = arr.slice(0, l).join(' ')
if (arr.length > l) ret += o if (arr.length > l) ret += o
return ret return ret
} }
} } as {[key: string]: FilterImpl}
+3 -3
View File
@@ -1,4 +1,4 @@
export default { export default {
'url_decode': x => x.split('+').map(decodeURIComponent).join(' '), 'url_decode': (x: string) => x.split('+').map(decodeURIComponent).join(' '),
'url_encode': x => x.split(' ').map(encodeURIComponent).join('+') 'url_encode': (x: string) => x.split(' ').map(encodeURIComponent).join('+')
} }
+7 -4
View File
@@ -1,20 +1,23 @@
import assert from 'src/util/assert' import assert from 'src/util/assert'
import { identifier } from 'src/parser/lexical' import { identifier } from 'src/parser/lexical'
import { AssignScope } from 'src/scope/scopes' import { AssignScope } from 'src/scope/scopes'
import TagToken from 'src/parser/tag-token';
import Scope from 'src/scope/scope';
import ITagImplOptions from 'src/template/tag/itag-impl-options';
const re = new RegExp(`(${identifier.source})\\s*=([^]*)`) const re = new RegExp(`(${identifier.source})\\s*=([^]*)`)
export default { export default {
parse: function (token) { parse: function (token: TagToken) {
const match = token.args.match(re) const match = token.args.match(re) as RegExpMatchArray
assert(match, `illegal token ${token.raw}`) assert(match, `illegal token ${token.raw}`)
this.key = match[1] this.key = match[1]
this.value = match[2] this.value = match[2]
}, },
render: function (scope) { render: function (scope: Scope) {
const ctx = new AssignScope() const ctx = new AssignScope()
ctx[this.key] = this.liquid.evalValue(this.value, scope) ctx[this.key] = this.liquid.evalValue(this.value, scope)
scope.push(ctx) scope.push(ctx)
return Promise.resolve('') return Promise.resolve('')
} }
} } as ITagImplOptions
+12 -7
View File
@@ -1,20 +1,25 @@
import BlockMode from 'src/scope/block-mode' import BlockMode from 'src/scope/block-mode'
import TagToken from 'src/parser/tag-token';
import Token from 'src/parser/token';
import ITemplate from 'src/template/itemplate'
import Scope from 'src/scope/scope';
import ITagImplOptions from 'src/template/tag/itag-impl-options';
import ParseStream from 'src/parser/parse-stream';
export default { export default {
parse: function (token, remainTokens) { parse: function (token: TagToken, remainTokens: Token[]) {
const match = /\w+/.exec(token.args) const match = /\w+/.exec(token.args)
this.block = match ? match[0] : '' this.block = match ? match[0] : ''
this.tpls = [] as ITemplate[]
this.tpls = [] const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
const stream = this.liquid.parser.parseStream(remainTokens)
.on('tag:endblock', () => stream.stop()) .on('tag:endblock', () => stream.stop())
.on('template', tpl => this.tpls.push(tpl)) .on('template', (tpl: ITemplate) => this.tpls.push(tpl))
.on('end', () => { .on('end', () => {
throw new Error(`tag ${token.raw} not closed`) throw new Error(`tag ${token.raw} not closed`)
}) })
stream.start() stream.start()
}, },
render: async function (scope) { render: async function (scope: Scope) {
const childDefined = scope.blocks[this.block] const childDefined = scope.blocks[this.block]
const html = childDefined !== undefined const html = childDefined !== undefined
? childDefined ? childDefined
@@ -26,4 +31,4 @@ export default {
} }
return html return html
} }
} } as ITagImplOptions
+4 -3
View File
@@ -4,12 +4,13 @@ import { CaptureScope } from 'src/scope/scopes'
import TagToken from 'src/parser/tag-token' import TagToken from 'src/parser/tag-token'
import Token from 'src/parser/token' import Token from 'src/parser/token'
import Scope from 'src/scope/scope' import Scope from 'src/scope/scope'
import ITagImplOptions from 'src/template/tag/itag-impl-options';
const re = new RegExp(`(${identifier.source})`) const re = new RegExp(`(${identifier.source})`)
export default { export default {
parse: function (tagToken: TagToken, remainTokens: Token[]) { parse: function (tagToken: TagToken, remainTokens: Token[]) {
const match = tagToken.args.match(re) const match = tagToken.args.match(re) as RegExpMatchArray
assert(match, `${tagToken.args} not valid identifier`) assert(match, `${tagToken.args} not valid identifier`)
this.variable = match[1] this.variable = match[1]
@@ -17,7 +18,7 @@ export default {
const stream = this.liquid.parser.parseStream(remainTokens) const stream = this.liquid.parser.parseStream(remainTokens)
stream.on('tag:endcapture', () => stream.stop()) stream.on('tag:endcapture', () => stream.stop())
.on('template', tpl => this.templates.push(tpl)) .on('template', (tpl) => this.templates.push(tpl))
.on('end', () => { .on('end', () => {
throw new Error(`tag ${tagToken.raw} not closed`) throw new Error(`tag ${tagToken.raw} not closed`)
}) })
@@ -29,4 +30,4 @@ export default {
ctx[this.variable] = html ctx[this.variable] = html
scope.push(ctx) scope.push(ctx)
} }
} } as ITagImplOptions
+13 -7
View File
@@ -1,14 +1,20 @@
import { evalExp } from 'src/render/syntax' import { evalExp } from 'src/render/syntax'
import TagToken from 'src/parser/tag-token';
import Token from 'src/parser/token';
import Scope from 'src/scope/scope';
import ITemplate from 'src/template/itemplate';
import ITagImplOptions from 'src/template/tag/itag-impl-options';
import ParseStream from 'src/parser/parse-stream';
export default { export default {
parse: function (tagToken, remainTokens) { parse: function (tagToken: TagToken, remainTokens: Token[]) {
this.cond = tagToken.args this.cond = tagToken.args
this.cases = [] this.cases = []
this.elseTemplates = [] this.elseTemplates = []
let p = [] let p: ITemplate[] = []
const stream = this.liquid.parser.parseStream(remainTokens) const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
.on('tag:when', token => { .on('tag:when', (token: TagToken) => {
this.cases.push({ this.cases.push({
val: token.args, val: token.args,
templates: p = [] templates: p = []
@@ -16,7 +22,7 @@ export default {
}) })
.on('tag:else', () => (p = this.elseTemplates)) .on('tag:else', () => (p = this.elseTemplates))
.on('tag:endcase', () => stream.stop()) .on('tag:endcase', () => stream.stop())
.on('template', tpl => p.push(tpl)) .on('template', (tpl: ITemplate) => p.push(tpl))
.on('end', () => { .on('end', () => {
throw new Error(`tag ${tagToken.raw} not closed`) throw new Error(`tag ${tagToken.raw} not closed`)
}) })
@@ -24,7 +30,7 @@ export default {
stream.start() stream.start()
}, },
render: function (scope) { render: function (scope: Scope) {
for (let i = 0; i < this.cases.length; i++) { for (let i = 0; i < this.cases.length; i++) {
const branch = this.cases[i] const branch = this.cases[i]
const val = evalExp(branch.val, scope) const val = evalExp(branch.val, scope)
@@ -35,4 +41,4 @@ export default {
} }
return this.liquid.renderer.renderTemplates(this.elseTemplates, scope) return this.liquid.renderer.renderTemplates(this.elseTemplates, scope)
} }
} } as ITagImplOptions
+7 -3
View File
@@ -1,8 +1,12 @@
import TagToken from "src/parser/tag-token";
import Token from "src/parser/token";
import ITagImplOptions from "src/template/tag/itag-impl-options";
export default { export default {
parse: function (tagToken, remainTokens) { parse: function (tagToken: TagToken, remainTokens: Token[]) {
const stream = this.liquid.parser.parseStream(remainTokens) const stream = this.liquid.parser.parseStream(remainTokens)
stream stream
.on('token', token => { .on('token', (token: TagToken) => {
if (token.name === 'endcomment') stream.stop() if (token.name === 'endcomment') stream.stop()
}) })
.on('end', () => { .on('end', () => {
@@ -10,4 +14,4 @@ export default {
}) })
stream.start() stream.start()
} }
} } as ITagImplOptions
+9 -7
View File
@@ -1,13 +1,16 @@
import assert from 'src/util/assert' import assert from 'src/util/assert'
import { value as rValue } from 'src/parser/lexical' import { value as rValue } from 'src/parser/lexical'
import { evalValue } from 'src/render/syntax' import { evalValue } from 'src/render/syntax'
import TagToken from 'src/parser/tag-token';
import Scope from 'src/scope/scope';
import ITagImplOptions from 'src/template/tag/itag-impl-options';
const groupRE = new RegExp(`^(?:(${rValue.source})\\s*:\\s*)?(.*)$`) const groupRE = new RegExp(`^(?:(${rValue.source})\\s*:\\s*)?(.*)$`)
const candidatesRE = new RegExp(rValue.source, 'g') const candidatesRE = new RegExp(rValue.source, 'g')
export default { export default <ITagImplOptions>{
parse: function (tagToken) { parse: function (tagToken: TagToken) {
let match = groupRE.exec(tagToken.args) let match: RegExpExecArray | null = groupRE.exec(tagToken.args) as RegExpExecArray
assert(match, `illegal tag: ${tagToken.raw}`) assert(match, `illegal tag: ${tagToken.raw}`)
this.group = match[1] || '' this.group = match[1] || ''
@@ -21,11 +24,10 @@ export default {
assert(this.candidates.length, `empty candidates: ${tagToken.raw}`) assert(this.candidates.length, `empty candidates: ${tagToken.raw}`)
}, },
render: function (scope) { render: function (scope: Scope) {
const group = evalValue(this.group, scope) const group = evalValue(this.group, scope)
const fingerprint = `cycle:${group}:` + this.candidates.join(',') const fingerprint = `cycle:${group}:` + this.candidates.join(',')
const groups = scope.groups
const groups = scope.opts.groups = scope.opts.groups || {}
let idx = groups[fingerprint] let idx = groups[fingerprint]
if (idx === undefined) { if (idx === undefined) {
@@ -38,4 +40,4 @@ export default {
return evalValue(candidate, scope) return evalValue(candidate, scope)
} }
} }
+3 -2
View File
@@ -3,10 +3,11 @@ import { identifier } from 'src/parser/lexical'
import { CaptureScope, AssignScope, DecrementScope } from 'src/scope/scopes' import { CaptureScope, AssignScope, DecrementScope } from 'src/scope/scopes'
import TagToken from 'src/parser/tag-token' import TagToken from 'src/parser/tag-token'
import Scope from 'src/scope/scope' import Scope from 'src/scope/scope'
import ITagImplOptions from 'src/template/tag/itag-impl-options';
export default { export default {
parse: function (token: TagToken) { parse: function (token: TagToken) {
const match = token.args.match(identifier) const match = token.args.match(identifier) as RegExpMatchArray
assert(match, `illegal identifier ${token.args}`) assert(match, `illegal identifier ${token.args}`)
this.variable = match[0] this.variable = match[0]
}, },
@@ -26,4 +27,4 @@ export default {
} }
return --context[this.variable] return --context[this.variable]
} }
} } as ITagImplOptions
+85 -79
View File
@@ -3,7 +3,13 @@ import { isString, isObject, isArray } from 'src/util/underscore'
import { evalExp } from 'src/render/syntax' import { evalExp } from 'src/render/syntax'
import assert from 'src/util/assert' import assert from 'src/util/assert'
import { identifier, value, hash } from 'src/parser/lexical' import { identifier, value, hash } from 'src/parser/lexical'
import { RenderBreakError } from 'src/util/error' import TagToken from 'src/parser/tag-token';
import Token from 'src/parser/token';
import Scope from 'src/scope/scope';
import Hash from 'src/template/tag/hash';
import ITemplate from 'src/template/itemplate';
import ITagImplOptions from 'src/template/tag/itag-impl-options';
import ParseStream from 'src/parser/parse-stream';
const re = new RegExp(`^(${identifier.source})\\s+in\\s+` + const re = new RegExp(`^(${identifier.source})\\s+in\\s+` +
`(${value.source})` + `(${value.source})` +
@@ -11,83 +17,83 @@ const re = new RegExp(`^(${identifier.source})\\s+in\\s+` +
`(?:\\s+(reversed))?` + `(?:\\s+(reversed))?` +
`(?:\\s+${hash.source})*$`) `(?:\\s+${hash.source})*$`)
function parse (tagToken, remainTokens) { export default <ITagImplOptions>{
const match = re.exec(tagToken.args) type: 'block',
assert(match, `illegal tag: ${tagToken.raw}`) parse: function (tagToken: TagToken, remainTokens: Token[]) {
this.variable = match[1] const match = re.exec(tagToken.args) as RegExpExecArray
this.collection = match[2] assert(match, `illegal tag: ${tagToken.raw}`)
this.reversed = !!match[3] this.variable = match[1]
this.collection = match[2]
this.templates = [] this.reversed = !!match[3]
this.elseTemplates = []
this.templates = []
let p this.elseTemplates = []
const stream = this.liquid.parser.parseStream(remainTokens)
.on('start', () => (p = this.templates)) let p
.on('tag:else', () => (p = this.elseTemplates)) const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
.on('tag:endfor', () => stream.stop()) .on('start', () => (p = this.templates))
.on('template', tpl => p.push(tpl)) .on('tag:else', () => (p = this.elseTemplates))
.on('end', () => { .on('tag:endfor', () => stream.stop())
throw new Error(`tag ${tagToken.raw} not closed`) .on('template', (tpl: ITemplate) => p.push(tpl))
.on('end', () => {
throw new Error(`tag ${tagToken.raw} not closed`)
})
stream.start()
},
render: async function (scope: Scope, hash: Hash) {
let collection = evalExp(this.collection, scope)
if (!isArray(collection)) {
if (isString(collection) && collection.length > 0) {
collection = [collection] as string[]
} else if (isObject(collection)) {
collection = Object.keys(collection).map((key) => [key, collection[key]]) as Array<[string, any]>
}
}
if (!isArray(collection) || !collection.length) {
return this.liquid.renderer.renderTemplates(this.elseTemplates, scope)
}
const offset = hash.offset || 0
const limit = (hash.limit === undefined) ? collection.length : hash.limit
collection = collection.slice(offset, offset + limit)
if (this.reversed) collection.reverse()
const contexts = collection.map((item: string, i: number) => {
const ctx = {}
ctx[this.variable] = item
ctx['forloop'] = {
first: i === 0,
index: i + 1,
index0: i,
last: i === collection.length - 1,
length: collection.length,
rindex: collection.length - i,
rindex0: collection.length - i - 1
}
return ctx
}) })
stream.start() let html = ''
} let finished = false
await mapSeries(contexts, async context => {
async function render (scope, hash) { if (finished) return
let collection = evalExp(this.collection, scope)
scope.push(context)
if (!isArray(collection)) { try {
if (isString(collection) && collection.length > 0) { html += await this.liquid.renderer.renderTemplates(this.templates, scope)
collection = [collection] } catch (e) {
} else if (isObject(collection)) { if (e.name === 'RenderBreakError') {
collection = Object.keys(collection).map((key) => [key, collection[key]]) html += e.resolvedHTML
} if (e.message === 'break') {
finished = true
}
} else throw e
}
scope.pop(context)
})
return html
} }
if (!isArray(collection) || !collection.length) { }
return this.liquid.renderer.renderTemplates(this.elseTemplates, scope)
}
const offset = hash.offset || 0
const limit = (hash.limit === undefined) ? collection.length : hash.limit
collection = collection.slice(offset, offset + limit)
if (this.reversed) collection.reverse()
const contexts = collection.map((item, i) => {
const ctx = {}
ctx[this.variable] = item
ctx['forloop'] = {
first: i === 0,
index: i + 1,
index0: i,
last: i === collection.length - 1,
length: collection.length,
rindex: collection.length - i,
rindex0: collection.length - i - 1
}
return ctx
})
let html = ''
let finished = false
await mapSeries(contexts, async context => {
if (finished) return
scope.push(context)
try {
html += await this.liquid.renderer.renderTemplates(this.templates, scope)
} catch (e) {
if (e instanceof RenderBreakError) {
html += e.resolvedHTML
if (e.message === 'break') {
finished = true
}
} else throw e
}
scope.pop(context)
})
return html
}
export default { parse, render }
+12 -6
View File
@@ -1,17 +1,23 @@
import { evalExp, isTruthy } from 'src/render/syntax' import { evalExp, isTruthy } from 'src/render/syntax'
import TagToken from 'src/parser/tag-token';
import Token from 'src/parser/token';
import Scope from 'src/scope/scope';
import ITemplate from 'src/template/itemplate';
import ITagImplOptions from 'src/template/tag/itag-impl-options';
import ParseStream from 'src/parser/parse-stream';
export default { export default {
parse: function (tagToken, remainTokens) { parse: function (tagToken: TagToken, remainTokens: Token[]) {
this.branches = [] this.branches = []
this.elseTemplates = [] this.elseTemplates = []
let p let p
const stream = this.liquid.parser.parseStream(remainTokens) const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
.on('start', () => this.branches.push({ .on('start', () => this.branches.push({
cond: tagToken.args, cond: tagToken.args,
templates: (p = []) templates: (p = [])
})) }))
.on('tag:elsif', token => { .on('tag:elsif', (token: TagToken) => {
this.branches.push({ this.branches.push({
cond: token.args, cond: token.args,
templates: p = [] templates: p = []
@@ -19,7 +25,7 @@ export default {
}) })
.on('tag:else', () => (p = this.elseTemplates)) .on('tag:else', () => (p = this.elseTemplates))
.on('tag:endif', () => stream.stop()) .on('tag:endif', () => stream.stop())
.on('template', tpl => p.push(tpl)) .on('template', (tpl: ITemplate) => p.push(tpl))
.on('end', () => { .on('end', () => {
throw new Error(`tag ${tagToken.raw} not closed`) throw new Error(`tag ${tagToken.raw} not closed`)
}) })
@@ -27,7 +33,7 @@ export default {
stream.start() stream.start()
}, },
render: function (scope) { render: function (scope: Scope) {
for (const branch of this.branches) { for (const branch of this.branches) {
const cond = evalExp(branch.cond, scope) const cond = evalExp(branch.cond, scope)
if (isTruthy(cond)) { if (isTruthy(cond)) {
@@ -36,4 +42,4 @@ export default {
} }
return this.liquid.renderer.renderTemplates(this.elseTemplates, scope) return this.liquid.renderer.renderTemplates(this.elseTemplates, scope)
} }
} } as ITagImplOptions
+7 -3
View File
@@ -2,12 +2,16 @@ import assert from 'src/util/assert'
import { value, quotedLine } from 'src/parser/lexical' import { value, quotedLine } from 'src/parser/lexical'
import { evalValue } from 'src/render/syntax' import { evalValue } from 'src/render/syntax'
import BlockMode from 'src/scope/block-mode' import BlockMode from 'src/scope/block-mode'
import TagToken from 'src/parser/tag-token';
import Scope from 'src/scope/scope';
import Hash from 'src/template/tag/hash';
import ITagImplOptions from 'src/template/tag/itag-impl-options';
const staticFileRE = /[^\s,]+/ const staticFileRE = /[^\s,]+/
const withRE = new RegExp(`with\\s+(${value.source})`) const withRE = new RegExp(`with\\s+(${value.source})`)
export default { export default <ITagImplOptions>{
parse: function (token) { parse: function (token: TagToken) {
let match = staticFileRE.exec(token.args) let match = staticFileRE.exec(token.args)
if (match) { if (match) {
this.staticValue = match[0] this.staticValue = match[0]
@@ -23,7 +27,7 @@ export default {
this.with = match[1] this.with = match[1]
} }
}, },
render: async function (scope, hash) { render: async function (scope: Scope, hash: Hash) {
let filepath let filepath
if (scope.opts.dynamicPartials) { if (scope.opts.dynamicPartials) {
if (quotedLine.exec(this.value)) { if (quotedLine.exec(this.value)) {
+3 -2
View File
@@ -1,12 +1,13 @@
import assert from 'src/util/assert' import assert from 'src/util/assert'
import { identifier } from 'src/parser/lexical' import { identifier } from 'src/parser/lexical'
import { CaptureScope, AssignScope, IncrementScope } from 'src/scope/scopes' import { CaptureScope, AssignScope, IncrementScope } from 'src/scope/scopes'
import ITagImplOptions from 'src/template/tag/itag-impl-options';
export default { export default {
parse: function (token) { parse: function (token) {
const match = token.args.match(identifier) const match = token.args.match(identifier)
assert(match, `illegal identifier ${token.args}`) assert(match, `illegal identifier ${token.args}`)
this.variable = match[0] this.variable = match![0]
}, },
render: function (scope) { render: function (scope) {
let context = scope.findContextFor( let context = scope.findContextFor(
@@ -26,4 +27,4 @@ export default {
context[this.variable]++ context[this.variable]++
return val return val
} }
} } as ITagImplOptions
+8 -3
View File
@@ -2,11 +2,16 @@ import assert from 'src/util/assert'
import { value as rValue } from 'src/parser/lexical' import { value as rValue } from 'src/parser/lexical'
import { evalValue } from 'src/render/syntax' import { evalValue } from 'src/render/syntax'
import BlockMode from 'src/scope/block-mode' import BlockMode from 'src/scope/block-mode'
import TagToken from 'src/parser/tag-token';
import Token from 'src/parser/token';
import Scope from 'src/scope/scope';
import Hash from 'src/template/tag/hash';
import ITagImplOptions from 'src/template/tag/itag-impl-options';
const staticFileRE = /\S+/ const staticFileRE = /\S+/
export default { export default {
parse: function (token, remainTokens) { parse: function (token: TagToken, remainTokens: Token[]) {
let match = staticFileRE.exec(token.args) let match = staticFileRE.exec(token.args)
if (match) { if (match) {
this.staticLayout = match[0] this.staticLayout = match[0]
@@ -19,7 +24,7 @@ export default {
this.tpls = this.liquid.parser.parse(remainTokens) this.tpls = this.liquid.parser.parse(remainTokens)
}, },
render: async function (scope, hash) { render: async function (scope: Scope, hash: Hash) {
const layout = scope.opts.dynamicPartials const layout = scope.opts.dynamicPartials
? evalValue(this.layout, scope) ? evalValue(this.layout, scope)
: this.staticLayout : this.staticLayout
@@ -38,4 +43,4 @@ export default {
scope.pop(hash) scope.pop(hash)
return partial return partial
} }
} } as ITagImplOptions
+8 -4
View File
@@ -1,10 +1,14 @@
export default { import TagToken from "src/parser/tag-token";
parse: function (tagToken, remainTokens) { import Token from "src/parser/token";
import ITagImplOptions from "src/template/tag/itag-impl-options";
export default <ITagImplOptions>{
parse: function (tagToken: TagToken, remainTokens: Token[]) {
this.tokens = [] this.tokens = []
const stream = this.liquid.parser.parseStream(remainTokens) const stream = this.liquid.parser.parseStream(remainTokens)
stream stream
.on('token', token => { .on('token', (token: TagToken) => {
if (token.name === 'endraw') stream.stop() if (token.name === 'endraw') stream.stop()
else this.tokens.push(token) else this.tokens.push(token)
}) })
@@ -14,6 +18,6 @@ export default {
stream.start() stream.start()
}, },
render: function () { render: function () {
return this.tokens.map(token => token.raw).join('') return this.tokens.map((token: Token) => token.raw).join('')
} }
} }
+15 -8
View File
@@ -2,14 +2,21 @@ import { mapSeries } from 'src/util/promise'
import assert from 'src/util/assert' import assert from 'src/util/assert'
import { evalExp } from 'src/render/syntax' import { evalExp } from 'src/render/syntax'
import { identifier, value, hash } from 'src/parser/lexical' import { identifier, value, hash } from 'src/parser/lexical'
import TagToken from 'src/parser/tag-token';
import Token from 'src/parser/token';
import ITemplate from 'src/template/itemplate';
import Scope from 'src/scope/scope';
import Hash from 'src/template/tag/hash';
import ITagImplOptions from 'src/template/tag/itag-impl-options';
import ParseStream from 'src/parser/parse-stream';
const re = new RegExp(`^(${identifier.source})\\s+in\\s+` + const re = new RegExp(`^(${identifier.source})\\s+in\\s+` +
`(${value.source})` + `(${value.source})` +
`(?:\\s+${hash.source})*$`) `(?:\\s+${hash.source})*$`)
export default { export default {
parse: function (tagToken, remainTokens) { parse: function (tagToken: TagToken, remainTokens: Token[]) {
const match = re.exec(tagToken.args) const match = re.exec(tagToken.args) as RegExpExecArray
assert(match, `illegal tag: ${tagToken.raw}`) assert(match, `illegal tag: ${tagToken.raw}`)
this.variable = match[1] this.variable = match[1]
@@ -17,10 +24,10 @@ export default {
this.templates = [] this.templates = []
let p let p
const stream = this.liquid.parser.parseStream(remainTokens) const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
.on('start', () => (p = this.templates)) .on('start', () => (p = this.templates))
.on('tag:endtablerow', () => stream.stop()) .on('tag:endtablerow', () => stream.stop())
.on('template', tpl => p.push(tpl)) .on('template', (tpl: ITemplate) => p.push(tpl))
.on('end', () => { .on('end', () => {
throw new Error(`tag ${tagToken.raw} not closed`) throw new Error(`tag ${tagToken.raw} not closed`)
}) })
@@ -28,20 +35,20 @@ export default {
stream.start() stream.start()
}, },
render: async function (scope, hash) { render: async function (scope: Scope, hash: Hash) {
let collection = evalExp(this.collection, scope) || [] let collection = evalExp(this.collection, scope) || []
const offset = hash.offset || 0 const offset = hash.offset || 0
const limit = (hash.limit === undefined) ? collection.length : hash.limit const limit = (hash.limit === undefined) ? collection.length : hash.limit
collection = collection.slice(offset, offset + limit) collection = collection.slice(offset, offset + limit)
const cols = hash.cols || collection.length const cols = hash.cols || collection.length
const contexts = collection.map(item => { const contexts = collection.map((item: any) => {
const ctx = {} const ctx = {}
ctx[this.variable] = item ctx[this.variable] = item
return ctx return ctx
}) })
let row let row: number = 0
let html = '' let html = ''
await mapSeries(contexts, async (context, idx) => { await mapSeries(contexts, async (context, idx) => {
row = Math.floor(idx / cols) + 1 row = Math.floor(idx / cols) + 1
@@ -65,4 +72,4 @@ export default {
} }
return html return html
} }
} } as ITagImplOptions
+10 -4
View File
@@ -1,11 +1,17 @@
import { evalExp, isFalsy } from 'src/render/syntax' import { evalExp, isFalsy } from 'src/render/syntax'
import TagToken from 'src/parser/tag-token';
import Token from 'src/parser/token';
import ITemplate from 'src/template/itemplate';
import Scope from 'src/scope/scope';
import ITagImplOptions from 'src/template/tag/itag-impl-options';
import ParseStream from 'src/parser/parse-stream';
export default { export default {
parse: function (tagToken, remainTokens) { parse: function (tagToken: TagToken, remainTokens: Token[]) {
this.templates = [] this.templates = []
this.elseTemplates = [] this.elseTemplates = []
let p let p
const stream = this.liquid.parser.parseStream(remainTokens) const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
.on('start', () => { .on('start', () => {
p = this.templates p = this.templates
this.cond = tagToken.args this.cond = tagToken.args
@@ -20,10 +26,10 @@ export default {
stream.start() stream.start()
}, },
render: function (scope) { render: function (scope: Scope) {
const cond = evalExp(this.cond, scope) const cond = evalExp(this.cond, scope)
return isFalsy(cond) return isFalsy(cond)
? this.liquid.renderer.renderTemplates(this.templates, scope) ? this.liquid.renderer.renderTemplates(this.templates, scope)
: this.liquid.renderer.renderTemplates(this.elseTemplates, scope) : this.liquid.renderer.renderTemplates(this.elseTemplates, scope)
} }
} } as ITagImplOptions
+2 -2
View File
@@ -1,7 +1,7 @@
import { last } from '../util/underscore' import { last } from '../util/underscore'
import IFS from './ifs' import IFS from './ifs'
function domResolve (root, path) { function domResolve (root: string, path: string) {
const base = document.createElement('base') const base = document.createElement('base')
base.href = root base.href = root
@@ -16,7 +16,7 @@ function domResolve (root, path) {
return resolved return resolved
} }
function resolve (root, filepath, ext) { function resolve (root: string, filepath: string, ext: string) {
if (root.length && last(root) !== '/') root += '/' if (root.length && last(root) !== '/') root += '/'
const url = domResolve(root, filepath) const url = domResolve(root, filepath)
return url.replace(/^(\w+:\/\/[^/]+)(\/[^?]+)/, (str, origin, path) => { return url.replace(/^(\w+:\/\/[^/]+)(\/[^?]+)/, (str, origin, path) => {
+1 -1
View File
@@ -1,5 +1,5 @@
export default interface IFS { export default interface IFS {
exists: (filepath?: string) => Promise<boolean> exists: (filepath: string) => Promise<boolean>
readFile: (filepath:string) => Promise<string> readFile: (filepath:string) => Promise<string>
resolve: (root: string, file: string, ext: string) => string resolve: (root: string, file: string, ext: string) => string
} }
+3 -3
View File
@@ -3,11 +3,11 @@ import { resolve, extname } from 'path'
import { stat, readFile } from 'fs' import { stat, readFile } from 'fs'
import IFS from './ifs' import IFS from './ifs'
const statAsync = _.promisify(stat) as (filepath: string) => Promise<object> const statAsync = _.promisify(stat)
const readFileAsync = _.promisify(readFile) as (filepath: string, encoding: string) => Promise<string> const readFileAsync = _.promisify<string, string, string>(readFile)
const fs: IFS = { const fs: IFS = {
exists: filepath => { exists: (filepath: string) => {
return statAsync(filepath).then(() => true).catch(() => false) return statAsync(filepath).then(() => true).catch(() => false)
}, },
readFile: filepath => { readFile: filepath => {
+7 -3
View File
@@ -48,6 +48,10 @@ export interface NormalizedFullOptions extends NormalizedOptions {
trim_tag_left: boolean trim_tag_left: boolean
trim_output_right: boolean trim_output_right: boolean
trim_output_left: boolean trim_output_left: boolean
tag_delimiter_left: string,
tag_delimiter_right: string,
output_delimiter_left: string,
output_delimiter_right: string,
greedy: boolean greedy: boolean
} }
@@ -69,7 +73,7 @@ const defaultOptions: NormalizedFullOptions = {
strict_variables: false strict_variables: false
} }
export function normalize (options: LiquidOptions): NormalizedOptions { export function normalize (options?: LiquidOptions): NormalizedOptions {
options = options || {} options = options || {}
if (options.hasOwnProperty('root')) { if (options.hasOwnProperty('root')) {
options.root = normalizeStringArray(options.root) options.root = normalizeStringArray(options.root)
@@ -77,11 +81,11 @@ export function normalize (options: LiquidOptions): NormalizedOptions {
return options as NormalizedOptions return options as NormalizedOptions
} }
export function applyDefault (options: NormalizedOptions): NormalizedFullOptions { export function applyDefault (options?: NormalizedOptions): NormalizedFullOptions {
return { ...defaultOptions, ...options } return { ...defaultOptions, ...options }
} }
function normalizeStringArray (value: string | string[]): string[] { function normalizeStringArray (value: any): string[] {
if (_.isArray(value)) return value as string[] if (_.isArray(value)) return value as string[]
if (_.isString(value)) return [value as string] if (_.isString(value)) return [value as string]
return [] return []
+10 -12
View File
@@ -6,7 +6,7 @@ import ITemplate from './template/itemplate'
import Tokenizer from './parser/tokenizer' import Tokenizer from './parser/tokenizer'
import Render from './render/render' import Render from './render/render'
import Tag from './template/tag/tag' import Tag from './template/tag/tag'
import Filter from './template/filter' import Filter from './template/filter/filter'
import Parser from './parser/parser' import Parser from './parser/parser'
import ITagImplOptions from './template/tag/itag-impl-options' import ITagImplOptions from './template/tag/itag-impl-options'
import Value from './template/value' import Value from './template/value'
@@ -14,19 +14,17 @@ import { isTruthy, isFalsy, evalExp, evalValue } from './render/syntax'
import builtinTags from './builtin/tags' import builtinTags from './builtin/tags'
import builtinFilters from './builtin/filters' import builtinFilters from './builtin/filters'
import { LiquidOptions, NormalizedFullOptions, applyDefault, normalize } from './liquid-options' import { LiquidOptions, NormalizedFullOptions, applyDefault, normalize } from './liquid-options'
import FilterImpl from './template/filter/filter-impl';
export default class Liquid { export default class Liquid {
public options: NormalizedFullOptions public options: NormalizedFullOptions
private cache: object public renderer: Render
private parser: Parser public parser: Parser
private renderer: Render private cache: object = {}
private tokenizer: Tokenizer private tokenizer: Tokenizer
constructor (opts: LiquidOptions = {}) { constructor (opts: LiquidOptions = {}) {
this.options = applyDefault(normalize(opts)) this.options = applyDefault(normalize(opts))
if (this.options.cache) {
this.cache = {}
}
this.parser = new Parser(this) this.parser = new Parser(this)
this.renderer = new Render() this.renderer = new Render()
this.tokenizer = new Tokenizer(this.options) this.tokenizer = new Tokenizer(this.options)
@@ -47,7 +45,7 @@ export default class Liquid {
const tpl = await this.parse(html) const tpl = await this.parse(html)
return this.render(tpl, ctx, opts) return this.render(tpl, ctx, opts)
} }
async getTemplate (file, opts?: LiquidOptions) { async getTemplate (file: string, opts?: LiquidOptions) {
const options = normalize(opts) const options = normalize(opts)
const roots = options.root ? [...options.root, ...this.options.root] : this.options.root const roots = options.root ? [...options.root, ...this.options.root] : this.options.root
const paths = roots.map(root => fs.resolve(root, file, this.options.extname)) const paths = roots.map(root => fs.resolve(root, file, this.options.extname))
@@ -66,7 +64,7 @@ export default class Liquid {
err.code = 'ENOENT' err.code = 'ENOENT'
throw err throw err
} }
async renderFile (file, ctx?: object, opts?: LiquidOptions) { async renderFile (file: string, ctx?: object, opts?: LiquidOptions) {
const options = normalize(opts) const options = normalize(opts)
const templates = await this.getTemplate(file, options) const templates = await this.getTemplate(file, options)
return this.render(templates, ctx, opts) return this.render(templates, ctx, opts)
@@ -74,18 +72,18 @@ export default class Liquid {
evalValue (str: string, scope: Scope) { evalValue (str: string, scope: Scope) {
return new Value(str, this.options.strict_filters).value(scope) return new Value(str, this.options.strict_filters).value(scope)
} }
registerFilter (name, filter) { registerFilter (name: string, filter: FilterImpl) {
return Filter.register(name, filter) return Filter.register(name, filter)
} }
registerTag (name: string, tag: ITagImplOptions) { registerTag (name: string, tag: ITagImplOptions) {
return Tag.register(name, tag) return Tag.register(name, tag)
} }
plugin (plugin) { plugin (plugin: (this: Liquid, L: typeof Liquid) => void) {
return plugin.call(this, Liquid) return plugin.call(this, Liquid)
} }
express () { express () {
const self = this const self = this
return function (filePath: string, ctx: object, cb: (err: Error, html?: string) => void) { return function (this: any, filePath: string, ctx: object, cb: (err: Error | null, html?: string) => void) {
const opts = { root: this.root } const opts = { root: this.root }
self.renderFile(filePath, ctx, opts).then(html => cb(null, html), cb) self.renderFile(filePath, ctx, opts).then(html => cb(null, html), cb)
} }
+4 -3
View File
@@ -1,12 +1,13 @@
import Token from './token' import Token from './token'
import { last } from 'src/util/underscore'
export default class DelimitedToken extends Token { export default class DelimitedToken extends Token {
trimLeft: boolean trimLeft: boolean
trimRight: boolean trimRight: boolean
constructor (raw, value, pos, input, file, line) { constructor (raw: string, value: string, input: string, line: number, pos: number, file?: string) {
super(raw, pos, input, file, line) super(raw, input, line, pos, file)
this.trimLeft = value[0] === '-' this.trimLeft = value[0] === '-'
this.trimRight = value[value.length - 1] === '-' this.trimRight = last(value) === '-'
this.value = value this.value = value
.slice( .slice(
this.trimLeft ? 1 : 0, this.trimLeft ? 1 : 0,
+2 -2
View File
@@ -1,8 +1,8 @@
import Token from './token' import Token from './token'
export default class HTMLToken extends Token { export default class HTMLToken extends Token {
constructor (str, begin, input, file, line) { constructor (str: string, input: string, line: number, col: number, file?: string) {
super(str, begin, input, file, line) super(str, input, line, col, file)
this.type = 'html' this.type = 'html'
this.value = str this.value = str
} }
+6 -6
View File
@@ -49,27 +49,27 @@ export const operators = [
/==|!=|<=|>=|<|>|\s+contains\s+/ /==|!=|<=|>=|<|>|\s+contains\s+/
] ]
export function isInteger (str) { export function isInteger (str: string) {
return integerLine.test(str) return integerLine.test(str)
} }
export function isLiteral (str) { export function isLiteral (str: string) {
return literalLine.test(str) return literalLine.test(str)
} }
export function isRange (str) { export function isRange (str: string) {
return rangeLine.test(str) return rangeLine.test(str)
} }
export function isVariable (str) { export function isVariable (str: string) {
return variableLine.test(str) return variableLine.test(str)
} }
export function matchValue (str) { export function matchValue (str: string) {
return value.exec(str) return value.exec(str)
} }
export function parseLiteral (str) { export function parseLiteral (str: string) {
let res = str.match(numberLine) let res = str.match(numberLine)
if (res) { if (res) {
return Number(str) return Number(str)
+2 -2
View File
@@ -1,8 +1,8 @@
import DelimitedToken from './delimited-token' import DelimitedToken from './delimited-token'
export default class OutputToken extends DelimitedToken { export default class OutputToken extends DelimitedToken {
constructor (raw, value, pos, input, file, line) { constructor (raw: string, value: string, input: string, line: number, pos: number, file?: string) {
super(raw, value, pos, input, file, line) super(raw, value, input, line, pos, file)
this.type = 'output' this.type = 'output'
} }
} }
+11 -11
View File
@@ -1,37 +1,37 @@
import Token from 'src/parser/token' import Token from 'src/parser/token'
import ITemplate from 'src/template/itemplate' import ITemplate from 'src/template/itemplate'
import TagToken from './tag-token';
type parseToken = (token: Token, remainTokens: Array<Token>) => ITemplate type ParseToken = ((token: Token, remainTokens: Array<Token>) => ITemplate)
type eventHandler = ((arg?: Token | ITemplate) => void)
export default class ParseStream { export default class ParseStream {
private tokens: Array<Token> private tokens: Array<Token>
private handlers: {[key: string]: eventHandler} = {} private handlers: {[key: string]: (arg: any) => void} = {}
private stopRequested: boolean private stopRequested: boolean = false
private parseToken: parseToken private parseToken: ParseToken
constructor (tokens: Array<Token>, parseToken: parseToken) { constructor (tokens: Array<Token>, parseToken: ParseToken) {
this.tokens = tokens this.tokens = tokens
this.parseToken = parseToken this.parseToken = parseToken
} }
on (name: string, cb: eventHandler) { on<T extends ITemplate | Token | undefined> (name: string, cb: (arg: T) => void): ParseStream {
this.handlers[name] = cb this.handlers[name] = cb
return this return this
} }
trigger (event: string, arg?: Token | ITemplate) { trigger <T extends Token | ITemplate>(event: string, arg?: T) {
const h = this.handlers[event] const h = this.handlers[event]
if (typeof h === 'function') { if (typeof h === 'function') {
h(arg) h(arg)
return true return true
} }
return false
} }
start () { start () {
this.trigger('start') this.trigger('start')
let token let token: Token | undefined
while (!this.stopRequested && (token = this.tokens.shift())) { while (!this.stopRequested && (token = this.tokens.shift())) {
if (this.trigger('token', token)) continue if (this.trigger('token', token)) continue
if (token.type === 'tag' && if (token.type === 'tag' && this.trigger(`tag:${(<TagToken>token).name}`, token)) {
this.trigger(`tag:${token.name}`, token)) {
continue continue
} }
const template = this.parseToken(token, this.tokens) const template = this.parseToken(token, this.tokens)
+2 -1
View File
@@ -7,6 +7,7 @@ import OutputToken from './output-token'
import Tag from 'src/template/tag/tag' import Tag from 'src/template/tag/tag'
import Output from 'src/template/output' import Output from 'src/template/output'
import HTML from 'src/template/html' import HTML from 'src/template/html'
import ITemplate from 'src/template/itemplate'
export default class Parser { export default class Parser {
liquid: Liquid liquid: Liquid
@@ -16,7 +17,7 @@ export default class Parser {
} }
parse (tokens: Array<Token>) { parse (tokens: Array<Token>) {
let token let token
const templates = [] const templates: ITemplate[] = []
while ((token = tokens.shift())) { while ((token = tokens.shift())) {
templates.push(this.parseToken(token, tokens)) templates.push(this.parseToken(token, tokens))
} }
+2 -2
View File
@@ -5,8 +5,8 @@ import * as lexical from './lexical'
export default class TagToken extends DelimitedToken { export default class TagToken extends DelimitedToken {
name: string name: string
args: string args: string
constructor (raw, value, pos, input, file, line) { constructor (raw: string, value: string, input: string, line: number, pos: number, file?: string) {
super(raw, value, pos, input, file, line) super(raw, value, input, line, pos, file)
this.type = 'tag' this.type = 'tag'
const match = this.value.match(lexical.tagLine) const match = this.value.match(lexical.tagLine)
if (!match) { if (!match) {
+4 -3
View File
@@ -1,15 +1,16 @@
export default class Token { export default class Token {
type: string type: string = 'notset'
line: number line: number
col: number col: number
raw: string raw: string
input: string input: string
file: string file?: string
value: string value: string
constructor (raw, col, input, file, line) { constructor (raw: string, input: string, line: number, col: number, file?: string) {
this.col = col this.col = col
this.line = line this.line = line
this.raw = raw this.raw = raw
this.value = raw
this.input = input this.input = input
this.file = file this.file = file
} }
+8 -8
View File
@@ -14,7 +14,7 @@ export default class Tokenizer {
this.options = applyDefault(options) this.options = applyDefault(options)
} }
tokenize (input: string, file?: string) { tokenize (input: string, file?: string) {
const tokens = [] const tokens: Token[] = []
const tagL = this.options.tag_delimiter_left const tagL = this.options.tag_delimiter_left
const tagR = this.options.tag_delimiter_right const tagR = this.options.tag_delimiter_right
const outputL = this.options.output_delimiter_left const outputL = this.options.output_delimiter_left
@@ -34,7 +34,7 @@ export default class Tokenizer {
} }
if (state === ParseState.HTML) { if (state === ParseState.HTML) {
if (input.substr(p, outputL.length) === outputL) { if (input.substr(p, outputL.length) === outputL) {
if (buffer) tokens.push(new HTMLToken(buffer, col, input, file, line)) if (buffer) tokens.push(new HTMLToken(buffer, input, line, col, file))
buffer = outputL buffer = outputL
line = curLine line = curLine
col = p - lineBegin + 1 col = p - lineBegin + 1
@@ -42,7 +42,7 @@ export default class Tokenizer {
state = ParseState.OUTPUT state = ParseState.OUTPUT
continue continue
} else if (input.substr(p, tagL.length) === tagL) { } else if (input.substr(p, tagL.length) === tagL) {
if (buffer) tokens.push(new HTMLToken(buffer, col, input, file, line)) if (buffer) tokens.push(new HTMLToken(buffer, input, line, col, file))
buffer = tagL buffer = tagL
line = curLine line = curLine
col = p - lineBegin + 1 col = p - lineBegin + 1
@@ -52,7 +52,7 @@ export default class Tokenizer {
} }
} else if (state === ParseState.OUTPUT && input.substr(p, outputR.length) === outputR) { } else if (state === ParseState.OUTPUT && input.substr(p, outputR.length) === outputR) {
buffer += outputR buffer += outputR
tokens.push(new OutputToken(buffer, buffer.slice(outputL.length, -outputR.length), col, input, file, line)) tokens.push(new OutputToken(buffer, buffer.slice(outputL.length, -outputR.length), input, line, col, file))
p += outputR.length p += outputR.length
buffer = '' buffer = ''
line = curLine line = curLine
@@ -61,7 +61,7 @@ export default class Tokenizer {
continue continue
} else if (input.substr(p, tagR.length) === tagR) { } else if (input.substr(p, tagR.length) === tagR) {
buffer += tagR buffer += tagR
tokens.push(new TagToken(buffer, buffer.slice(tagL.length, -tagR.length), col, input, file, line)) tokens.push(new TagToken(buffer, buffer.slice(tagL.length, -tagR.length), input, line, col, file))
p += tagR.length p += tagR.length
buffer = '' buffer = ''
line = curLine line = curLine
@@ -75,11 +75,11 @@ export default class Tokenizer {
const t = state === ParseState.OUTPUT ? 'output' : 'tag' const t = state === ParseState.OUTPUT ? 'output' : 'tag'
const str = buffer.length > 16 ? buffer.slice(0, 13) + '...' : buffer const str = buffer.length > 16 ? buffer.slice(0, 13) + '...' : buffer
throw new TokenizationError( throw new TokenizationError(
new Error(`${t} "${str}" not closed`), `${t} "${str}" not closed`,
new Token(buffer, col, input, file, line) new Token(buffer, input, line, col, file)
) )
} }
if (buffer) tokens.push(new HTMLToken(buffer, col, input, file, line)) if (buffer) tokens.push(new HTMLToken(buffer, input, line, col, file))
whiteSpaceCtrl(tokens, this.options) whiteSpaceCtrl(tokens, this.options)
return tokens return tokens
+2 -2
View File
@@ -21,13 +21,13 @@ export default function whiteSpaceCtrl (tokens: Token[], options: NormalizedFull
}) })
} }
function shouldTrimLeft (token: DelimitedToken, inRaw: boolean, options) { function shouldTrimLeft (token: DelimitedToken, inRaw: boolean, options: NormalizedFullOptions) {
if (inRaw) return false if (inRaw) return false
if (token.type === 'tag') return token.trimLeft || options.trim_tag_left if (token.type === 'tag') return token.trimLeft || options.trim_tag_left
if (token.type === 'output') return token.trimLeft || options.trim_output_left if (token.type === 'output') return token.trimLeft || options.trim_output_left
} }
function shouldTrimRight (token: DelimitedToken, inRaw: boolean, options) { function shouldTrimRight (token: DelimitedToken, inRaw: boolean, options: NormalizedFullOptions) {
if (inRaw) return false if (inRaw) return false
if (token.type === 'tag') return token.trimRight || options.trim_tag_right if (token.type === 'tag') return token.trimRight || options.trim_tag_right
if (token.type === 'output') return token.trimRight || options.trim_output_right if (token.type === 'output') return token.trimRight || options.trim_output_right
+5 -3
View File
@@ -1,8 +1,10 @@
import { RenderBreakError, RenderError } from 'src/util/error' import { RenderBreakError, RenderError } from 'src/util/error'
import assert from 'src/util/assert' import assert from 'src/util/assert'
import Scope from 'src/scope/scope';
import ITemplate from 'src/template/itemplate';
export default class Render { export default class Render {
async renderTemplates (templates, scope) { async renderTemplates (templates: ITemplate[], scope: Scope) {
assert(scope, 'unable to evalTemplates: scope undefined') assert(scope, 'unable to evalTemplates: scope undefined')
let html = '' let html = ''
@@ -10,11 +12,11 @@ export default class Render {
try { try {
html += await tpl.render(scope) html += await tpl.render(scope)
} catch (e) { } catch (e) {
if (e instanceof RenderBreakError) { if (e.name === 'RenderBreakError') {
e.resolvedHTML = html e.resolvedHTML = html
throw e throw e
} }
throw e instanceof RenderError ? e : new RenderError(e, tpl) throw e.name === 'RenderError' ? e : new RenderError(e, tpl)
} }
} }
return html return html
+16 -18
View File
@@ -1,23 +1,25 @@
import * as lexical from '../parser/lexical' import * as lexical from '../parser/lexical'
import assert from '../util/assert' import assert from '../util/assert'
import Scope from 'src/scope/scope'
import { range } from 'src/util/underscore'
const operators = { const operators = {
'==': (l, r) => l === r, '==': (l: any, r: any) => l === r,
'!=': (l, r) => l !== r, '!=': (l: any, r: any) => l !== r,
'>': (l, r) => l !== null && r !== null && l > r, '>': (l: any, r: any) => l !== null && r !== null && l > r,
'<': (l, r) => l !== null && r !== null && l < r, '<': (l: any, r: any) => l !== null && r !== null && l < r,
'>=': (l, r) => l !== null && r !== null && l >= r, '>=': (l: any, r: any) => l !== null && r !== null && l >= r,
'<=': (l, r) => l !== null && r !== null && l <= r, '<=': (l: any, r: any) => l !== null && r !== null && l <= r,
'contains': (l, r) => { 'contains': (l: any, r: any) => {
if (!l) return false if (!l) return false
if (typeof l.indexOf !== 'function') return false if (typeof l.indexOf !== 'function') return false
return l.indexOf(r) > -1 return l.indexOf(r) > -1
}, },
'and': (l, r) => isTruthy(l) && isTruthy(r), 'and': (l: any, r: any) => isTruthy(l) && isTruthy(r),
'or': (l, r) => isTruthy(l) || isTruthy(r) 'or': (l: any, r: any) => isTruthy(l) || isTruthy(r)
} }
export function evalExp (exp, scope) { export function evalExp (exp: string, scope: Scope): any {
assert(scope, 'unable to evalExp: scope undefined') assert(scope, 'unable to evalExp: scope undefined')
const operatorREs = lexical.operators const operatorREs = lexical.operators
let match let match
@@ -35,17 +37,13 @@ export function evalExp (exp, scope) {
if ((match = exp.match(lexical.rangeLine))) { if ((match = exp.match(lexical.rangeLine))) {
const low = evalValue(match[1], scope) const low = evalValue(match[1], scope)
const high = evalValue(match[2], scope) const high = evalValue(match[2], scope)
const range = [] return range(low, high + 1)
for (let j = low; j <= high; j++) {
range.push(j)
}
return range
} }
return evalValue(exp, scope) return evalValue(exp, scope)
} }
export function evalValue (str, scope) { export function evalValue (str: string, scope: Scope) {
str = str && str.trim() str = str && str.trim()
if (!str) return undefined if (!str) return undefined
@@ -58,10 +56,10 @@ export function evalValue (str, scope) {
throw new TypeError(`cannot eval '${str}' as value`) throw new TypeError(`cannot eval '${str}' as value`)
} }
export function isTruthy (val) { export function isTruthy (val: any): boolean {
return !isFalsy(val) return !isFalsy(val)
} }
export function isFalsy (val) { export function isFalsy (val: any): boolean {
return val === false || undefined === val || val === null return val === false || undefined === val || val === null
} }
+4
View File
@@ -0,0 +1,4 @@
export default interface IContext {
[key: string]: any;
liquid_method_missing?: (key: string) => any;
}
+14 -10
View File
@@ -1,20 +1,23 @@
import * as _ from '../util/underscore' import * as _ from '../util/underscore'
import { __assign } from 'tslib'
import * as lexical from '../parser/lexical' import * as lexical from '../parser/lexical'
import assert from '../util/assert' import assert from '../util/assert'
import { NormalizedFullOptions, applyDefault } from '../liquid-options' import { NormalizedFullOptions, applyDefault } from '../liquid-options'
import BlockMode from './block-mode' import BlockMode from './block-mode'
import IContext from './icontext';
export default class Scope { export default class Scope {
opts: NormalizedFullOptions opts: NormalizedFullOptions
contexts: Array<object> contexts: Array<IContext>
blocks: object = {} blocks: object = {}
groups: {[key: string]: number} = {}
blockMode: BlockMode = BlockMode.OUTPUT blockMode: BlockMode = BlockMode.OUTPUT
constructor (ctx: object = {}, opts?: NormalizedFullOptions) { constructor (ctx: object = {}, opts?: NormalizedFullOptions) {
this.opts = applyDefault(opts) this.opts = applyDefault(opts)
this.contexts = [ctx || {}] this.contexts = [ctx || {}]
} }
getAll () { getAll () {
return this.contexts.reduce((ctx, val) => _.assign(ctx, val), _.create(null)) return this.contexts.reduce((ctx, val) => __assign(ctx, val), {})
} }
get (path: string): any { get (path: string): any {
const paths = this.propertyAccessSeq(path) const paths = this.propertyAccessSeq(path)
@@ -36,6 +39,7 @@ export default class Scope {
scope[key] = {} scope[key] = {}
} }
scope = scope[key] scope = scope[key]
return false
}) })
} }
unshift (ctx: object) { unshift (ctx: object) {
@@ -44,7 +48,7 @@ export default class Scope {
push (ctx: object) { push (ctx: object) {
return this.contexts.push(ctx) return this.contexts.push(ctx)
} }
pop (ctx?: object): object { pop (ctx?: object): object | undefined {
if (!arguments.length) { if (!arguments.length) {
return this.contexts.pop() return this.contexts.pop()
} }
@@ -64,7 +68,7 @@ export default class Scope {
} }
return null return null
} }
readProperty (obj, key) { readProperty (obj: IContext, key: string) {
let val let val
if (_.isNil(obj)) { if (_.isNil(obj)) {
val = undefined val = undefined
@@ -72,7 +76,7 @@ export default class Scope {
obj = toLiquid(obj) obj = toLiquid(obj)
val = key === 'size' ? readSize(obj) : obj[key] val = key === 'size' ? readSize(obj) : obj[key]
if (_.isFunction(obj.liquid_method_missing)) { if (_.isFunction(obj.liquid_method_missing)) {
val = obj.liquid_method_missing(key) val = obj.liquid_method_missing!(key)
} }
} }
if (_.isNil(val) && this.opts.strict_variables) { if (_.isNil(val) && this.opts.strict_variables) {
@@ -89,9 +93,9 @@ export default class Scope {
* accessSeq("foo['b]r']") // ['foo', 'b]r'] * accessSeq("foo['b]r']") // ['foo', 'b]r']
* accessSeq("foo[bar.coo]") // ['foo', 'bar'], for bar.coo == 'bar' * accessSeq("foo[bar.coo]") // ['foo', 'bar'], for bar.coo == 'bar'
*/ */
propertyAccessSeq (str) { propertyAccessSeq (str: string) {
str = String(str) str = String(str)
const seq = [] const seq: string[] = []
let name = '' let name = ''
let j let j
let i = 0 let i = 0
@@ -141,7 +145,7 @@ export default class Scope {
} }
} }
function toLiquid (obj) { function toLiquid (obj: IContext) {
if (_.isFunction(obj.to_liquid)) { if (_.isFunction(obj.to_liquid)) {
return obj.to_liquid() return obj.to_liquid()
} }
@@ -151,13 +155,13 @@ function toLiquid (obj) {
return obj return obj
} }
function readSize (obj) { function readSize (obj: IContext) {
if (!_.isNil(obj.size)) return obj.size if (!_.isNil(obj.size)) return obj.size
if (_.isArray(obj) || _.isString(obj)) return obj.length if (_.isArray(obj) || _.isString(obj)) return obj.length
return obj.size return obj.size
} }
function matchRightBracket (str, begin) { function matchRightBracket (str: string, begin: number) {
let stack = 1 // count of '[' - count of ']' let stack = 1 // count of '[' - count of ']'
for (let i = begin; i < str.length; i++) { for (let i = begin; i < str.length; i++) {
if (str[i] === '[') { if (str[i] === '[') {
+3
View File
@@ -0,0 +1,3 @@
type FilterImpl = (value: any, ...args: any[]) => any
export default FilterImpl
@@ -2,19 +2,18 @@ import assert from 'src/util/assert'
import * as lexical from 'src/parser/lexical' import * as lexical from 'src/parser/lexical'
import { evalValue } from 'src/render/syntax' import { evalValue } from 'src/render/syntax'
import Scope from 'src/scope/scope' import Scope from 'src/scope/scope'
import FilterImpl from './filter-impl'
type impl = (value: any, ...args: any[]) => any
const valueRE = new RegExp(`${lexical.value.source}`, 'g') const valueRE = new RegExp(`${lexical.value.source}`, 'g')
export default class Filter { export default class Filter {
name: string name: string
impl: impl impl: FilterImpl
args: string[] args: string[]
private static impls: {[key: string]: impl} = {} private static impls: {[key: string]: FilterImpl} = {}
constructor (str: string, strictFilters: boolean = false) { constructor (str: string, strictFilters: boolean = false) {
const match = lexical.filterLine.exec(str) const match = lexical.filterLine.exec(str) as string[]
assert(match, 'illegal filter: ' + str) assert(match, 'illegal filter: ' + str)
const name = match[1] const name = match[1]
@@ -27,7 +26,7 @@ export default class Filter {
this.args = this.parseArgs(argList) this.args = this.parseArgs(argList)
} }
parseArgs (argList: string): string[] { parseArgs (argList: string): string[] {
let match; const args = [] let match; const args: string[] = []
while ((match = valueRE.exec(argList.trim()))) { while ((match = valueRE.exec(argList.trim()))) {
const v = match[0] const v = match[0]
const re = new RegExp(`${v}\\s*:`, 'g') const re = new RegExp(`${v}\\s*:`, 'g')
@@ -39,10 +38,9 @@ export default class Filter {
} }
render (value: any, scope: Scope): any { render (value: any, scope: Scope): any {
const args = this.args.map(arg => evalValue(arg, scope)) const args = this.args.map(arg => evalValue(arg, scope))
args.unshift(value) return this.impl.apply(null, [value, ...args])
return this.impl.apply(null, args)
} }
static register (name, filter) { static register (name: string, filter: FilterImpl) {
Filter.impls[name] = filter Filter.impls[name] = filter
} }
static clear () { static clear () {
+3 -3
View File
@@ -1,10 +1,10 @@
import Template from 'src/template/template' import Template from 'src/template/template'
import ITemplate from 'src/template/itemplate' import ITemplate from 'src/template/itemplate'
import Token from 'src/parser/token' import HTMLToken from 'src/parser/html-token'
export default class extends Template implements ITemplate { export default class extends Template<HTMLToken> implements ITemplate {
str: string str: string
constructor (token: Token) { constructor (token: HTMLToken) {
super(token) super(token)
this.str = token.value this.str = token.value
} }
+1 -1
View File
@@ -5,7 +5,7 @@ import ITemplate from 'src/template/itemplate'
import Scope from 'src/scope/scope' import Scope from 'src/scope/scope'
import OutputToken from 'src/parser/output-token' import OutputToken from 'src/parser/output-token'
export default class Output extends Template implements ITemplate { export default class Output extends Template<OutputToken> implements ITemplate {
value: Value value: Value
constructor (token: OutputToken, strictFilters?: boolean) { constructor (token: OutputToken, strictFilters?: boolean) {
super(token) super(token)
+45
View File
@@ -0,0 +1,45 @@
import { create, stringify } from 'src/util/underscore'
import assert from 'src/util/assert'
import Scope from 'src/scope/scope'
import ITagImpl from './itag-impl'
import ITagImplOptions from './itag-impl-options'
import Liquid from 'src/liquid'
import Hash from './hash'
import Template from 'src/template/template'
import ITemplate from 'src/template/itemplate'
import TagToken from 'src/parser/tag-token'
import Token from 'src/parser/token'
export default class Tag extends Template<TagToken> implements ITemplate {
name: string
private impl: ITagImpl
static impls: { [key: string]: ITagImplOptions } = {}
constructor (token: TagToken, tokens: Token[], liquid: Liquid) {
super(token)
this.name = token.name
const impl = Tag.impls[token.name]
assert(impl, `tag ${token.name} not found`)
this.impl = create<ITagImplOptions, ITagImpl>(impl)
this.impl.liquid = liquid
if (this.impl.parse) {
this.impl.parse(token, tokens)
}
}
async render (scope: Scope) {
const hash = new Hash(this.token.args, scope)
const impl = this.impl
if (typeof impl.render !== 'function') {
return ''
}
const html = await impl.render(scope, hash)
return stringify(html)
}
static register (name: string, tag: ITagImplOptions) {
Tag.impls[name] = tag
}
static clear () {
Tag.impls = {}
}
}
+2 -1
View File
@@ -1,5 +1,6 @@
import { hashCapture } from 'src/parser/lexical' import { hashCapture } from 'src/parser/lexical'
import { evalValue } from 'src/render/syntax' import { evalValue } from 'src/render/syntax'
import Scope from 'src/scope/scope';
/** /**
* Key-Value Pairs Representing Tag Arguments * Key-Value Pairs Representing Tag Arguments
@@ -9,7 +10,7 @@ import { evalValue } from 'src/render/syntax'
*/ */
export default class Hash { export default class Hash {
[key: string]: any [key: string]: any
constructor (markup, scope) { constructor (markup: string, scope: Scope) {
let match let match
hashCapture.lastIndex = 0 hashCapture.lastIndex = 0
while ((match = hashCapture.exec(markup))) { while ((match = hashCapture.exec(markup))) {
+2 -1
View File
@@ -2,5 +2,6 @@ import Liquid from 'src/liquid'
import ITagImplOptions from './itag-impl-options' import ITagImplOptions from './itag-impl-options'
export default interface ITagImpl extends ITagImplOptions { export default interface ITagImpl extends ITagImplOptions {
liquid: Liquid liquid: Liquid,
[key: string]: any
} }
+1 -2
View File
@@ -10,9 +10,8 @@ import ITemplate from 'src/template/itemplate'
import TagToken from 'src/parser/tag-token' import TagToken from 'src/parser/tag-token'
import Token from 'src/parser/token' import Token from 'src/parser/token'
export default class Tag extends Template implements ITemplate { export default class Tag extends Template<TagToken> implements ITemplate {
name: string name: string
token: TagToken
private impl: ITagImpl private impl: ITagImpl
static impls: { [key: string]: ITagImplOptions } = {} static impls: { [key: string]: ITagImplOptions } = {}
+3 -5
View File
@@ -1,8 +1,6 @@
import Token from 'src/parser/token' export default abstract class Template<T> {
token: T;
export default class Template { constructor (token: T) {
token: Token;
constructor (token) {
this.token = token this.token = token
} }
} }
+5 -9
View File
@@ -1,26 +1,22 @@
import { evalExp } from 'src/render/syntax' import { evalExp } from 'src/render/syntax'
import * as lexical from 'src/parser/lexical' import * as lexical from 'src/parser/lexical'
import assert from 'src/util/assert' import assert from 'src/util/assert'
import Filter from './filter' import Filter from './filter/filter'
import Scope from 'src/scope/scope' import Scope from 'src/scope/scope'
export default class { export default class {
initial: any initial: any
filters: Array<any> filters: Array<Filter> = []
constructor (str: string, strictFilters?: boolean) { constructor (str: string, strictFilters?: boolean) {
let match = lexical.matchValue(str) let match: RegExpExecArray | null = lexical.matchValue(str) as RegExpExecArray
assert(match, `illegal value string: ${str}`) assert(match, `illegal value string: ${str}`)
const initial = match[0] this.initial = match[0]
str = str.substr(match.index + match[0].length) str = str.substr(match.index + match[0].length)
const filters = []
while ((match = lexical.filter.exec(str))) { while ((match = lexical.filter.exec(str))) {
filters.push([match[0].trim()]) this.filters.push(new Filter(match[0].trim(), strictFilters))
} }
this.initial = initial
this.filters = filters.map(str => new Filter(str, strictFilters))
} }
value (scope: Scope) { value (scope: Scope) {
return this.filters.reduce( return this.filters.reduce(
+35 -61
View File
@@ -1,98 +1,77 @@
import * as _ from './underscore' import * as _ from './underscore'
import { __extends } from 'tslib'
import Token from 'src/parser/token' import Token from 'src/parser/token'
import ITemplate from 'src/template/itemplate'
function captureStack () { abstract class LiquidError extends Error {
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor)
}
}
abstract class LiquidError {
name: string
message: string
stack: string
private file: string
private input: string
private token: Token private token: Token
private originalError: Error private originalError: Error
constructor (err, token) { constructor (err: Error, token: Token) {
this.input = token.input super(err.message)
this.file = token.file
this.originalError = err this.originalError = err
this.token = token this.token = token
} }
captureStackTrace (obj) { protected update() {
this.name = obj.constructor.name
captureStack.call(obj)
const err = this.originalError const err = this.originalError
const context = mkContext(this.input, this.token.line) const context = mkContext(this.token)
this.message = mkMessage(err.message, this.token) this.message = mkMessage(err.message, this.token)
this.stack = this.message + '\n' + context + this.stack = this.message + '\n' + context +
'\n' + (this.stack || this.message) + '\n' + this.stack + '\nFrom ' + err.stack
(err.stack ? '\nFrom ' + err.stack : '')
} }
} }
export class TokenizationError extends LiquidError { export class TokenizationError extends LiquidError {
constructor (message, token) { constructor (message: string, token: Token) {
super({ message }, token) super(new Error(message), token)
super.captureStackTrace(this) this.name = 'TokenizationError'
super.update()
} }
} }
TokenizationError.prototype = _.create(Error.prototype) as any
TokenizationError.prototype.constructor = TokenizationError
export class ParseError extends LiquidError { export class ParseError extends LiquidError {
constructor (err, token) { constructor (err: Error, token: Token) {
super(err, token) super(err, token)
this.name = 'ParseError'
this.message = err.message this.message = err.message
super.captureStackTrace(this) super.update()
} }
} }
ParseError.prototype = _.create(Error.prototype) as any
ParseError.prototype.constructor = ParseError
export class RenderError extends LiquidError { export class RenderError extends LiquidError {
constructor (err, tpl) { constructor (err: Error, tpl: ITemplate) {
super(err, tpl.token) super(err, tpl.token)
this.name = 'RenderError'
this.message = err.message this.message = err.message
super.captureStackTrace(this) super.update()
} }
} }
RenderError.prototype = _.create(Error.prototype) as any
RenderError.prototype.constructor = RenderError
export class RenderBreakError { export class RenderBreakError extends Error {
message: string resolvedHTML: string = ''
resolvedHTML: string constructor (message: string) {
constructor (message) { super(message)
captureStack.call(this) this.name = 'RenderBreakError'
this.message = message + '' this.message = message + ''
} }
} }
RenderBreakError.prototype = _.create(Error.prototype) as any
RenderBreakError.prototype.constructor = RenderBreakError
export class AssertionError { export class AssertionError extends Error {
message: string constructor (message: string) {
constructor (message) { super(message)
captureStack.call(this) this.name = 'AssertionError'
this.message = message + '' this.message = message + ''
} }
} }
AssertionError.prototype = _.create(Error.prototype) as any
AssertionError.prototype.constructor = AssertionError
function mkContext (input, targetLine) { function mkContext (token: Token) {
const lines = input.split('\n') const lines = token.input.split('\n')
const begin = Math.max(targetLine - 2, 1) const begin = Math.max(token.line - 2, 1)
const end = Math.min(targetLine + 3, lines.length) const end = Math.min(token.line + 3, lines.length)
const context = _ const context = _
.range(begin, end + 1) .range(begin, end + 1)
.map(lineNumber => { .map(lineNumber => {
const indicator = (lineNumber === targetLine) ? '>> ' : ' ' const indicator = (lineNumber === token.line) ? '>> ' : ' '
const num = _.padStart(String(lineNumber), String(end).length) const num = _.padStart(String(lineNumber), String(end).length)
const text = lines[lineNumber - 1] const text = lines[lineNumber - 1]
return `${indicator}${num}| ${text}` return `${indicator}${num}| ${text}`
@@ -102,13 +81,8 @@ function mkContext (input, targetLine) {
return context return context
} }
function mkMessage (msg, token) { function mkMessage (msg: string, token: Token) {
msg = msg || '' if (token.file) msg += `, file:${token.file}`
if (token.file) { msg += `, line:${token.line}, col:${token.col}`
msg += ', file:' + token.file
}
if (token.line) {
msg += `, line:${token.line}, col:${token.col}`
}
return msg return msg
} }
+6 -17
View File
@@ -1,26 +1,15 @@
/*
* Call functions in serial until someone resolved.
* @param iterable the array to iterate with.
* @param iteratee returns a new promise.
* The iteratee is invoked with three arguments: (value, index, iterable).
*/
export function anySeries (iterable, iteratee) {
let ret: Promise<any> = Promise.reject(new Error('init'))
iterable.forEach(function (item, idx) {
ret = ret.catch(() => iteratee(item, idx, iterable))
})
return ret
}
/* /*
* Call functions in serial until someone rejected. * Call functions in serial until someone rejected.
* @param {Array} iterable the array to iterate with. * @param {Array} iterable the array to iterate with.
* @param {Array} iteratee returns a new promise. * @param {Array} iteratee returns a new promise.
* The iteratee is invoked with three arguments: (value, index, iterable). * The iteratee is invoked with three arguments: (value, index, iterable).
*/ */
export function mapSeries (iterable, iteratee) { export function mapSeries<T1, T2> (
let ret: Promise<any> = Promise.resolve('init') iterable: T1[],
const result = [] iteratee: (item: T1, idx: number, iterable: T1[]) => Promise<T2> | T2
): Promise<T2[]> {
let ret = Promise.resolve(0)
const result: T2[] = []
iterable.forEach(function (item, idx) { iterable.forEach(function (item, idx) {
ret = ret ret = ret
.then(() => iteratee(item, idx, iterable)) .then(() => iteratee(item, idx, iterable))
+39 -39
View File
@@ -16,18 +16,18 @@ const suffixes = {
'default': 'th' 'default': 'th'
} }
function abbr (str) { function abbr (str: string) {
return str.slice(0, 3) return str.slice(0, 3)
} }
// prototype extensions // prototype extensions
const _date = { const _date = {
daysInMonth: function (d) { daysInMonth: function (d: Date) {
const feb = _date.isLeapYear(d) ? 29 : 28 const feb = _date.isLeapYear(d) ? 29 : 28
return [31, feb, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] return [31, feb, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
}, },
getDayOfYear: function (d) { getDayOfYear: function (d: Date) {
let num = 0 let num = 0
for (let i = 0; i < d.getMonth(); ++i) { for (let i = 0; i < d.getMonth(); ++i) {
num += _date.daysInMonth(d)[i] num += _date.daysInMonth(d)[i]
@@ -35,7 +35,7 @@ const _date = {
return num + d.getDate() return num + d.getDate()
}, },
getWeekOfYear: function (d, startDay) { getWeekOfYear: function (d: Date, startDay: number) {
// Skip to startDay of this week // Skip to startDay of this week
const now = this.getDayOfYear(d) + (startDay - d.getDay()) const now = this.getDayOfYear(d) + (startDay - d.getDay())
// Find the first startDay of the year // Find the first startDay of the year
@@ -44,111 +44,111 @@ const _date = {
return padStart(String(Math.floor((now - then) / 7) + 1), 2, '0') return padStart(String(Math.floor((now - then) / 7) + 1), 2, '0')
}, },
isLeapYear: function (d) { isLeapYear: function (d: Date) {
const year = d.getFullYear() const year = d.getFullYear()
return !!((year & 3) === 0 && (year % 100 || (year % 400 === 0 && year))) return !!((year & 3) === 0 && (year % 100 || (year % 400 === 0 && year)))
}, },
getSuffix: function (d) { getSuffix: function (d: Date) {
const str = d.getDate().toString() const str = d.getDate().toString()
const index = parseInt(str.slice(-1)) const index = parseInt(str.slice(-1))
return suffixes[index] || suffixes['default'] return suffixes[index] || suffixes['default']
}, },
century: function (d) { century: function (d: Date) {
return parseInt(d.getFullYear().toString().substring(0, 2), 10) return parseInt(d.getFullYear().toString().substring(0, 2), 10)
} }
} }
const formatCodes = { const formatCodes = {
a: function (d) { a: function (d: Date) {
return dayNamesShort[d.getDay()] return dayNamesShort[d.getDay()]
}, },
A: function (d) { A: function (d: Date) {
return dayNames[d.getDay()] return dayNames[d.getDay()]
}, },
b: function (d) { b: function (d: Date) {
return monthNamesShort[d.getMonth()] return monthNamesShort[d.getMonth()]
}, },
B: function (d) { B: function (d: Date) {
return monthNames[d.getMonth()] return monthNames[d.getMonth()]
}, },
c: function (d) { c: function (d: Date) {
return d.toLocaleString() return d.toLocaleString()
}, },
C: function (d) { C: function (d: Date) {
return _date.century(d) return _date.century(d)
}, },
d: function (d) { d: function (d: Date) {
return padStart(d.getDate(), 2, '0') return padStart(d.getDate(), 2, '0')
}, },
e: function (d) { e: function (d: Date) {
return padStart(d.getDate(), 2) return padStart(d.getDate(), 2)
}, },
H: function (d) { H: function (d: Date) {
return padStart(d.getHours(), 2, '0') return padStart(d.getHours(), 2, '0')
}, },
I: function (d) { I: function (d: Date) {
return padStart(String(d.getHours() % 12 || 12), 2, '0') return padStart(String(d.getHours() % 12 || 12), 2, '0')
}, },
j: function (d) { j: function (d: Date) {
return padStart(_date.getDayOfYear(d), 3, '0') return padStart(_date.getDayOfYear(d), 3, '0')
}, },
k: function (d) { k: function (d: Date) {
return padStart(d.getHours(), 2) return padStart(d.getHours(), 2)
}, },
l: function (d) { l: function (d: Date) {
return padStart(String(d.getHours() % 12 || 12), 2) return padStart(String(d.getHours() % 12 || 12), 2)
}, },
L: function (d) { L: function (d: Date) {
return padStart(d.getMilliseconds(), 3, '0') return padStart(d.getMilliseconds(), 3, '0')
}, },
m: function (d) { m: function (d: Date) {
return padStart(d.getMonth() + 1, 2, '0') return padStart(d.getMonth() + 1, 2, '0')
}, },
M: function (d) { M: function (d: Date) {
return padStart(d.getMinutes(), 2, '0') return padStart(d.getMinutes(), 2, '0')
}, },
p: function (d) { p: function (d: Date) {
return (d.getHours() < 12 ? 'AM' : 'PM') return (d.getHours() < 12 ? 'AM' : 'PM')
}, },
P: function (d) { P: function (d: Date) {
return (d.getHours() < 12 ? 'am' : 'pm') return (d.getHours() < 12 ? 'am' : 'pm')
}, },
q: function (d) { q: function (d: Date) {
return _date.getSuffix(d) return _date.getSuffix(d)
}, },
s: function (d) { s: function (d: Date) {
return Math.round(d.valueOf() / 1000) return Math.round(d.valueOf() / 1000)
}, },
S: function (d) { S: function (d: Date) {
return padStart(d.getSeconds(), 2, '0') return padStart(d.getSeconds(), 2, '0')
}, },
u: function (d) { u: function (d: Date) {
return d.getDay() || 7 return d.getDay() || 7
}, },
U: function (d) { U: function (d: Date) {
return _date.getWeekOfYear(d, 0) return _date.getWeekOfYear(d, 0)
}, },
w: function (d) { w: function (d: Date) {
return d.getDay() return d.getDay()
}, },
W: function (d) { W: function (d: Date) {
return _date.getWeekOfYear(d, 1) return _date.getWeekOfYear(d, 1)
}, },
x: function (d) { x: function (d: Date) {
return d.toLocaleDateString() return d.toLocaleDateString()
}, },
X: function (d) { X: function (d: Date) {
return d.toLocaleTimeString() return d.toLocaleTimeString()
}, },
y: function (d) { y: function (d: Date) {
return d.getFullYear().toString().substring(2, 4) return d.getFullYear().toString().substring(2, 4)
}, },
Y: function (d) { Y: function (d: Date) {
return d.getFullYear() return d.getFullYear()
}, },
z: function (d) { z: function (d: Date) {
const tz = d.getTimezoneOffset() / 60 * 100 const tz = d.getTimezoneOffset() / 60 * 100
return (tz > 0 ? '-' : '+') + padStart(String(Math.abs(tz)), 4, '0') return (tz > 0 ? '-' : '+') + padStart(String(Math.abs(tz)), 4, '0')
}, },
@@ -159,7 +159,7 @@ const formatCodes = {
(formatCodes as any).h = formatCodes.b; (formatCodes as any).h = formatCodes.b;
(formatCodes as any).N = formatCodes.L (formatCodes as any).N = formatCodes.L
export default function (d, format) { export default function (d: Date, format: string) {
let output = '' let output = ''
let remaining = format let remaining = format
@@ -179,6 +179,6 @@ export default function (d, format) {
// Add the format code // Add the format code
const ch = results[0].charAt(1) const ch = results[0].charAt(1)
const func = formatCodes[ch] const func = formatCodes[ch]
output += func ? func.call(this, d) : '%' + ch output += func ? func.call(null, d) : '%' + ch
} }
} }
+15 -43
View File
@@ -14,10 +14,12 @@ export function isFunction (value: any) {
return typeof value === 'function' return typeof value === 'function'
} }
export function promisify (fn) { export function promisify<T1, T2> (fn: (arg1: T1, cb: (err: Error | null, result: T2) => void) => void): (arg1: T1) => Promise<T2>;
return function (...args) { export function promisify<T1, T2, T3> (fn: (arg1: T1, arg2: T2, cb: (err: Error | null, result: T3) => void) => void):(arg1: T1, arg2: T2) => Promise<T3>;
export function promisify (fn: any) {
return function (...args: any[]) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fn(...args, (err, result) => { fn(...args, (err: Error, result: any) => {
err ? reject(err) : resolve(result) err ? reject(err) : resolve(result)
}) })
}) })
@@ -35,7 +37,7 @@ export function stringify (value: any): string {
} }
function defaultToString (value: any): string { function defaultToString (value: any): string {
const cache = [] const cache: string[] = []
return JSON.stringify(value, (key, value) => { return JSON.stringify(value, (key, value) => {
if (isObject(value)) { if (isObject(value)) {
if (cache.indexOf(value) !== -1) { if (cache.indexOf(value) !== -1) {
@@ -75,7 +77,10 @@ export function isError (value: any): boolean {
* @param {Function} iteratee The function invoked per iteration. * @param {Function} iteratee The function invoked per iteration.
* @return {Object} Returns object. * @return {Object} Returns object.
*/ */
export function forOwn (object, iteratee: ((val: any, key: string, obj: object) => boolean | void)) { export function forOwn <T>(
object: {[key: string]: T} | undefined,
iteratee: ((val: T, key: string, obj: {[key: string]: T}) => boolean | void)
) {
object = object || {} object = object || {}
for (const k in object) { for (const k in object) {
if (object.hasOwnProperty(k)) { if (object.hasOwnProperty(k)) {
@@ -85,45 +90,12 @@ export function forOwn (object, iteratee: ((val: any, key: string, obj: object)
return object return object
} }
/* export function last <T>(arr: T[]): T;
* Assigns own enumerable string keyed properties of source objects to the destination object. export function last (arr: string): string;
* Source objects are applied from left to right. export function last (arr: any[] | string): any | string {
* Subsequent sources overwrite property assignments of previous sources.
*
* Note: This method mutates object and is loosely based on Object.assign.
*
* @param {Object} object The destination object.
* @param {...Object} sources The source objects.
* @return {Object} Returns object.
*/
export function assign (obj: object, ...srcs: object[]): object {
obj = isObject(obj) ? obj : {}
srcs.forEach(src => binaryAssign(obj, src))
return obj
}
function binaryAssign (target: object, src: object): object {
for (const key in src) if (src.hasOwnProperty(key)) target[key] = src[key]
return target
}
export function last (arr: any[]): any {
return arr[arr.length - 1] return arr[arr.length - 1]
} }
export function uniq (arr: any[]): any[] {
const u = {}
const a = []
for (let i = 0, l = arr.length; i < l; ++i) {
if (u.hasOwnProperty(arr[i])) {
continue
}
a.push(arr[i])
u[arr[i]] = 1
}
return a
}
/* /*
* Checks if value is the language type of Object. * Checks if value is the language type of Object.
* (e.g. arrays, functions, objects, regexes, new Number(0), and new String('')) * (e.g. arrays, functions, objects, regexes, new Number(0), and new String(''))
@@ -144,13 +116,13 @@ export function isObject (value: any): boolean {
* negative — if you'd like a negative range, use a negative step. * negative — if you'd like a negative range, use a negative step.
*/ */
export function range (start: number, stop?: number, step?: number) { export function range (start: number, stop?: number, step?: number) {
if (arguments.length === 1) { if (stop === undefined) {
stop = start stop = start
start = 0 start = 0
} }
step = step || 1 step = step || 1
const arr = [] const arr: number[] = []
for (let i = start; i < stop; i += step) { for (let i = start; i < stop; i += step) {
arr.push(i) arr.push(i)
} }
+2 -2
View File
@@ -2,10 +2,10 @@ import { expect } from 'chai'
import Liquid from '../..' import Liquid from '../..'
describe('.evalValue()', function () { describe('.evalValue()', function () {
var engine var engine: Liquid
beforeEach(() => { engine = new Liquid() }) beforeEach(() => { engine = new Liquid() })
it('should throw when scope undefined', function () { it('should throw when scope undefined', function () {
expect(() => engine.evalValue('{{"foo"}}')).to.throw(/scope undefined/) expect(() => engine.evalValue('{{"foo"}}', null as any)).to.throw(/scope undefined/)
}) })
}) })
+2 -2
View File
@@ -8,7 +8,7 @@ describe('express()', function () {
const root = resolve(__dirname, '../stub/root') const root = resolve(__dirname, '../stub/root')
const views = resolve(__dirname, '../stub/views') const views = resolve(__dirname, '../stub/views')
const partials = resolve(__dirname, '../stub/partials') const partials = resolve(__dirname, '../stub/partials')
let app, engine let app: express.Application, engine: Liquid
beforeEach(function () { beforeEach(function () {
app = express() app = express()
@@ -45,7 +45,7 @@ describe('express()', function () {
} }
const file = '/not-exist.html' const file = '/not-exist.html'
const ctx = {} const ctx = {}
engine.express().call(view, file, ctx, function (err) { engine.express().call(view, file, ctx, function (err: any) {
try { try {
expect(err.code).to.equal('ENOENT') expect(err.code).to.equal('ENOENT')
expect(err.message).to.match(/Failed to lookup/) expect(err.message).to.match(/Failed to lookup/)
+1 -1
View File
@@ -5,7 +5,7 @@ import * as chaiAsPromised from 'chai-as-promised'
use(chaiAsPromised) use(chaiAsPromised)
describe('.parseAndRender()', function () { describe('.parseAndRender()', function () {
var engine, strictEngine var engine: Liquid, strictEngine: Liquid
beforeEach(function () { beforeEach(function () {
engine = new Liquid() engine = new Liquid()
strictEngine = new Liquid({ strictEngine = new Liquid({
+1 -1
View File
@@ -8,7 +8,7 @@ use(chaiAsPromised)
describe('#renderFile()', function () { describe('#renderFile()', function () {
const root = resolve(__dirname, '../stub/root') const root = resolve(__dirname, '../stub/root')
const views = resolve(__dirname, '../stub/views') const views = resolve(__dirname, '../stub/views')
let engine let engine: Liquid
beforeEach(function () { beforeEach(function () {
engine = new Liquid({ engine = new Liquid({
root, root,
+6 -6
View File
@@ -1,5 +1,5 @@
import Liquid from '../../dist/liquid.js' import Liquid from '../../dist/liquid.js'
import { createFakeServer, useFakeXMLHttpRequest } from 'sinon' import * as sinon from 'sinon'
import { expect, use } from 'chai' import { expect, use } from 'chai'
import { JSDOM } from 'jsdom' import { JSDOM } from 'jsdom'
import * as chaiAsPromised from 'chai-as-promised' import * as chaiAsPromised from 'chai-as-promised'
@@ -7,13 +7,13 @@ import * as chaiAsPromised from 'chai-as-promised'
use(chaiAsPromised) use(chaiAsPromised)
describe('xhr', () => { describe('xhr', () => {
if (+process.version.match(/^v(\d+)/)[1] < 8) { if (+(process.version.match(/^v(\d+)/) as RegExpMatchArray)[1] < 8) {
console.info('jsdom not supported, skipping xhr...') console.info('jsdom not supported, skipping xhr...')
return return
} }
let server, engine let server: sinon.SinonFakeServer, engine: Liquid
beforeEach(() => { beforeEach(() => {
server = createFakeServer() server = sinon.fakeServer.create()
server.autoRespond = true server.autoRespond = true
server.respondWith('GET', 'https://example.com/views/hello.html', server.respondWith('GET', 'https://example.com/views/hello.html',
[200, { 'Content-Type': 'text/plain' }, 'hello {{name}}']) [200, { 'Content-Type': 'text/plain' }, 'hello {{name}}'])
@@ -22,7 +22,7 @@ describe('xhr', () => {
contentType: 'text/html', contentType: 'text/html',
includeNodeLocations: true includeNodeLocations: true
}); });
(global as any).XMLHttpRequest = useFakeXMLHttpRequest(); (global as any).XMLHttpRequest = sinon.FakeXMLHttpRequest;
(global as any).document = dom.window.document (global as any).document = dom.window.document
engine = new Liquid({ engine = new Liquid({
root: 'https://example.com/views/', root: 'https://example.com/views/',
@@ -68,7 +68,7 @@ describe('xhr', () => {
it('should throw error', function () { it('should throw error', function () {
const result = expect(engine.renderFile('hello.html')) const result = expect(engine.renderFile('hello.html'))
.to.be.rejectedWith('An error occurred whilst receiving the response.'); .to.be.rejectedWith('An error occurred whilst receiving the response.');
(global as any).XMLHttpRequest.onCreate = function (request) { (global as any).XMLHttpRequest.onCreate = function (request: sinon.SinonFakeXMLHttpRequest) {
setTimeout(() => request.error()) setTimeout(() => request.error())
} }
return result return result
+1 -1
View File
@@ -21,7 +21,7 @@ export function mock (options: { [path: string]: (string | fileDescriptor) }) {
return file.content return file.content
} }
fs.exists = async function (path) { fs.exists = async function (path: string) {
console.log('mock fs exists called', path) console.log('mock fs exists called', path)
return !!files[path] return !!files[path]
} }
+1 -1
View File
@@ -12,7 +12,7 @@ export const ctx = {
posts: [{ category: 'foo' }, { category: 'bar' }] posts: [{ category: 'foo' }, { category: 'bar' }]
} }
export async function test (src, dst) { export async function test (src: string, dst: string) {
const html = await liquid.parseAndRender(src, ctx) const html = await liquid.parseAndRender(src, ctx)
return expect(html).to.equal(dst) return expect(html).to.equal(dst)
} }
+2 -1
View File
@@ -1,11 +1,12 @@
import Liquid from 'src/liquid' import Liquid from 'src/liquid'
import { expect, use } from 'chai' import { expect, use } from 'chai'
import * as chaiAsPromised from 'chai-as-promised' import * as chaiAsPromised from 'chai-as-promised'
import IContext from 'src/scope/icontext';
use(chaiAsPromised) use(chaiAsPromised)
describe('tags/for', function () { describe('tags/for', function () {
let liquid, ctx let liquid: Liquid, ctx: IContext
before(function () { before(function () {
liquid = new Liquid() liquid = new Liquid()
liquid.registerTag('throwingTag', { liquid.registerTag('throwingTag', {
+1 -1
View File
@@ -3,7 +3,7 @@ import { expect } from 'chai'
import { mock, restore } from 'test/stub/mockfs' import { mock, restore } from 'test/stub/mockfs'
describe('tags/include', function () { describe('tags/include', function () {
let liquid let liquid: Liquid
before(function () { before(function () {
liquid = new Liquid({ liquid = new Liquid({
root: '/', root: '/',
+1 -1
View File
@@ -3,7 +3,7 @@ import { expect } from 'chai'
import { mock, restore } from 'test/stub/mockfs' import { mock, restore } from 'test/stub/mockfs'
describe('tags/layout', function () { describe('tags/layout', function () {
let liquid let liquid: Liquid
before(function () { before(function () {
liquid = new Liquid({ liquid = new Liquid({
root: '/', root: '/',
+1 -1
View File
@@ -5,7 +5,7 @@ import * as chaiAsPromised from 'chai-as-promised'
use(chaiAsPromised) use(chaiAsPromised)
describe('tags/unless', function () { describe('tags/unless', function () {
let liquid let liquid: Liquid
before(() => { liquid = new Liquid() }) before(() => { liquid = new Liquid() })
it('should render else when predicate yields true', async function () { it('should render else when predicate yields true', async function () {
+3 -3
View File
@@ -8,7 +8,7 @@ const resolve = fs.resolve
describe('fs/browser', function () { describe('fs/browser', function () {
describe('#resolve()', function () { describe('#resolve()', function () {
if (+process.version.match(/^v(\d+)/)[1] < 8) { if (+(process.version.match(/^v(\d+)/) as RegExpMatchArray)[1] < 8) {
console.info('jsdom not supported, skipping template-browser...') console.info('jsdom not supported, skipping template-browser...')
return return
} }
@@ -54,9 +54,9 @@ describe('fs/browser', function () {
}) })
describe('#readFile()', () => { describe('#readFile()', () => {
let server let server: sinon.SinonFakeServer
beforeEach(() => { beforeEach(() => {
server = sinon.createFakeServer() server = sinon.fakeServer.create()
server.autoRespond = true server.autoRespond = true
server.respondWith('GET', 'https://example.com/views/hello.html', server.respondWith('GET', 'https://example.com/views/hello.html',
[200, { 'Content-Type': 'text/plain' }, 'hello {{name}}']); [200, { 'Content-Type': 'text/plain' }, 'hello {{name}}']);
+1 -1
View File
@@ -3,7 +3,7 @@ import Liquid from 'src/liquid'
import { mock, restore } from 'test/stub/mockfs' import { mock, restore } from 'test/stub/mockfs'
describe('LiquidOptions#cache', function () { describe('LiquidOptions#cache', function () {
let engine let engine: Liquid
beforeEach(function () { beforeEach(function () {
engine = new Liquid({ engine = new Liquid({
root: '/root/', root: '/root/',
+2 -2
View File
@@ -33,14 +33,14 @@ describe('Liquid', function () {
}) })
after(restore) after(restore)
it('should render single template', function (done) { it('should render single template', function (done) {
render.call({ root: '.' }, 'foo', null, (err, result) => { render.call({ root: '.' }, 'foo', null as any, (err: Error | null, result: string | undefined) => {
if (err) return done(err) if (err) return done(err)
expect(result).to.equal('foo') expect(result).to.equal('foo')
done() done()
}) })
}) })
it('should render single template with Array-typed root', function (done) { it('should render single template with Array-typed root', function (done) {
render.call({ root: ['.'] }, 'foo', null, (err, result) => { render.call({ root: ['.'] }, 'foo', null as any, (err: Error | null, result: string | undefined) => {
if (err) return done(err) if (err) return done(err)
expect(result).to.equal('foo') expect(result).to.equal('foo')
done() done()
+1 -1
View File
@@ -2,7 +2,7 @@ import Liquid from 'src/liquid'
import { expect } from 'chai' import { expect } from 'chai'
describe('LiquidOptions#strict_*', function () { describe('LiquidOptions#strict_*', function () {
let engine let engine: Liquid
const ctx = {} const ctx = {}
beforeEach(function () { beforeEach(function () {
engine = new Liquid({ engine = new Liquid({
+1 -1
View File
@@ -56,7 +56,7 @@ describe('tokenizer', function () {
const tokens = tokenizer.tokenize(html) const tokens = tokenizer.tokenize(html)
expect(tokens.length).to.equal(1) expect(tokens.length).to.equal(1)
expect(tokens[0]).instanceOf(TagToken) expect(tokens[0]).instanceOf(TagToken)
expect(tokens[0].args).to.equal('a:a\nb:1.23') expect((tokens[0] as TagToken).args).to.equal('a:a\nb:1.23')
expect(tokens[0].raw).to.equal('{%foo\na:a\nb:1.23\n%}') expect(tokens[0].raw).to.equal('{%foo\na:a\nb:1.23\n%}')
}) })
it('should handle multiple lines value', function () { it('should handle multiple lines value', function () {
+3 -3
View File
@@ -2,12 +2,12 @@ import { expect } from 'chai'
import Scope from 'src/scope/scope' import Scope from 'src/scope/scope'
import Token from 'src/parser/token' import Token from 'src/parser/token'
import Tag from 'src/template/tag/tag' import Tag from 'src/template/tag/tag'
import Filter from 'src/template/filter' import Filter from 'src/template/filter/Filter'
import Render from 'src/render/render' import Render from 'src/render/render'
import HTML from 'src/template/html' import HTML from 'src/template/html'
describe('render', function () { describe('render', function () {
let render let render: Render
before(function () { before(function () {
Filter.clear() Filter.clear()
Tag.clear() Tag.clear()
@@ -16,7 +16,7 @@ describe('render', function () {
describe('.renderTemplates()', function () { describe('.renderTemplates()', function () {
it('should throw when scope undefined', function () { it('should throw when scope undefined', function () {
expect(render.renderTemplates([])).to.be.rejectedWith(/scope undefined/) expect(render.renderTemplates([], null as any)).to.be.rejectedWith(/scope undefined/)
}) })
it('should render html', async function () { it('should render html', async function () {
+1 -1
View File
@@ -3,7 +3,7 @@ import { expect } from 'chai'
import { evalExp, evalValue, isTruthy } from 'src/render/syntax' import { evalExp, evalValue, isTruthy } from 'src/render/syntax'
describe('expression', function () { describe('expression', function () {
let scope let scope: Scope
beforeEach(function () { beforeEach(function () {
scope = new Scope({ scope = new Scope({
+4 -3
View File
@@ -1,10 +1,11 @@
import * as chai from 'chai' import * as chai from 'chai'
import Scope from 'src/scope/scope' import Scope from 'src/scope/scope'
import IContext from 'src/scope/icontext';
const expect = chai.expect const expect = chai.expect
describe('scope', function () { describe('scope', function () {
let scope, ctx let scope: Scope, ctx: IContext
beforeEach(function () { beforeEach(function () {
ctx = { ctx = {
foo: 'zoo', foo: 'zoo',
@@ -68,7 +69,7 @@ describe('scope', function () {
} }
expect(fn).to.not.throw() expect(fn).to.not.throw()
expect(scope.get('notdefined')).to.equal(undefined) expect(scope.get('notdefined')).to.equal(undefined)
expect(scope.get(false)).to.equal(undefined) expect(scope.get(false as any)).to.equal(undefined)
}) })
it('should throw for invalid path', function () { it('should throw for invalid path', function () {
@@ -169,7 +170,7 @@ describe('scope', function () {
}) })
}) })
describe('strict_variables', function () { describe('strict_variables', function () {
let scope let scope: Scope
beforeEach(function () { beforeEach(function () {
scope = new Scope(ctx, { scope = new Scope(ctx, {
strict_variables: true strict_variables: true
+2 -2
View File
@@ -1,14 +1,14 @@
import * as chai from 'chai' import * as chai from 'chai'
import * as sinon from 'sinon' import * as sinon from 'sinon'
import * as sinonChai from 'sinon-chai' import * as sinonChai from 'sinon-chai'
import Filter from 'src/template/filter' import Filter from 'src/template/filter/filter'
import Scope from 'src/scope/scope' import Scope from 'src/scope/scope'
chai.use(sinonChai) chai.use(sinonChai)
const expect = chai.expect const expect = chai.expect
describe('filter', function () { describe('filter', function () {
let scope let scope: Scope
beforeEach(function () { beforeEach(function () {
Filter.clear() Filter.clear()
scope = new Scope() scope = new Scope()
+3 -3
View File
@@ -2,7 +2,7 @@ import * as chai from 'chai'
import Scope from 'src/scope/scope' import Scope from 'src/scope/scope'
import Output from 'src/template/output' import Output from 'src/template/output'
import OutputToken from 'src/parser/output-token' import OutputToken from 'src/parser/output-token'
import Filter from 'src/template/filter' import Filter from 'src/template/filter/filter'
const expect = chai.expect const expect = chai.expect
@@ -35,7 +35,7 @@ describe('Output', function () {
return expect(html).equal('{"num":2,"circular":{"bar":"bar"}}') return expect(html).equal('{"num":2,"circular":{"bar":"bar"}}')
}) })
it('should skip function property', async function () { it('should skip function property', async function () {
const scope = new Scope({ obj: { foo: 'foo', bar: x => x } }) const scope = new Scope({ obj: { foo: 'foo', bar: (x: any) => x } })
const output = new Output({ value: 'obj' } as OutputToken) const output = new Output({ value: 'obj' } as OutputToken)
const html = await output.render(scope) const html = await output.render(scope)
return expect(html).to.equal('{"foo":"foo"}') return expect(html).to.equal('{"foo":"foo"}')
@@ -53,7 +53,7 @@ describe('Output', function () {
return expect(str).to.equal('FOO') return expect(str).to.equal('FOO')
}) })
it('should respect to .liquid_method_missing()', async () => { it('should respect to .liquid_method_missing()', async () => {
const scope = new Scope({ obj: { liquid_method_missing: x => x.toUpperCase() } }) const scope = new Scope({ obj: { liquid_method_missing: (x: string) => x.toUpperCase() } })
const output = new Output({ value: 'obj.foo' } as OutputToken) const output = new Output({ value: 'obj.foo' } as OutputToken)
const str = await output.render(scope) const str = await output.render(scope)
return expect(str).to.equal('FOO') return expect(str).to.equal('FOO')
+3 -3
View File
@@ -11,7 +11,7 @@ const expect = chai.expect
const liquid = new Liquid() const liquid = new Liquid()
describe('tag', function () { describe('tag', function () {
let scope let scope: Scope
before(function () { before(function () {
scope = new Scope({ scope = new Scope({
foo: 'bar', foo: 'bar',
@@ -56,7 +56,7 @@ describe('tag', function () {
}) })
describe('hash', function () { describe('hash', function () {
let spy, token let spy: sinon.SinonSpy, token: TagToken
beforeEach(function () { beforeEach(function () {
spy = sinon.spy() spy = sinon.spy()
Tag.register('foo', { Tag.register('foo', {
@@ -67,7 +67,7 @@ describe('tag', function () {
value: 'foo aa:foo bb: arr[0] cc: 2.3\ndd:bar.coo', value: 'foo aa:foo bb: arr[0] cc: 2.3\ndd:bar.coo',
name: 'foo', name: 'foo',
args: 'aa:foo bb: arr[0] cc: 2.3\ndd:bar.coo' args: 'aa:foo bb: arr[0] cc: 2.3\ndd:bar.coo'
} } as TagToken
}) })
it('should call tag.render with scope', async function () { it('should call tag.render with scope', async function () {
await new Tag(token, [], liquid).render(scope) await new Tag(token, [], liquid).render(scope)
+2 -2
View File
@@ -2,13 +2,13 @@ import * as chai from 'chai'
import * as sinonChai from 'sinon-chai' import * as sinonChai from 'sinon-chai'
import * as sinon from 'sinon' import * as sinon from 'sinon'
import Scope from 'src/scope/scope' import Scope from 'src/scope/scope'
import Filter from 'src/template/filter' import Filter from 'src/template/filter/filter'
import Value from 'src/template/value' import Value from 'src/template/value'
chai.use(sinonChai) chai.use(sinonChai)
const expect = chai.expect const expect = chai.expect
const add = (l, r) => l + r const add = (l: number, r: number) => l + r
describe('Value', function () { describe('Value', function () {
beforeEach(() => Filter.clear()) beforeEach(() => Filter.clear())
+2 -44
View File
@@ -32,10 +32,10 @@ describe('error', function () {
expect(err.stack).to.contain(message.join('\n')) expect(err.stack).to.contain(message.join('\n'))
expect(err.name).to.equal('TokenizationError') expect(err.name).to.equal('TokenizationError')
}) })
it('should contain the whole template content in err.input', async function () { it('should contain the whole template content in err.token.input', async function () {
const html = 'bar\nfoo{% . a %}\nfoo' const html = 'bar\nfoo{% . a %}\nfoo'
const err = await expect(engine.parseAndRender(html)).be.rejected const err = await expect(engine.parseAndRender(html)).be.rejected
expect(err.input).to.equal(html) expect(err.token.input).to.equal(html)
}) })
it('should contain line number in err.token.line', async function () { it('should contain line number in err.token.line', async function () {
const err = await expect(engine.parseAndRender('1\n2\n{% . a %}\n4')).be.rejected const err = await expect(engine.parseAndRender('1\n2\n{% . a %}\n4')).be.rejected
@@ -49,25 +49,12 @@ describe('error', function () {
expect(err.stack).to.contain('at Liquid.parse') expect(err.stack).to.contain('at Liquid.parse')
}) })
describe('captureStackTrace compatibility', function () { describe('captureStackTrace compatibility', function () {
const captureStackTrace = Error.captureStackTrace
before(() => (Error.captureStackTrace = null))
after(() => (Error.captureStackTrace = captureStackTrace))
it('should be empty when captureStackTrace undefined', async function () { it('should be empty when captureStackTrace undefined', async function () {
const err = await expect(engine.parseAndRender('{% . a %}')).be.rejected const err = await expect(engine.parseAndRender('{% . a %}')).be.rejected
expect(err.stack).to.contain('illegal tag syntax') expect(err.stack).to.contain('illegal tag syntax')
expect(err.stack).to.not.contain('at Object.parse') expect(err.stack).to.not.contain('at Object.parse')
}) })
}) })
it('should contain file path in err.file', async function () {
const html = '<html>\n<head>\n\n{% . a %}\n\n'
mock({
'/foo.html': html
})
const err = await expect(engine.renderFile('/foo.html')).be.rejected
restore()
expect(err.name).to.equal('TokenizationError')
expect(err.file).to.equal(path.resolve('/foo.html'))
})
it('should throw error with line and pos if tag unmatched', async function () { it('should throw error with line and pos if tag unmatched', async function () {
const err = await expect(engine.parseAndRender('1\n2\nfoo{% assign a = 4 }\n4')).be.rejected const err = await expect(engine.parseAndRender('1\n2\nfoo{% assign a = 4 }\n4')).be.rejected
expect(err.name).to.equal('TokenizationError') expect(err.name).to.equal('TokenizationError')
@@ -187,12 +174,6 @@ describe('error', function () {
expect(err.stack).to.contain(message.join('\n')) expect(err.stack).to.contain(message.join('\n'))
expect(err.name).to.equal('RenderError') expect(err.name).to.equal('RenderError')
}) })
it('should contain the whole template content in err.input', async function () {
const html = 'bar\nfoo{%throwingTag%}\nfoo'
const err = await expect(engine.parseAndRender(html)).be.rejected
expect(err.input).to.equal(html)
expect(err.name).to.equal('RenderError')
})
it('should contain line number in err.token.line', async function () { it('should contain line number in err.token.line', async function () {
const src = '1\n2\n{{1|throwingFilter}}\n4' const src = '1\n2\n{{1|throwingFilter}}\n4'
const err = await expect(engine.parseAndRender(src)).be.rejected const err = await expect(engine.parseAndRender(src)).be.rejected
@@ -204,18 +185,6 @@ describe('error', function () {
expect(err.message).to.contain('intended render reject') expect(err.message).to.contain('intended render reject')
expect(err.stack).to.match(/at .*:\d+:\d+/) expect(err.stack).to.match(/at .*:\d+:\d+/)
}) })
it('should contain file path in err.file', async function () {
const html = '<html>\n<head>\n\n{% throwingTag %}\n\n'
mock({
'/foo.html': html
})
const err = await expect(engine.renderFile('/foo.html')).be.rejected
restore()
console.log(err, err.name)
expect(err.name).to.equal('RenderError')
expect(err.file).to.equal(path.resolve('/foo.html'))
})
}) })
describe('ParseError', function () { describe('ParseError', function () {
@@ -296,16 +265,5 @@ describe('error', function () {
expect(err.stack).to.contain('ParseError: tag -a not found') expect(err.stack).to.contain('ParseError: tag -a not found')
expect(err.stack).to.match(/at .*:\d+:\d+\)/) expect(err.stack).to.match(/at .*:\d+:\d+\)/)
}) })
it('should contain file path in err.file', async function () {
const html = '<html>\n<head>\n\n{% raw %}\n\n'
mock({
'/foo.html': html
})
const err = await expect(engine.renderFile('/foo.html')).be.rejected
restore()
expect(err.name).to.equal('ParseError')
expect(err.file).to.equal(path.resolve('/foo.html'))
})
}) })
}) })
+6 -48
View File
@@ -1,68 +1,26 @@
import * as chai from 'chai' import * as chai from 'chai'
import * as sinon from 'sinon' import * as sinon from 'sinon'
import * as sinonChai from 'sinon-chai' import * as sinonChai from 'sinon-chai'
import * as chaiAsPromised from 'chai-as-promised'
const expect = chai.expect const expect = chai.expect
chai.use(sinonChai) chai.use(sinonChai)
chai.use(chaiAsPromised)
const P = require('src/util/promise') const P = require('src/util/promise')
describe('util/promise', function () { describe('util/promise', function () {
describe('.anySeries()', function () {
it('should resolve in series', function () {
const spy1 = sinon.spy()
const spy2 = sinon.spy()
return P
.anySeries(
['first', 'second'],
(item, idx) => new Promise(function (resolve, reject) {
if (idx === 0) {
setTimeout(function () {
spy1()
reject(new Error('first cb'))
}, 10)
} else {
spy2()
resolve('foo')
}
}))
.then(() => expect(spy2).to.have.been.calledAfter(spy1))
})
it('should reject when all rejected', function () {
const p = P.anySeries(['first', 'second', 'third'],
item => Promise.reject(new Error(item)))
return expect(p).to.be.rejectedWith('third')
})
it('should resolve the value that first callback resolved', async () => {
const result = await P.anySeries(
['first', 'second'],
item => Promise.resolve(item)
)
return expect(result).to.equal('first')
})
it('should not call rest of callbacks once resolved', () => {
const spy = sinon.spy()
return P
.anySeries(['first', 'second'], (item, idx) => {
if (idx > 0) {
spy()
}
return Promise.resolve(item)
})
.then(() => expect(spy).to.not.have.been.called)
})
})
describe('.mapSeries()', async function () { describe('.mapSeries()', async function () {
it('should resolve when all resolved', async function () { it('should resolve when all resolved', async function () {
const result = await P.mapSeries( const result = await P.mapSeries(
['first', 'second', 'third'], ['first', 'second', 'third'],
item => Promise.resolve(item) (item: string) => Promise.resolve(item)
) )
return expect(result).to.deep.equal(['first', 'second', 'third']) return expect(result).to.deep.equal(['first', 'second', 'third'])
}) })
it('should reject with the error that first callback rejected', () => { it('should reject with the error that first callback rejected', () => {
const p = P.mapSeries(['first', 'second'], const p = P.mapSeries(['first', 'second'],
item => Promise.reject(item)) (item: string) => Promise.reject(item))
return expect(p).to.rejectedWith('first') return expect(p).to.rejectedWith('first')
}) })
it('should resolve in series', function () { it('should resolve in series', function () {
@@ -71,7 +29,7 @@ describe('util/promise', function () {
return P return P
.mapSeries( .mapSeries(
['first', 'second'], ['first', 'second'],
(item, idx) => new Promise(function (resolve) { (item: string, idx: number) => new Promise(function (resolve) {
if (idx === 0) { if (idx === 0) {
setTimeout(function () { setTimeout(function () {
spy1() spy1()
@@ -87,7 +45,7 @@ describe('util/promise', function () {
it('should not call rest of callbacks once rejected', () => { it('should not call rest of callbacks once rejected', () => {
const spy = sinon.spy() const spy = sinon.spy()
return P return P
.mapSeries(['first', 'second'], (item, idx) => { .mapSeries(['first', 'second'], (item: string, idx: number) => {
if (idx > 0) { if (idx > 0) {
spy() spy()
} }
+2 -2
View File
@@ -4,8 +4,8 @@ import t from 'src/util/strftime'
const expect = chai.expect const expect = chai.expect
describe('util/strftime', function () { describe('util/strftime', function () {
let now let now: Date
let then let then: Date
before(function () { before(function () {
mockUTC() mockUTC()
now = new Date('2016-01-04T13:15:23.000Z') now = new Date('2016-01-04T13:15:23.000Z')
+3 -47
View File
@@ -3,6 +3,7 @@ import * as sinonChai from 'sinon-chai'
import * as sinon from 'sinon' import * as sinon from 'sinon'
import { RenderError, RenderBreakError } from 'src/util/error' import { RenderError, RenderBreakError } from 'src/util/error'
import * as _ from 'src/util/underscore' import * as _ from 'src/util/underscore'
import ITemplate from 'src/template/itemplate'
const expect = chai.expect const expect = chai.expect
chai.use(sinonChai) chai.use(sinonChai)
@@ -17,7 +18,7 @@ describe('util/underscore', function () {
token: { token: {
input: 'xx' input: 'xx'
} }
} } as ITemplate
expect(_.isError(new RenderError(new Error(), tpl))).to.be.true expect(_.isError(new RenderError(new Error(), tpl))).to.be.true
}) })
it('should return true for RenderBreakError', function () { it('should return true for RenderBreakError', function () {
@@ -104,7 +105,7 @@ describe('util/underscore', function () {
}) })
describe('.isObject()', function () { describe('.isObject()', function () {
it('should return true for function', function () { it('should return true for function', function () {
expect(_.isObject(x => x)).to.be.true expect(_.isObject((x: any) => x)).to.be.true
}) })
it('should return true for plain object', function () { it('should return true for plain object', function () {
expect(_.isObject({})).to.be.true expect(_.isObject({})).to.be.true
@@ -116,49 +117,4 @@ describe('util/underscore', function () {
expect(_.isObject(2)).to.be.false expect(_.isObject(2)).to.be.false
}) })
}) })
describe('.assign()', function () {
it('should handle null dst', function () {
expect(_.assign(null, {
foo: 'bar'
})).to.deep.equal({
foo: 'bar'
})
})
it('should assign 2 objects', function () {
const src = {
foo: 'foo',
bar: 'bar'
}
const dst = {
foo: 'bar',
kaa: 'kaa'
}
expect(_.assign(dst, src)).to.deep.equal({
foo: 'foo',
bar: 'bar',
kaa: 'kaa'
})
})
it('should assign 3 objects', function () {
expect(_.assign({
foo: 'foo'
}, {
bar: 'bar'
}, {
car: 'car'
})).to.deep.equal({
foo: 'foo',
bar: 'bar',
car: 'car'
})
})
})
describe('.uniq()', function () {
it('should handle empty array', function () {
expect(_.uniq([])).to.deep.equal([])
})
it('should do uniq', function () {
expect(_.uniq([1, 'a', 'a', 1])).to.deep.equal([1, 'a'])
})
})
}) })
+2 -2
View File
@@ -6,8 +6,8 @@
"sourceMap": true, "sourceMap": true,
"declaration": true, "declaration": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"experimentalDecorators": true, "strict": true,
"emitDecoratorMetadata": true, "suppressImplicitAnyIndexErrors": true,
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"src/fs": ["src/fs/node"], "src/fs": ["src/fs/node"],