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
+12 -13
View File
@@ -1,24 +1,23 @@
import { last } from 'src/util/underscore'
export default {
'join': (v, arg) => v.join(arg === undefined ? ' ' : arg),
'last': v => last(v),
'first': v => v[0],
'map': (arr, arg) => arr.map(v => v[arg]),
'reverse': v => v.reverse(),
'sort': (v, arg) => v.sort(arg),
'size': v => v.length,
'slice': (v, begin, length) => {
'join': (v: any[], arg: string) => v.join(arg === undefined ? ' ' : arg),
'last': <T>(v: T[]): T => last(v),
'first': <T>(v: T[]): T => v[0],
'map': <T1, T2>(arr: {[key: string]: T1}[], arg: string): T1[] => arr.map(v => v[arg]),
'reverse': (v: any[]) => v.reverse(),
'sort': <T>(v: T[], arg: (lhs: T, rhs: T) => number) => v.sort(arg),
'size': (v: string | any[]) => v.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
return v.slice(begin, begin + length)
},
'uniq': function (arr) {
'uniq': function<T> (arr: T[]): T[] {
const u = {}
return (arr || []).filter(val => {
if (u.hasOwnProperty(val)) {
return false
}
u[val] = true
if (u.hasOwnProperty(String(val))) return false
u[String(val)] = true
return true
})
}
+2 -2
View File
@@ -2,7 +2,7 @@ import strftime from 'src/util/strftime'
import { isString } from 'src/util/underscore'
export default {
'date': (v, arg) => {
'date': (v: string | Date, arg: string) => {
let date = v
if (v === 'now') {
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())
}
+5 -5
View File
@@ -13,17 +13,17 @@ const unescapeMap = {
'&#39;': "'"
}
function escape (str) {
function escape (str: string) {
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])
}
export default {
'escape': escape,
'escape_once': str => escape(unescape(str)),
'newline_to_br': v => v.replace(/\n/g, '<br />'),
'strip_html': v => String(v).replace(/<script.*?<\/script>|<!--.*?-->|<style.*?<\/style>|<.*?>/g, '')
'escape_once': (str: string) => escape(unescape(str)),
'newline_to_br': (v: string) => v.replace(/\n/g, '<br />'),
'strip_html': (v: string) => v.replace(/<script.*?<\/script>|<!--.*?-->|<style.*?<\/style>|<.*?>/g, '')
}
+13 -13
View File
@@ -1,25 +1,25 @@
export default {
'abs': v => Math.abs(v),
'ceil': v => Math.ceil(v),
'divided_by': (v, arg) => v / arg,
'floor': v => Math.floor(v),
'minus': bindFixed((v, arg) => v - arg),
'modulo': bindFixed((v, arg) => v % arg),
'round': (v, arg) => {
const amp = Math.pow(10, arg || 0)
'abs': (v: number) => Math.abs(v),
'ceil': (v: number) => Math.ceil(v),
'divided_by': (v: number, arg: number) => v / arg,
'floor': (v: number) => Math.floor(v),
'minus': bindFixed((v: number, arg: number) => v - arg),
'modulo': bindFixed((v: number, arg: number) => v % arg),
'round': (v: number, arg: number = 0) => {
const amp = Math.pow(10, arg)
return Math.round(v * amp) / amp
},
'plus': bindFixed((v, arg) => Number(v) + Number(arg)),
'times': (v, arg) => v * arg
'plus': bindFixed((v: number, arg: number) => Number(v) + Number(arg)),
'times': (v: number, arg: number) => v * arg
}
function getFixed (v) {
function getFixed (v: number) {
const p = String(v).split('.')
return (p.length > 1) ? p[1].length : 0
}
function bindFixed (cb) {
return (l, r) => {
function bindFixed (cb: (v: number, arg: number) => number) {
return (l: number, r: number) => {
const f = Math.max(getFixed(l), getFixed(r))
return cb(l, r).toFixed(f)
}
+1 -1
View File
@@ -1,5 +1,5 @@
import { isTruthy } from 'src/render/syntax'
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 {
'append': (v, arg) => v + arg,
'prepend': (v, arg) => arg + v,
'capitalize': str => String(str).charAt(0).toUpperCase() + str.slice(1),
'concat': (v, arg) => Array.prototype.concat.call(v, arg),
'lstrip': v => String(v).replace(/^\s+/, ''),
'downcase': v => v.toLowerCase(),
'upcase': str => String(str).toUpperCase(),
'remove': (v, arg) => v.split(arg).join(''),
'remove_first': (v, l) => v.replace(l, ''),
'replace': (v, pattern, replacement) =>
'append': (v: string, arg: string) => v + arg,
'prepend': (v: string, arg: string) => arg + v,
'capitalize': (str: string) => String(str).charAt(0).toUpperCase() + str.slice(1),
'lstrip': (v: string) => String(v).replace(/^\s+/, ''),
'downcase': (v: string) => v.toLowerCase(),
'upcase': (str: string) => String(str).toUpperCase(),
'remove': (v: string, arg: string) => v.split(arg).join(''),
'remove_first': (v: string, l: string) => v.replace(l, ''),
'replace': (v: string, pattern: string, replacement: string) =>
String(v).split(pattern).join(replacement),
'replace_first': (v, arg1, arg2) => String(v).replace(arg1, arg2),
'rstrip': str => String(str).replace(/\s+$/, ''),
'split': (v, arg) => String(v).split(arg),
'strip': (v) => String(v).trim(),
'strip_newlines': v => String(v).replace(/\n/g, ''),
'truncate': (v, l, o) => {
'replace_first': (v: string, arg1: string, arg2: string) => String(v).replace(arg1, arg2),
'rstrip': (str: string) => String(str).replace(/\s+$/, ''),
'split': (v: string, arg: string) => String(v).split(arg),
'strip': (v: string) => String(v).trim(),
'strip_newlines': (v: string) => String(v).replace(/\n/g, ''),
'truncate': (v: string, l: number = 16, o: string = '...') => {
v = String(v)
o = (o === undefined) ? '...' : o
l = l || 16
if (v.length <= l) return v
return v.substr(0, l - o.length) + o
},
'truncatewords': (v, l, o) => {
if (o === undefined) o = '...'
'truncatewords': (v: string, l: number = v.length, o: string = '...') => {
const arr = v.split(' ')
let ret = arr.slice(0, l).join(' ')
if (arr.length > l) ret += o
return ret
}
}
} as {[key: string]: FilterImpl}
+3 -3
View File
@@ -1,4 +1,4 @@
export default {
'url_decode': x => x.split('+').map(decodeURIComponent).join(' '),
'url_encode': x => x.split(' ').map(encodeURIComponent).join('+')
}
'url_decode': (x: string) => x.split('+').map(decodeURIComponent).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 { identifier } from 'src/parser/lexical'
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*=([^]*)`)
export default {
parse: function (token) {
const match = token.args.match(re)
parse: function (token: TagToken) {
const match = token.args.match(re) as RegExpMatchArray
assert(match, `illegal token ${token.raw}`)
this.key = match[1]
this.value = match[2]
},
render: function (scope) {
render: function (scope: Scope) {
const ctx = new AssignScope()
ctx[this.key] = this.liquid.evalValue(this.value, scope)
scope.push(ctx)
return Promise.resolve('')
}
}
} as ITagImplOptions
+12 -7
View File
@@ -1,20 +1,25 @@
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 {
parse: function (token, remainTokens) {
parse: function (token: TagToken, remainTokens: Token[]) {
const match = /\w+/.exec(token.args)
this.block = match ? match[0] : ''
this.tpls = []
const stream = this.liquid.parser.parseStream(remainTokens)
this.tpls = [] as ITemplate[]
const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
.on('tag:endblock', () => stream.stop())
.on('template', tpl => this.tpls.push(tpl))
.on('template', (tpl: ITemplate) => this.tpls.push(tpl))
.on('end', () => {
throw new Error(`tag ${token.raw} not closed`)
})
stream.start()
},
render: async function (scope) {
render: async function (scope: Scope) {
const childDefined = scope.blocks[this.block]
const html = childDefined !== undefined
? childDefined
@@ -26,4 +31,4 @@ export default {
}
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 Token from 'src/parser/token'
import Scope from 'src/scope/scope'
import ITagImplOptions from 'src/template/tag/itag-impl-options';
const re = new RegExp(`(${identifier.source})`)
export default {
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`)
this.variable = match[1]
@@ -17,7 +18,7 @@ export default {
const stream = this.liquid.parser.parseStream(remainTokens)
stream.on('tag:endcapture', () => stream.stop())
.on('template', tpl => this.templates.push(tpl))
.on('template', (tpl) => this.templates.push(tpl))
.on('end', () => {
throw new Error(`tag ${tagToken.raw} not closed`)
})
@@ -29,4 +30,4 @@ export default {
ctx[this.variable] = html
scope.push(ctx)
}
}
} as ITagImplOptions
+13 -7
View File
@@ -1,14 +1,20 @@
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 {
parse: function (tagToken, remainTokens) {
parse: function (tagToken: TagToken, remainTokens: Token[]) {
this.cond = tagToken.args
this.cases = []
this.elseTemplates = []
let p = []
const stream = this.liquid.parser.parseStream(remainTokens)
.on('tag:when', token => {
let p: ITemplate[] = []
const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
.on('tag:when', (token: TagToken) => {
this.cases.push({
val: token.args,
templates: p = []
@@ -16,7 +22,7 @@ export default {
})
.on('tag:else', () => (p = this.elseTemplates))
.on('tag:endcase', () => stream.stop())
.on('template', tpl => p.push(tpl))
.on('template', (tpl: ITemplate) => p.push(tpl))
.on('end', () => {
throw new Error(`tag ${tagToken.raw} not closed`)
})
@@ -24,7 +30,7 @@ export default {
stream.start()
},
render: function (scope) {
render: function (scope: Scope) {
for (let i = 0; i < this.cases.length; i++) {
const branch = this.cases[i]
const val = evalExp(branch.val, scope)
@@ -35,4 +41,4 @@ export default {
}
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 {
parse: function (tagToken, remainTokens) {
parse: function (tagToken: TagToken, remainTokens: Token[]) {
const stream = this.liquid.parser.parseStream(remainTokens)
stream
.on('token', token => {
.on('token', (token: TagToken) => {
if (token.name === 'endcomment') stream.stop()
})
.on('end', () => {
@@ -10,4 +14,4 @@ export default {
})
stream.start()
}
}
} as ITagImplOptions
+9 -7
View File
@@ -1,13 +1,16 @@
import assert from 'src/util/assert'
import { value as rValue } from 'src/parser/lexical'
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 candidatesRE = new RegExp(rValue.source, 'g')
export default {
parse: function (tagToken) {
let match = groupRE.exec(tagToken.args)
export default <ITagImplOptions>{
parse: function (tagToken: TagToken) {
let match: RegExpExecArray | null = groupRE.exec(tagToken.args) as RegExpExecArray
assert(match, `illegal tag: ${tagToken.raw}`)
this.group = match[1] || ''
@@ -21,11 +24,10 @@ export default {
assert(this.candidates.length, `empty candidates: ${tagToken.raw}`)
},
render: function (scope) {
render: function (scope: Scope) {
const group = evalValue(this.group, scope)
const fingerprint = `cycle:${group}:` + this.candidates.join(',')
const groups = scope.opts.groups = scope.opts.groups || {}
const groups = scope.groups
let idx = groups[fingerprint]
if (idx === undefined) {
@@ -38,4 +40,4 @@ export default {
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 TagToken from 'src/parser/tag-token'
import Scope from 'src/scope/scope'
import ITagImplOptions from 'src/template/tag/itag-impl-options';
export default {
parse: function (token: TagToken) {
const match = token.args.match(identifier)
const match = token.args.match(identifier) as RegExpMatchArray
assert(match, `illegal identifier ${token.args}`)
this.variable = match[0]
},
@@ -26,4 +27,4 @@ export default {
}
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 assert from 'src/util/assert'
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+` +
`(${value.source})` +
@@ -11,83 +17,83 @@ const re = new RegExp(`^(${identifier.source})\\s+in\\s+` +
`(?:\\s+(reversed))?` +
`(?:\\s+${hash.source})*$`)
function parse (tagToken, remainTokens) {
const match = re.exec(tagToken.args)
assert(match, `illegal tag: ${tagToken.raw}`)
this.variable = match[1]
this.collection = match[2]
this.reversed = !!match[3]
this.templates = []
this.elseTemplates = []
let p
const stream = this.liquid.parser.parseStream(remainTokens)
.on('start', () => (p = this.templates))
.on('tag:else', () => (p = this.elseTemplates))
.on('tag:endfor', () => stream.stop())
.on('template', tpl => p.push(tpl))
.on('end', () => {
throw new Error(`tag ${tagToken.raw} not closed`)
export default <ITagImplOptions>{
type: 'block',
parse: function (tagToken: TagToken, remainTokens: Token[]) {
const match = re.exec(tagToken.args) as RegExpExecArray
assert(match, `illegal tag: ${tagToken.raw}`)
this.variable = match[1]
this.collection = match[2]
this.reversed = !!match[3]
this.templates = []
this.elseTemplates = []
let p
const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
.on('start', () => (p = this.templates))
.on('tag:else', () => (p = this.elseTemplates))
.on('tag:endfor', () => stream.stop())
.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()
}
async function render (scope, hash) {
let collection = evalExp(this.collection, scope)
if (!isArray(collection)) {
if (isString(collection) && collection.length > 0) {
collection = [collection]
} else if (isObject(collection)) {
collection = Object.keys(collection).map((key) => [key, collection[key]])
}
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.name === 'RenderBreakError') {
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 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 {
parse: function (tagToken, remainTokens) {
parse: function (tagToken: TagToken, remainTokens: Token[]) {
this.branches = []
this.elseTemplates = []
let p
const stream = this.liquid.parser.parseStream(remainTokens)
const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
.on('start', () => this.branches.push({
cond: tagToken.args,
templates: (p = [])
}))
.on('tag:elsif', token => {
.on('tag:elsif', (token: TagToken) => {
this.branches.push({
cond: token.args,
templates: p = []
@@ -19,7 +25,7 @@ export default {
})
.on('tag:else', () => (p = this.elseTemplates))
.on('tag:endif', () => stream.stop())
.on('template', tpl => p.push(tpl))
.on('template', (tpl: ITemplate) => p.push(tpl))
.on('end', () => {
throw new Error(`tag ${tagToken.raw} not closed`)
})
@@ -27,7 +33,7 @@ export default {
stream.start()
},
render: function (scope) {
render: function (scope: Scope) {
for (const branch of this.branches) {
const cond = evalExp(branch.cond, scope)
if (isTruthy(cond)) {
@@ -36,4 +42,4 @@ export default {
}
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 { evalValue } from 'src/render/syntax'
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 withRE = new RegExp(`with\\s+(${value.source})`)
export default {
parse: function (token) {
export default <ITagImplOptions>{
parse: function (token: TagToken) {
let match = staticFileRE.exec(token.args)
if (match) {
this.staticValue = match[0]
@@ -23,7 +27,7 @@ export default {
this.with = match[1]
}
},
render: async function (scope, hash) {
render: async function (scope: Scope, hash: Hash) {
let filepath
if (scope.opts.dynamicPartials) {
if (quotedLine.exec(this.value)) {
+3 -2
View File
@@ -1,12 +1,13 @@
import assert from 'src/util/assert'
import { identifier } from 'src/parser/lexical'
import { CaptureScope, AssignScope, IncrementScope } from 'src/scope/scopes'
import ITagImplOptions from 'src/template/tag/itag-impl-options';
export default {
parse: function (token) {
const match = token.args.match(identifier)
assert(match, `illegal identifier ${token.args}`)
this.variable = match[0]
this.variable = match![0]
},
render: function (scope) {
let context = scope.findContextFor(
@@ -26,4 +27,4 @@ export default {
context[this.variable]++
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 { evalValue } from 'src/render/syntax'
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+/
export default {
parse: function (token, remainTokens) {
parse: function (token: TagToken, remainTokens: Token[]) {
let match = staticFileRE.exec(token.args)
if (match) {
this.staticLayout = match[0]
@@ -19,7 +24,7 @@ export default {
this.tpls = this.liquid.parser.parse(remainTokens)
},
render: async function (scope, hash) {
render: async function (scope: Scope, hash: Hash) {
const layout = scope.opts.dynamicPartials
? evalValue(this.layout, scope)
: this.staticLayout
@@ -38,4 +43,4 @@ export default {
scope.pop(hash)
return partial
}
}
} as ITagImplOptions
+8 -4
View File
@@ -1,10 +1,14 @@
export default {
parse: function (tagToken, remainTokens) {
import TagToken from "src/parser/tag-token";
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 = []
const stream = this.liquid.parser.parseStream(remainTokens)
stream
.on('token', token => {
.on('token', (token: TagToken) => {
if (token.name === 'endraw') stream.stop()
else this.tokens.push(token)
})
@@ -14,6 +18,6 @@ export default {
stream.start()
},
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 { evalExp } from 'src/render/syntax'
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+` +
`(${value.source})` +
`(?:\\s+${hash.source})*$`)
export default {
parse: function (tagToken, remainTokens) {
const match = re.exec(tagToken.args)
parse: function (tagToken: TagToken, remainTokens: Token[]) {
const match = re.exec(tagToken.args) as RegExpExecArray
assert(match, `illegal tag: ${tagToken.raw}`)
this.variable = match[1]
@@ -17,10 +24,10 @@ export default {
this.templates = []
let p
const stream = this.liquid.parser.parseStream(remainTokens)
const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
.on('start', () => (p = this.templates))
.on('tag:endtablerow', () => stream.stop())
.on('template', tpl => p.push(tpl))
.on('template', (tpl: ITemplate) => p.push(tpl))
.on('end', () => {
throw new Error(`tag ${tagToken.raw} not closed`)
})
@@ -28,20 +35,20 @@ export default {
stream.start()
},
render: async function (scope, hash) {
render: async function (scope: Scope, hash: Hash) {
let collection = evalExp(this.collection, scope) || []
const offset = hash.offset || 0
const limit = (hash.limit === undefined) ? collection.length : hash.limit
collection = collection.slice(offset, offset + limit)
const cols = hash.cols || collection.length
const contexts = collection.map(item => {
const contexts = collection.map((item: any) => {
const ctx = {}
ctx[this.variable] = item
return ctx
})
let row
let row: number = 0
let html = ''
await mapSeries(contexts, async (context, idx) => {
row = Math.floor(idx / cols) + 1
@@ -65,4 +72,4 @@ export default {
}
return html
}
}
} as ITagImplOptions
+10 -4
View File
@@ -1,11 +1,17 @@
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 {
parse: function (tagToken, remainTokens) {
parse: function (tagToken: TagToken, remainTokens: Token[]) {
this.templates = []
this.elseTemplates = []
let p
const stream = this.liquid.parser.parseStream(remainTokens)
const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
.on('start', () => {
p = this.templates
this.cond = tagToken.args
@@ -20,10 +26,10 @@ export default {
stream.start()
},
render: function (scope) {
render: function (scope: Scope) {
const cond = evalExp(this.cond, scope)
return isFalsy(cond)
? this.liquid.renderer.renderTemplates(this.templates, scope)
: this.liquid.renderer.renderTemplates(this.elseTemplates, scope)
}
}
} as ITagImplOptions
+2 -2
View File
@@ -1,7 +1,7 @@
import { last } from '../util/underscore'
import IFS from './ifs'
function domResolve (root, path) {
function domResolve (root: string, path: string) {
const base = document.createElement('base')
base.href = root
@@ -16,7 +16,7 @@ function domResolve (root, path) {
return resolved
}
function resolve (root, filepath, ext) {
function resolve (root: string, filepath: string, ext: string) {
if (root.length && last(root) !== '/') root += '/'
const url = domResolve(root, filepath)
return url.replace(/^(\w+:\/\/[^/]+)(\/[^?]+)/, (str, origin, path) => {
+1 -1
View File
@@ -1,5 +1,5 @@
export default interface IFS {
exists: (filepath?: string) => Promise<boolean>
exists: (filepath: string) => Promise<boolean>
readFile: (filepath:string) => Promise<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 IFS from './ifs'
const statAsync = _.promisify(stat) as (filepath: string) => Promise<object>
const readFileAsync = _.promisify(readFile) as (filepath: string, encoding: string) => Promise<string>
const statAsync = _.promisify(stat)
const readFileAsync = _.promisify<string, string, string>(readFile)
const fs: IFS = {
exists: filepath => {
exists: (filepath: string) => {
return statAsync(filepath).then(() => true).catch(() => false)
},
readFile: filepath => {
+7 -3
View File
@@ -48,6 +48,10 @@ export interface NormalizedFullOptions extends NormalizedOptions {
trim_tag_left: boolean
trim_output_right: boolean
trim_output_left: boolean
tag_delimiter_left: string,
tag_delimiter_right: string,
output_delimiter_left: string,
output_delimiter_right: string,
greedy: boolean
}
@@ -69,7 +73,7 @@ const defaultOptions: NormalizedFullOptions = {
strict_variables: false
}
export function normalize (options: LiquidOptions): NormalizedOptions {
export function normalize (options?: LiquidOptions): NormalizedOptions {
options = options || {}
if (options.hasOwnProperty('root')) {
options.root = normalizeStringArray(options.root)
@@ -77,11 +81,11 @@ export function normalize (options: LiquidOptions): NormalizedOptions {
return options as NormalizedOptions
}
export function applyDefault (options: NormalizedOptions): NormalizedFullOptions {
export function applyDefault (options?: NormalizedOptions): NormalizedFullOptions {
return { ...defaultOptions, ...options }
}
function normalizeStringArray (value: string | string[]): string[] {
function normalizeStringArray (value: any): string[] {
if (_.isArray(value)) return value as string[]
if (_.isString(value)) return [value as string]
return []
+10 -12
View File
@@ -6,7 +6,7 @@ import ITemplate from './template/itemplate'
import Tokenizer from './parser/tokenizer'
import Render from './render/render'
import Tag from './template/tag/tag'
import Filter from './template/filter'
import Filter from './template/filter/filter'
import Parser from './parser/parser'
import ITagImplOptions from './template/tag/itag-impl-options'
import Value from './template/value'
@@ -14,19 +14,17 @@ import { isTruthy, isFalsy, evalExp, evalValue } from './render/syntax'
import builtinTags from './builtin/tags'
import builtinFilters from './builtin/filters'
import { LiquidOptions, NormalizedFullOptions, applyDefault, normalize } from './liquid-options'
import FilterImpl from './template/filter/filter-impl';
export default class Liquid {
public options: NormalizedFullOptions
private cache: object
private parser: Parser
private renderer: Render
public renderer: Render
public parser: Parser
private cache: object = {}
private tokenizer: Tokenizer
constructor (opts: LiquidOptions = {}) {
this.options = applyDefault(normalize(opts))
if (this.options.cache) {
this.cache = {}
}
this.parser = new Parser(this)
this.renderer = new Render()
this.tokenizer = new Tokenizer(this.options)
@@ -47,7 +45,7 @@ export default class Liquid {
const tpl = await this.parse(html)
return this.render(tpl, ctx, opts)
}
async getTemplate (file, opts?: LiquidOptions) {
async getTemplate (file: string, opts?: LiquidOptions) {
const options = normalize(opts)
const roots = options.root ? [...options.root, ...this.options.root] : this.options.root
const paths = roots.map(root => fs.resolve(root, file, this.options.extname))
@@ -66,7 +64,7 @@ export default class Liquid {
err.code = 'ENOENT'
throw err
}
async renderFile (file, ctx?: object, opts?: LiquidOptions) {
async renderFile (file: string, ctx?: object, opts?: LiquidOptions) {
const options = normalize(opts)
const templates = await this.getTemplate(file, options)
return this.render(templates, ctx, opts)
@@ -74,18 +72,18 @@ export default class Liquid {
evalValue (str: string, scope: Scope) {
return new Value(str, this.options.strict_filters).value(scope)
}
registerFilter (name, filter) {
registerFilter (name: string, filter: FilterImpl) {
return Filter.register(name, filter)
}
registerTag (name: string, tag: ITagImplOptions) {
return Tag.register(name, tag)
}
plugin (plugin) {
plugin (plugin: (this: Liquid, L: typeof Liquid) => void) {
return plugin.call(this, Liquid)
}
express () {
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 }
self.renderFile(filePath, ctx, opts).then(html => cb(null, html), cb)
}
+4 -3
View File
@@ -1,12 +1,13 @@
import Token from './token'
import { last } from 'src/util/underscore'
export default class DelimitedToken extends Token {
trimLeft: boolean
trimRight: boolean
constructor (raw, value, pos, input, file, line) {
super(raw, pos, input, file, line)
constructor (raw: string, value: string, input: string, line: number, pos: number, file?: string) {
super(raw, input, line, pos, file)
this.trimLeft = value[0] === '-'
this.trimRight = value[value.length - 1] === '-'
this.trimRight = last(value) === '-'
this.value = value
.slice(
this.trimLeft ? 1 : 0,
+2 -2
View File
@@ -1,8 +1,8 @@
import Token from './token'
export default class HTMLToken extends Token {
constructor (str, begin, input, file, line) {
super(str, begin, input, file, line)
constructor (str: string, input: string, line: number, col: number, file?: string) {
super(str, input, line, col, file)
this.type = 'html'
this.value = str
}
+6 -6
View File
@@ -49,27 +49,27 @@ export const operators = [
/==|!=|<=|>=|<|>|\s+contains\s+/
]
export function isInteger (str) {
export function isInteger (str: string) {
return integerLine.test(str)
}
export function isLiteral (str) {
export function isLiteral (str: string) {
return literalLine.test(str)
}
export function isRange (str) {
export function isRange (str: string) {
return rangeLine.test(str)
}
export function isVariable (str) {
export function isVariable (str: string) {
return variableLine.test(str)
}
export function matchValue (str) {
export function matchValue (str: string) {
return value.exec(str)
}
export function parseLiteral (str) {
export function parseLiteral (str: string) {
let res = str.match(numberLine)
if (res) {
return Number(str)
+2 -2
View File
@@ -1,8 +1,8 @@
import DelimitedToken from './delimited-token'
export default class OutputToken extends DelimitedToken {
constructor (raw, value, pos, input, file, line) {
super(raw, value, pos, input, file, line)
constructor (raw: string, value: string, input: string, line: number, pos: number, file?: string) {
super(raw, value, input, line, pos, file)
this.type = 'output'
}
}
+11 -11
View File
@@ -1,37 +1,37 @@
import Token from 'src/parser/token'
import ITemplate from 'src/template/itemplate'
import TagToken from './tag-token';
type parseToken = (token: Token, remainTokens: Array<Token>) => ITemplate
type eventHandler = ((arg?: Token | ITemplate) => void)
type ParseToken = ((token: Token, remainTokens: Array<Token>) => ITemplate)
export default class ParseStream {
private tokens: Array<Token>
private handlers: {[key: string]: eventHandler} = {}
private stopRequested: boolean
private parseToken: parseToken
private handlers: {[key: string]: (arg: any) => void} = {}
private stopRequested: boolean = false
private parseToken: ParseToken
constructor (tokens: Array<Token>, parseToken: parseToken) {
constructor (tokens: Array<Token>, parseToken: ParseToken) {
this.tokens = tokens
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
return this
}
trigger (event: string, arg?: Token | ITemplate) {
trigger <T extends Token | ITemplate>(event: string, arg?: T) {
const h = this.handlers[event]
if (typeof h === 'function') {
h(arg)
return true
}
return false
}
start () {
this.trigger('start')
let token
let token: Token | undefined
while (!this.stopRequested && (token = this.tokens.shift())) {
if (this.trigger('token', token)) continue
if (token.type === 'tag' &&
this.trigger(`tag:${token.name}`, token)) {
if (token.type === 'tag' && this.trigger(`tag:${(<TagToken>token).name}`, token)) {
continue
}
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 Output from 'src/template/output'
import HTML from 'src/template/html'
import ITemplate from 'src/template/itemplate'
export default class Parser {
liquid: Liquid
@@ -16,7 +17,7 @@ export default class Parser {
}
parse (tokens: Array<Token>) {
let token
const templates = []
const templates: ITemplate[] = []
while ((token = tokens.shift())) {
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 {
name: string
args: string
constructor (raw, value, pos, input, file, line) {
super(raw, value, pos, input, file, line)
constructor (raw: string, value: string, input: string, line: number, pos: number, file?: string) {
super(raw, value, input, line, pos, file)
this.type = 'tag'
const match = this.value.match(lexical.tagLine)
if (!match) {
+4 -3
View File
@@ -1,15 +1,16 @@
export default class Token {
type: string
type: string = 'notset'
line: number
col: number
raw: string
input: string
file: string
file?: string
value: string
constructor (raw, col, input, file, line) {
constructor (raw: string, input: string, line: number, col: number, file?: string) {
this.col = col
this.line = line
this.raw = raw
this.value = raw
this.input = input
this.file = file
}
+8 -8
View File
@@ -14,7 +14,7 @@ export default class Tokenizer {
this.options = applyDefault(options)
}
tokenize (input: string, file?: string) {
const tokens = []
const tokens: Token[] = []
const tagL = this.options.tag_delimiter_left
const tagR = this.options.tag_delimiter_right
const outputL = this.options.output_delimiter_left
@@ -34,7 +34,7 @@ export default class Tokenizer {
}
if (state === ParseState.HTML) {
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
line = curLine
col = p - lineBegin + 1
@@ -42,7 +42,7 @@ export default class Tokenizer {
state = ParseState.OUTPUT
continue
} 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
line = curLine
col = p - lineBegin + 1
@@ -52,7 +52,7 @@ export default class Tokenizer {
}
} else if (state === ParseState.OUTPUT && input.substr(p, outputR.length) === 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
buffer = ''
line = curLine
@@ -61,7 +61,7 @@ export default class Tokenizer {
continue
} else if (input.substr(p, tagR.length) === 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
buffer = ''
line = curLine
@@ -75,11 +75,11 @@ export default class Tokenizer {
const t = state === ParseState.OUTPUT ? 'output' : 'tag'
const str = buffer.length > 16 ? buffer.slice(0, 13) + '...' : buffer
throw new TokenizationError(
new Error(`${t} "${str}" not closed`),
new Token(buffer, col, input, file, line)
`${t} "${str}" not closed`,
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)
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 (token.type === 'tag') return token.trimLeft || options.trim_tag_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 (token.type === 'tag') return token.trimRight || options.trim_tag_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 assert from 'src/util/assert'
import Scope from 'src/scope/scope';
import ITemplate from 'src/template/itemplate';
export default class Render {
async renderTemplates (templates, scope) {
async renderTemplates (templates: ITemplate[], scope: Scope) {
assert(scope, 'unable to evalTemplates: scope undefined')
let html = ''
@@ -10,11 +12,11 @@ export default class Render {
try {
html += await tpl.render(scope)
} catch (e) {
if (e instanceof RenderBreakError) {
if (e.name === 'RenderBreakError') {
e.resolvedHTML = html
throw e
}
throw e instanceof RenderError ? e : new RenderError(e, tpl)
throw e.name === 'RenderError' ? e : new RenderError(e, tpl)
}
}
return html
+16 -18
View File
@@ -1,23 +1,25 @@
import * as lexical from '../parser/lexical'
import assert from '../util/assert'
import Scope from 'src/scope/scope'
import { range } from 'src/util/underscore'
const operators = {
'==': (l, r) => l === r,
'!=': (l, r) => l !== r,
'>': (l, r) => l !== null && r !== null && l > r,
'<': (l, r) => l !== null && r !== null && l < r,
'>=': (l, r) => l !== null && r !== null && l >= r,
'<=': (l, r) => l !== null && r !== null && l <= r,
'contains': (l, r) => {
'==': (l: any, r: any) => l === r,
'!=': (l: any, r: any) => l !== r,
'>': (l: any, r: any) => l !== null && r !== null && l > r,
'<': (l: any, r: any) => l !== null && r !== null && l < r,
'>=': (l: any, r: any) => l !== null && r !== null && l >= r,
'<=': (l: any, r: any) => l !== null && r !== null && l <= r,
'contains': (l: any, r: any) => {
if (!l) return false
if (typeof l.indexOf !== 'function') return false
return l.indexOf(r) > -1
},
'and': (l, r) => isTruthy(l) && isTruthy(r),
'or': (l, r) => isTruthy(l) || isTruthy(r)
'and': (l: any, r: any) => 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')
const operatorREs = lexical.operators
let match
@@ -35,17 +37,13 @@ export function evalExp (exp, scope) {
if ((match = exp.match(lexical.rangeLine))) {
const low = evalValue(match[1], scope)
const high = evalValue(match[2], scope)
const range = []
for (let j = low; j <= high; j++) {
range.push(j)
}
return range
return range(low, high + 1)
}
return evalValue(exp, scope)
}
export function evalValue (str, scope) {
export function evalValue (str: string, scope: Scope) {
str = str && str.trim()
if (!str) return undefined
@@ -58,10 +56,10 @@ export function evalValue (str, scope) {
throw new TypeError(`cannot eval '${str}' as value`)
}
export function isTruthy (val) {
export function isTruthy (val: any): boolean {
return !isFalsy(val)
}
export function isFalsy (val) {
export function isFalsy (val: any): boolean {
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 { __assign } from 'tslib'
import * as lexical from '../parser/lexical'
import assert from '../util/assert'
import { NormalizedFullOptions, applyDefault } from '../liquid-options'
import BlockMode from './block-mode'
import IContext from './icontext';
export default class Scope {
opts: NormalizedFullOptions
contexts: Array<object>
contexts: Array<IContext>
blocks: object = {}
groups: {[key: string]: number} = {}
blockMode: BlockMode = BlockMode.OUTPUT
constructor (ctx: object = {}, opts?: NormalizedFullOptions) {
this.opts = applyDefault(opts)
this.contexts = [ctx || {}]
}
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 {
const paths = this.propertyAccessSeq(path)
@@ -36,6 +39,7 @@ export default class Scope {
scope[key] = {}
}
scope = scope[key]
return false
})
}
unshift (ctx: object) {
@@ -44,7 +48,7 @@ export default class Scope {
push (ctx: object) {
return this.contexts.push(ctx)
}
pop (ctx?: object): object {
pop (ctx?: object): object | undefined {
if (!arguments.length) {
return this.contexts.pop()
}
@@ -64,7 +68,7 @@ export default class Scope {
}
return null
}
readProperty (obj, key) {
readProperty (obj: IContext, key: string) {
let val
if (_.isNil(obj)) {
val = undefined
@@ -72,7 +76,7 @@ export default class Scope {
obj = toLiquid(obj)
val = key === 'size' ? readSize(obj) : obj[key]
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) {
@@ -89,9 +93,9 @@ export default class Scope {
* accessSeq("foo['b]r']") // ['foo', 'b]r']
* accessSeq("foo[bar.coo]") // ['foo', 'bar'], for bar.coo == 'bar'
*/
propertyAccessSeq (str) {
propertyAccessSeq (str: string) {
str = String(str)
const seq = []
const seq: string[] = []
let name = ''
let j
let i = 0
@@ -141,7 +145,7 @@ export default class Scope {
}
}
function toLiquid (obj) {
function toLiquid (obj: IContext) {
if (_.isFunction(obj.to_liquid)) {
return obj.to_liquid()
}
@@ -151,13 +155,13 @@ function toLiquid (obj) {
return obj
}
function readSize (obj) {
function readSize (obj: IContext) {
if (!_.isNil(obj.size)) return obj.size
if (_.isArray(obj) || _.isString(obj)) return obj.length
return obj.size
}
function matchRightBracket (str, begin) {
function matchRightBracket (str: string, begin: number) {
let stack = 1 // count of '[' - count of ']'
for (let i = begin; i < str.length; 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 { evalValue } from 'src/render/syntax'
import Scope from 'src/scope/scope'
type impl = (value: any, ...args: any[]) => any
import FilterImpl from './filter-impl'
const valueRE = new RegExp(`${lexical.value.source}`, 'g')
export default class Filter {
name: string
impl: impl
impl: FilterImpl
args: string[]
private static impls: {[key: string]: impl} = {}
private static impls: {[key: string]: FilterImpl} = {}
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)
const name = match[1]
@@ -27,7 +26,7 @@ export default class Filter {
this.args = this.parseArgs(argList)
}
parseArgs (argList: string): string[] {
let match; const args = []
let match; const args: string[] = []
while ((match = valueRE.exec(argList.trim()))) {
const v = match[0]
const re = new RegExp(`${v}\\s*:`, 'g')
@@ -39,10 +38,9 @@ export default class Filter {
}
render (value: any, scope: Scope): any {
const args = this.args.map(arg => evalValue(arg, scope))
args.unshift(value)
return this.impl.apply(null, args)
return this.impl.apply(null, [value, ...args])
}
static register (name, filter) {
static register (name: string, filter: FilterImpl) {
Filter.impls[name] = filter
}
static clear () {
+3 -3
View File
@@ -1,10 +1,10 @@
import Template from 'src/template/template'
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
constructor (token: Token) {
constructor (token: HTMLToken) {
super(token)
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 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
constructor (token: OutputToken, strictFilters?: boolean) {
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 { evalValue } from 'src/render/syntax'
import Scope from 'src/scope/scope';
/**
* Key-Value Pairs Representing Tag Arguments
@@ -9,7 +10,7 @@ import { evalValue } from 'src/render/syntax'
*/
export default class Hash {
[key: string]: any
constructor (markup, scope) {
constructor (markup: string, scope: Scope) {
let match
hashCapture.lastIndex = 0
while ((match = hashCapture.exec(markup))) {
+2 -1
View File
@@ -2,5 +2,6 @@ import Liquid from 'src/liquid'
import ITagImplOptions from './itag-impl-options'
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 Token from 'src/parser/token'
export default class Tag extends Template implements ITemplate {
export default class Tag extends Template<TagToken> implements ITemplate {
name: string
token: TagToken
private impl: ITagImpl
static impls: { [key: string]: ITagImplOptions } = {}
+3 -5
View File
@@ -1,8 +1,6 @@
import Token from 'src/parser/token'
export default class Template {
token: Token;
constructor (token) {
export default abstract class Template<T> {
token: T;
constructor (token: T) {
this.token = token
}
}
+5 -9
View File
@@ -1,26 +1,22 @@
import { evalExp } from 'src/render/syntax'
import * as lexical from 'src/parser/lexical'
import assert from 'src/util/assert'
import Filter from './filter'
import Filter from './filter/filter'
import Scope from 'src/scope/scope'
export default class {
initial: any
filters: Array<any>
filters: Array<Filter> = []
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}`)
const initial = match[0]
this.initial = match[0]
str = str.substr(match.index + match[0].length)
const filters = []
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) {
return this.filters.reduce(
+35 -61
View File
@@ -1,98 +1,77 @@
import * as _ from './underscore'
import { __extends } from 'tslib'
import Token from 'src/parser/token'
import ITemplate from 'src/template/itemplate'
function captureStack () {
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor)
}
}
abstract class LiquidError {
name: string
message: string
stack: string
private file: string
private input: string
abstract class LiquidError extends Error {
private token: Token
private originalError: Error
constructor (err, token) {
this.input = token.input
this.file = token.file
constructor (err: Error, token: Token) {
super(err.message)
this.originalError = err
this.token = token
}
captureStackTrace (obj) {
this.name = obj.constructor.name
captureStack.call(obj)
protected update() {
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.stack = this.message + '\n' + context +
'\n' + (this.stack || this.message) +
(err.stack ? '\nFrom ' + err.stack : '')
'\n' + this.stack + '\nFrom ' + err.stack
}
}
export class TokenizationError extends LiquidError {
constructor (message, token) {
super({ message }, token)
super.captureStackTrace(this)
constructor (message: string, token: Token) {
super(new Error(message), token)
this.name = 'TokenizationError'
super.update()
}
}
TokenizationError.prototype = _.create(Error.prototype) as any
TokenizationError.prototype.constructor = TokenizationError
export class ParseError extends LiquidError {
constructor (err, token) {
constructor (err: Error, token: Token) {
super(err, token)
this.name = 'ParseError'
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 {
constructor (err, tpl) {
constructor (err: Error, tpl: ITemplate) {
super(err, tpl.token)
this.name = 'RenderError'
this.message = err.message
super.captureStackTrace(this)
super.update()
}
}
RenderError.prototype = _.create(Error.prototype) as any
RenderError.prototype.constructor = RenderError
export class RenderBreakError {
message: string
resolvedHTML: string
constructor (message) {
captureStack.call(this)
export class RenderBreakError extends Error {
resolvedHTML: string = ''
constructor (message: string) {
super(message)
this.name = 'RenderBreakError'
this.message = message + ''
}
}
RenderBreakError.prototype = _.create(Error.prototype) as any
RenderBreakError.prototype.constructor = RenderBreakError
export class AssertionError {
message: string
constructor (message) {
captureStack.call(this)
export class AssertionError extends Error {
constructor (message: string) {
super(message)
this.name = 'AssertionError'
this.message = message + ''
}
}
AssertionError.prototype = _.create(Error.prototype) as any
AssertionError.prototype.constructor = AssertionError
function mkContext (input, targetLine) {
const lines = input.split('\n')
const begin = Math.max(targetLine - 2, 1)
const end = Math.min(targetLine + 3, lines.length)
function mkContext (token: Token) {
const lines = token.input.split('\n')
const begin = Math.max(token.line - 2, 1)
const end = Math.min(token.line + 3, lines.length)
const context = _
.range(begin, end + 1)
.map(lineNumber => {
const indicator = (lineNumber === targetLine) ? '>> ' : ' '
const indicator = (lineNumber === token.line) ? '>> ' : ' '
const num = _.padStart(String(lineNumber), String(end).length)
const text = lines[lineNumber - 1]
return `${indicator}${num}| ${text}`
@@ -102,13 +81,8 @@ function mkContext (input, targetLine) {
return context
}
function mkMessage (msg, token) {
msg = msg || ''
if (token.file) {
msg += ', file:' + token.file
}
if (token.line) {
msg += `, line:${token.line}, col:${token.col}`
}
function mkMessage (msg: string, token: Token) {
if (token.file) msg += `, file:${token.file}`
msg += `, line:${token.line}, col:${token.col}`
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.
* @param {Array} iterable the array to iterate with.
* @param {Array} iteratee returns a new promise.
* The iteratee is invoked with three arguments: (value, index, iterable).
*/
export function mapSeries (iterable, iteratee) {
let ret: Promise<any> = Promise.resolve('init')
const result = []
export function mapSeries<T1, T2> (
iterable: T1[],
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) {
ret = ret
.then(() => iteratee(item, idx, iterable))
+39 -39
View File
@@ -16,18 +16,18 @@ const suffixes = {
'default': 'th'
}
function abbr (str) {
function abbr (str: string) {
return str.slice(0, 3)
}
// prototype extensions
const _date = {
daysInMonth: function (d) {
daysInMonth: function (d: Date) {
const feb = _date.isLeapYear(d) ? 29 : 28
return [31, feb, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
},
getDayOfYear: function (d) {
getDayOfYear: function (d: Date) {
let num = 0
for (let i = 0; i < d.getMonth(); ++i) {
num += _date.daysInMonth(d)[i]
@@ -35,7 +35,7 @@ const _date = {
return num + d.getDate()
},
getWeekOfYear: function (d, startDay) {
getWeekOfYear: function (d: Date, startDay: number) {
// Skip to startDay of this week
const now = this.getDayOfYear(d) + (startDay - d.getDay())
// Find the first startDay of the year
@@ -44,111 +44,111 @@ const _date = {
return padStart(String(Math.floor((now - then) / 7) + 1), 2, '0')
},
isLeapYear: function (d) {
isLeapYear: function (d: Date) {
const year = d.getFullYear()
return !!((year & 3) === 0 && (year % 100 || (year % 400 === 0 && year)))
},
getSuffix: function (d) {
getSuffix: function (d: Date) {
const str = d.getDate().toString()
const index = parseInt(str.slice(-1))
return suffixes[index] || suffixes['default']
},
century: function (d) {
century: function (d: Date) {
return parseInt(d.getFullYear().toString().substring(0, 2), 10)
}
}
const formatCodes = {
a: function (d) {
a: function (d: Date) {
return dayNamesShort[d.getDay()]
},
A: function (d) {
A: function (d: Date) {
return dayNames[d.getDay()]
},
b: function (d) {
b: function (d: Date) {
return monthNamesShort[d.getMonth()]
},
B: function (d) {
B: function (d: Date) {
return monthNames[d.getMonth()]
},
c: function (d) {
c: function (d: Date) {
return d.toLocaleString()
},
C: function (d) {
C: function (d: Date) {
return _date.century(d)
},
d: function (d) {
d: function (d: Date) {
return padStart(d.getDate(), 2, '0')
},
e: function (d) {
e: function (d: Date) {
return padStart(d.getDate(), 2)
},
H: function (d) {
H: function (d: Date) {
return padStart(d.getHours(), 2, '0')
},
I: function (d) {
I: function (d: Date) {
return padStart(String(d.getHours() % 12 || 12), 2, '0')
},
j: function (d) {
j: function (d: Date) {
return padStart(_date.getDayOfYear(d), 3, '0')
},
k: function (d) {
k: function (d: Date) {
return padStart(d.getHours(), 2)
},
l: function (d) {
l: function (d: Date) {
return padStart(String(d.getHours() % 12 || 12), 2)
},
L: function (d) {
L: function (d: Date) {
return padStart(d.getMilliseconds(), 3, '0')
},
m: function (d) {
m: function (d: Date) {
return padStart(d.getMonth() + 1, 2, '0')
},
M: function (d) {
M: function (d: Date) {
return padStart(d.getMinutes(), 2, '0')
},
p: function (d) {
p: function (d: Date) {
return (d.getHours() < 12 ? 'AM' : 'PM')
},
P: function (d) {
P: function (d: Date) {
return (d.getHours() < 12 ? 'am' : 'pm')
},
q: function (d) {
q: function (d: Date) {
return _date.getSuffix(d)
},
s: function (d) {
s: function (d: Date) {
return Math.round(d.valueOf() / 1000)
},
S: function (d) {
S: function (d: Date) {
return padStart(d.getSeconds(), 2, '0')
},
u: function (d) {
u: function (d: Date) {
return d.getDay() || 7
},
U: function (d) {
U: function (d: Date) {
return _date.getWeekOfYear(d, 0)
},
w: function (d) {
w: function (d: Date) {
return d.getDay()
},
W: function (d) {
W: function (d: Date) {
return _date.getWeekOfYear(d, 1)
},
x: function (d) {
x: function (d: Date) {
return d.toLocaleDateString()
},
X: function (d) {
X: function (d: Date) {
return d.toLocaleTimeString()
},
y: function (d) {
y: function (d: Date) {
return d.getFullYear().toString().substring(2, 4)
},
Y: function (d) {
Y: function (d: Date) {
return d.getFullYear()
},
z: function (d) {
z: function (d: Date) {
const tz = d.getTimezoneOffset() / 60 * 100
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).N = formatCodes.L
export default function (d, format) {
export default function (d: Date, format: string) {
let output = ''
let remaining = format
@@ -179,6 +179,6 @@ export default function (d, format) {
// Add the format code
const ch = results[0].charAt(1)
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'
}
export function promisify (fn) {
return function (...args) {
export function promisify<T1, T2> (fn: (arg1: T1, cb: (err: Error | null, result: T2) => void) => void): (arg1: T1) => Promise<T2>;
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) => {
fn(...args, (err, result) => {
fn(...args, (err: Error, result: any) => {
err ? reject(err) : resolve(result)
})
})
@@ -35,7 +37,7 @@ export function stringify (value: any): string {
}
function defaultToString (value: any): string {
const cache = []
const cache: string[] = []
return JSON.stringify(value, (key, value) => {
if (isObject(value)) {
if (cache.indexOf(value) !== -1) {
@@ -75,7 +77,10 @@ export function isError (value: any): boolean {
* @param {Function} iteratee The function invoked per iteration.
* @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 || {}
for (const k in object) {
if (object.hasOwnProperty(k)) {
@@ -85,45 +90,12 @@ export function forOwn (object, iteratee: ((val: any, key: string, obj: object)
return object
}
/*
* Assigns own enumerable string keyed properties of source objects to the destination object.
* Source objects are applied from left to right.
* 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 {
export function last <T>(arr: T[]): T;
export function last (arr: string): string;
export function last (arr: any[] | string): any | string {
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.
* (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.
*/
export function range (start: number, stop?: number, step?: number) {
if (arguments.length === 1) {
if (stop === undefined) {
stop = start
start = 0
}
step = step || 1
const arr = []
const arr: number[] = []
for (let i = start; i < stop; i += step) {
arr.push(i)
}