mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-15 12:20:40 -07:00
refactor: strictly typed
This commit is contained in:
@@ -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,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())
|
||||
}
|
||||
|
||||
@@ -13,17 +13,17 @@ const unescapeMap = {
|
||||
''': "'"
|
||||
}
|
||||
|
||||
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
@@ -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,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
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
@@ -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('+')
|
||||
}
|
||||
@@ -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
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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,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
@@ -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
@@ -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
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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('')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 => {
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export default interface IContext {
|
||||
[key: string]: any;
|
||||
liquid_method_missing?: (key: string) => any;
|
||||
}
|
||||
+14
-10
@@ -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] === '[') {
|
||||
|
||||
@@ -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 () {
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = {}
|
||||
}
|
||||
}
|
||||
@@ -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,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
|
||||
}
|
||||
|
||||
@@ -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 } = {}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user