mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-16 04:40:39 -07:00
perf: introduce AST to avoid reparse
This commit is contained in:
@@ -1,43 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
function isQuote (c) {
|
|
||||||
return c === '"' || c === "'"
|
|
||||||
}
|
|
||||||
|
|
||||||
function isOperator (c) {
|
|
||||||
return '!=<>'.includes(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isNumber (c) {
|
|
||||||
return c >= '0' && c <= '9'
|
|
||||||
}
|
|
||||||
|
|
||||||
function isCharacter (c) {
|
|
||||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|
|
||||||
}
|
|
||||||
|
|
||||||
function isVariable (c) {
|
|
||||||
return '_-?'.includes(c) || isCharacter(c) || isNumber(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isBlank (c) {
|
|
||||||
return c === '\n' || c === '\t' || c === ' ' || c === '\r'
|
|
||||||
}
|
|
||||||
|
|
||||||
const types = []
|
|
||||||
for (let i = 0; i < 128; i++) {
|
|
||||||
const c = String.fromCharCode(i)
|
|
||||||
let n = 0
|
|
||||||
if (isVariable(c)) n |= 1
|
|
||||||
if (isOperator(c)) n |= 2
|
|
||||||
if (isBlank(c)) n |= 4
|
|
||||||
if (isQuote(c)) n |= 8
|
|
||||||
types.push(n)
|
|
||||||
}
|
|
||||||
console.log(`
|
|
||||||
const TYPES = '${types.join('')}'
|
|
||||||
const VARIABLE = 1
|
|
||||||
const OPERATOR = 2
|
|
||||||
const BLANK = 4
|
|
||||||
const QUOTE = 8
|
|
||||||
`.trim())
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const isQuote = c => c === '"' || c === "'"
|
||||||
|
const isOperator = c => '!=<>'.includes(c)
|
||||||
|
const isNumber = c => c >= '0' && c <= '9'
|
||||||
|
const isCharacter = c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|
||||||
|
const isVariable = c => '_-?'.includes(c) || isCharacter(c) || isNumber(c)
|
||||||
|
const isBlank = c => c === '\n' || c === '\t' || c === ' ' || c === '\r'
|
||||||
|
const isInlineBlank = c => c === '\t' || c === ' ' || c === '\r'
|
||||||
|
const isSign = c => c === '-' || c === '+'
|
||||||
|
|
||||||
|
const types = []
|
||||||
|
for (let i = 0; i < 128; i++) {
|
||||||
|
const c = String.fromCharCode(i)
|
||||||
|
let n = 0
|
||||||
|
if (isVariable(c)) n |= 1
|
||||||
|
if (isOperator(c)) n |= 2
|
||||||
|
if (isBlank(c)) n |= 4
|
||||||
|
if (isQuote(c)) n |= 8
|
||||||
|
if (isInlineBlank(c)) n |= 16
|
||||||
|
if (isNumber(c)) n |= 32
|
||||||
|
if (isSign(c)) n |= 64
|
||||||
|
types.push(n)
|
||||||
|
}
|
||||||
|
console.log(`
|
||||||
|
// bitmask character types to boost performance
|
||||||
|
// generated by bin/character-gen.js
|
||||||
|
export const TYPES = [${types.join(', ')}]
|
||||||
|
export const VARIABLE = 1
|
||||||
|
export const OPERATOR = 2
|
||||||
|
export const BLANK = 4
|
||||||
|
export const QUOTE = 8
|
||||||
|
export const INLINE_BLANK = 16
|
||||||
|
export const NUMBER = 32
|
||||||
|
export const SIGN = 64
|
||||||
|
`.trim())
|
||||||
@@ -31,7 +31,7 @@ function slice<T> (v: T[], begin: number, length = 1): T[] {
|
|||||||
|
|
||||||
function where<T extends object> (this: FilterImpl, arr: T[], property: string, expected?: any): T[] {
|
function where<T extends object> (this: FilterImpl, arr: T[], property: string, expected?: any): T[] {
|
||||||
return arr.filter(obj => {
|
return arr.filter(obj => {
|
||||||
const value = this.context.getFromScope(obj, property)
|
const value = this.context.getFromScope(obj, property.split('.'))
|
||||||
return expected === undefined ? isTruthy(value) : value === expected
|
return expected === undefined ? isTruthy(value) : value === expected
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import { assert } from '../../util/assert'
|
import { Tokenizer, assert, TagImplOptions, TagToken, Context } from '../../types'
|
||||||
import { identifier } from '../../parser/lexical'
|
|
||||||
import { TagImplOptions, TagToken, Context } from '../../types'
|
|
||||||
|
|
||||||
const re = new RegExp(`(${identifier.source})\\s*=([^]*)`)
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
parse: function (token: TagToken) {
|
parse: function (token: TagToken) {
|
||||||
const match = token.args.match(re) as RegExpMatchArray
|
const tokenizer = new Tokenizer(token.args)
|
||||||
assert(match, `illegal token ${token.raw}`)
|
this.key = tokenizer.readWord().content
|
||||||
this.key = match[1]
|
tokenizer.skipBlank()
|
||||||
this.value = match[2]
|
assert(tokenizer.peek() === '=', () => `illegal token ${token.getText()}`)
|
||||||
|
tokenizer.advance()
|
||||||
|
this.value = tokenizer.remaining()
|
||||||
},
|
},
|
||||||
render: function * (ctx: Context) {
|
render: function * (ctx: Context) {
|
||||||
ctx.bottom()[this.key] = yield this.liquid._evalValue(this.value, ctx)
|
ctx.bottom()[this.key] = yield this.liquid._evalValue(this.value, ctx)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import BlockMode from '../../context/block-mode'
|
import BlockMode from '../../context/block-mode'
|
||||||
import { ParseStream, TagToken, Token, Template, Context, TagImplOptions, Emitter } from '../../types'
|
import { ParseStream, TagToken, TopLevelToken, Template, Context, TagImplOptions, Emitter } from '../../types'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
parse: function (token: TagToken, remainTokens: Token[]) {
|
parse: function (token: TagToken, remainTokens: TopLevelToken[]) {
|
||||||
const match = /\w+/.exec(token.args)
|
const match = /\w+/.exec(token.args)
|
||||||
this.block = match ? match[0] : ''
|
this.block = match ? match[0] : ''
|
||||||
this.tpls = [] as Template[]
|
this.tpls = [] as Template[]
|
||||||
@@ -10,7 +10,7 @@ export default {
|
|||||||
.on('tag:endblock', () => stream.stop())
|
.on('tag:endblock', () => stream.stop())
|
||||||
.on('template', (tpl: Template) => this.tpls.push(tpl))
|
.on('template', (tpl: Template) => this.tpls.push(tpl))
|
||||||
.on('end', () => {
|
.on('end', () => {
|
||||||
throw new Error(`tag ${token.raw} not closed`)
|
throw new Error(`tag ${token.getText()} not closed`)
|
||||||
})
|
})
|
||||||
stream.start()
|
stream.start()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,22 +1,18 @@
|
|||||||
import { assert } from '../../util/assert'
|
import { Tokenizer, assert, Template, Context, TagImplOptions, TagToken, TopLevelToken } from '../../types'
|
||||||
import { identifier } from '../../parser/lexical'
|
|
||||||
import { Template, Context, TagImplOptions, TagToken, Token } from '../../types'
|
|
||||||
|
|
||||||
const re = new RegExp(`(${identifier.source})`)
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
parse: function (tagToken: TagToken, remainTokens: Token[]) {
|
parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) {
|
||||||
const match = tagToken.args.match(re) as RegExpMatchArray
|
const tokenizer = new Tokenizer(tagToken.args)
|
||||||
assert(match, `${tagToken.args} not valid identifier`)
|
this.variable = tokenizer.readWord().content
|
||||||
|
assert(this.variable, () => `${tagToken.args} not valid identifier`)
|
||||||
|
|
||||||
this.variable = match[1]
|
|
||||||
this.templates = []
|
this.templates = []
|
||||||
|
|
||||||
const stream = this.liquid.parser.parseStream(remainTokens)
|
const stream = this.liquid.parser.parseStream(remainTokens)
|
||||||
stream.on('tag:endcapture', () => stream.stop())
|
stream.on('tag:endcapture', () => stream.stop())
|
||||||
.on('template', (tpl: Template) => this.templates.push(tpl))
|
.on('template', (tpl: Template) => this.templates.push(tpl))
|
||||||
.on('end', () => {
|
.on('end', () => {
|
||||||
throw new Error(`tag ${tagToken.raw} not closed`)
|
throw new Error(`tag ${tagToken.getText()} not closed`)
|
||||||
})
|
})
|
||||||
stream.start()
|
stream.start()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Expression, Emitter, TagToken, Token, Context, Template, TagImplOptions, ParseStream } from '../../types'
|
import { Expression, Emitter, TagToken, TopLevelToken, Context, Template, TagImplOptions, ParseStream } from '../../types'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
parse: function (tagToken: TagToken, remainTokens: Token[]) {
|
parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) {
|
||||||
this.cond = tagToken.args
|
this.cond = tagToken.args
|
||||||
this.cases = []
|
this.cases = []
|
||||||
this.elseTemplates = []
|
this.elseTemplates = []
|
||||||
@@ -18,7 +18,7 @@ export default {
|
|||||||
.on('tag:endcase', () => stream.stop())
|
.on('tag:endcase', () => stream.stop())
|
||||||
.on('template', (tpl: Template) => p.push(tpl))
|
.on('template', (tpl: Template) => p.push(tpl))
|
||||||
.on('end', () => {
|
.on('end', () => {
|
||||||
throw new Error(`tag ${tagToken.raw} not closed`)
|
throw new Error(`tag ${tagToken.getText()} not closed`)
|
||||||
})
|
})
|
||||||
|
|
||||||
stream.start()
|
stream.start()
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { TagToken } from '../../parser/tag-token'
|
import { TagToken } from '../../tokens/tag-token'
|
||||||
import { Token } from '../../parser/token'
|
import { TopLevelToken } from '../../tokens/toplevel-token'
|
||||||
import { TagImplOptions } from '../../template/tag/tag-impl-options'
|
import { TagImplOptions } from '../../template/tag/tag-impl-options'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
parse: function (tagToken: TagToken, remainTokens: Token[]) {
|
parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) {
|
||||||
const stream = this.liquid.parser.parseStream(remainTokens)
|
const stream = this.liquid.parser.parseStream(remainTokens)
|
||||||
stream
|
stream
|
||||||
.on('token', (token: TagToken) => {
|
.on('token', (token: TagToken) => {
|
||||||
if (token.name === 'endcomment') stream.stop()
|
if (token.name === 'endcomment') stream.stop()
|
||||||
})
|
})
|
||||||
.on('end', () => {
|
.on('end', () => {
|
||||||
throw new Error(`tag ${tagToken.raw} not closed`)
|
throw new Error(`tag ${tagToken.getText()} not closed`)
|
||||||
})
|
})
|
||||||
stream.start()
|
stream.start()
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-16
@@ -1,28 +1,32 @@
|
|||||||
import { assert } from '../../util/assert'
|
import { assert } from '../../util/assert'
|
||||||
import { value as rValue } from '../../parser/lexical'
|
import { evalToken, Emitter, TagToken, Context, TagImplOptions } from '../../types'
|
||||||
import { Emitter, Expression, TagToken, Context, TagImplOptions } from '../../types'
|
import { Tokenizer } from '../../parser/tokenizer'
|
||||||
|
|
||||||
const groupRE = new RegExp(`^(?:(${rValue.source})\\s*:\\s*)?(.*)$`)
|
|
||||||
const candidatesRE = new RegExp(rValue.source, 'g')
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
parse: function (tagToken: TagToken) {
|
parse: function (tagToken: TagToken) {
|
||||||
let match: RegExpExecArray | null = groupRE.exec(tagToken.args) as RegExpExecArray
|
const tokenizer = new Tokenizer(tagToken.args)
|
||||||
assert(match, `illegal tag: ${tagToken.raw}`)
|
const group = tokenizer.readValue()
|
||||||
|
tokenizer.skipBlank()
|
||||||
this.group = new Expression(match[1])
|
|
||||||
const candidates = match[2]
|
|
||||||
|
|
||||||
this.candidates = []
|
this.candidates = []
|
||||||
|
|
||||||
while ((match = candidatesRE.exec(candidates))) {
|
if (group) {
|
||||||
this.candidates.push(match[0])
|
if (tokenizer.peek() === ':') {
|
||||||
|
this.group = group
|
||||||
|
tokenizer.advance()
|
||||||
|
} else this.candidates.push(group)
|
||||||
}
|
}
|
||||||
assert(this.candidates.length, `empty candidates: ${tagToken.raw}`)
|
|
||||||
|
while (!tokenizer.end()) {
|
||||||
|
const value = tokenizer.readValue()
|
||||||
|
if (value) this.candidates.push(value)
|
||||||
|
tokenizer.readTo(',')
|
||||||
|
}
|
||||||
|
assert(this.candidates.length, () => `empty candidates: ${tagToken.getText()}`)
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function * (ctx: Context, emitter: Emitter) {
|
render: function (ctx: Context, emitter: Emitter) {
|
||||||
const group = yield this.group.value(ctx)
|
const group = evalToken(this.group, ctx)
|
||||||
const fingerprint = `cycle:${group}:` + this.candidates.join(',')
|
const fingerprint = `cycle:${group}:` + this.candidates.join(',')
|
||||||
const groups = ctx.getRegister('cycle')
|
const groups = ctx.getRegister('cycle')
|
||||||
let idx = groups[fingerprint]
|
let idx = groups[fingerprint]
|
||||||
@@ -34,7 +38,7 @@ export default {
|
|||||||
const candidate = this.candidates[idx]
|
const candidate = this.candidates[idx]
|
||||||
idx = (idx + 1) % this.candidates.length
|
idx = (idx + 1) % this.candidates.length
|
||||||
groups[fingerprint] = idx
|
groups[fingerprint] = idx
|
||||||
const html = yield new Expression(candidate).value(ctx)
|
const html = evalToken(candidate, ctx)
|
||||||
emitter.write(html)
|
emitter.write(html)
|
||||||
}
|
}
|
||||||
} as TagImplOptions
|
} as TagImplOptions
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import { assert } from '../../util/assert'
|
import { Tokenizer, Emitter, TagToken, Context, TagImplOptions } from '../../types'
|
||||||
import { identifier } from '../../parser/lexical'
|
|
||||||
import { Emitter, TagToken, Context, TagImplOptions } from '../../types'
|
|
||||||
import { isNumber, stringify } from '../../util/underscore'
|
import { isNumber, stringify } from '../../util/underscore'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
parse: function (token: TagToken) {
|
parse: function (token: TagToken) {
|
||||||
const match = token.args.match(identifier) as RegExpMatchArray
|
const tokenizer = new Tokenizer(token.args)
|
||||||
assert(match, `illegal identifier ${token.args}`)
|
this.variable = tokenizer.readWord().content
|
||||||
this.variable = match[0]
|
|
||||||
},
|
},
|
||||||
render: function (context: Context, emitter: Emitter) {
|
render: function (context: Context, emitter: Emitter) {
|
||||||
const scope = context.environments
|
const scope = context.environments
|
||||||
|
|||||||
+17
-15
@@ -1,21 +1,24 @@
|
|||||||
import { Emitter, TagToken, Token, Context, Template, TagImplOptions, ParseStream } from '../../types'
|
import { assert, Tokenizer, evalToken, Emitter, TagToken, TopLevelToken, Context, Template, TagImplOptions, ParseStream } from '../../types'
|
||||||
import { toCollection } from '../../util/collection'
|
import { toCollection } from '../../util/collection'
|
||||||
import { Expression } from '../../render/expression'
|
|
||||||
import { assert } from '../../util/assert'
|
|
||||||
import { identifier, value } from '../../parser/lexical'
|
|
||||||
import { ForloopDrop } from '../../drop/forloop-drop'
|
import { ForloopDrop } from '../../drop/forloop-drop'
|
||||||
import { Hash } from '../../template/tag/hash'
|
import { Hash } from '../../template/tag/hash'
|
||||||
|
|
||||||
const re = new RegExp(`^(${identifier.source})\\s+in\\s+(${value.source})`)
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
type: 'block',
|
type: 'block',
|
||||||
parse: function (tagToken: TagToken, remainTokens: Token[]) {
|
parse: function (token: TagToken, remainTokens: TopLevelToken[]) {
|
||||||
const match = re.exec(tagToken.args) as RegExpExecArray
|
const toknenizer = new Tokenizer(token.args)
|
||||||
assert(match, `illegal tag: ${tagToken.raw}`)
|
|
||||||
this.variable = match[1]
|
const variable = toknenizer.readWord()
|
||||||
this.collection = match[2]
|
const inStr = toknenizer.readWord()
|
||||||
this.hash = new Hash(tagToken.args.slice(match[0].length))
|
const collection = toknenizer.readValue()
|
||||||
|
assert(
|
||||||
|
variable.size() && inStr.content === 'in' && collection,
|
||||||
|
() => `illegal tag: ${token.getText()}`
|
||||||
|
)
|
||||||
|
|
||||||
|
this.variable = variable.content
|
||||||
|
this.collection = collection
|
||||||
|
this.hash = new Hash(toknenizer.remaining())
|
||||||
this.templates = []
|
this.templates = []
|
||||||
this.elseTemplates = []
|
this.elseTemplates = []
|
||||||
|
|
||||||
@@ -26,15 +29,14 @@ export default {
|
|||||||
.on('tag:endfor', () => stream.stop())
|
.on('tag:endfor', () => stream.stop())
|
||||||
.on('template', (tpl: Template) => p.push(tpl))
|
.on('template', (tpl: Template) => p.push(tpl))
|
||||||
.on('end', () => {
|
.on('end', () => {
|
||||||
throw new Error(`tag ${tagToken.raw} not closed`)
|
throw new Error(`tag ${token.getText()} not closed`)
|
||||||
})
|
})
|
||||||
|
|
||||||
stream.start()
|
stream.start()
|
||||||
},
|
},
|
||||||
render: function * (ctx: Context, emitter: Emitter) {
|
render: function * (ctx: Context, emitter: Emitter) {
|
||||||
const r = this.liquid.renderer
|
const r = this.liquid.renderer
|
||||||
let collection = yield new Expression(this.collection).value(ctx)
|
let collection = toCollection(evalToken(this.collection, ctx))
|
||||||
collection = toCollection(collection)
|
|
||||||
|
|
||||||
if (!collection.length) {
|
if (!collection.length) {
|
||||||
yield r.renderTemplates(this.elseTemplates, ctx, emitter)
|
yield r.renderTemplates(this.elseTemplates, ctx, emitter)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Emitter, isTruthy, Expression, TagToken, Token, Context, Template, TagImplOptions, ParseStream } from '../../types'
|
import { Emitter, isTruthy, Expression, TagToken, TopLevelToken, Context, Template, TagImplOptions, ParseStream } from '../../types'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
parse: function (tagToken: TagToken, remainTokens: Token[]) {
|
parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) {
|
||||||
this.branches = []
|
this.branches = []
|
||||||
this.elseTemplates = []
|
this.elseTemplates = []
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ export default {
|
|||||||
.on('tag:endif', () => stream.stop())
|
.on('tag:endif', () => stream.stop())
|
||||||
.on('template', (tpl: Template) => p.push(tpl))
|
.on('template', (tpl: Template) => p.push(tpl))
|
||||||
.on('end', () => {
|
.on('end', () => {
|
||||||
throw new Error(`tag ${tagToken.raw} not closed`)
|
throw new Error(`tag ${tagToken.getText()} not closed`)
|
||||||
})
|
})
|
||||||
|
|
||||||
stream.start()
|
stream.start()
|
||||||
|
|||||||
+24
-18
@@ -1,35 +1,41 @@
|
|||||||
import { assert } from '../../util/assert'
|
import { assert, evalQuotedToken, TypeGuards, Tokenizer, evalToken, Hash, Emitter, TagToken, Context, TagImplOptions } from '../../types'
|
||||||
import { Expression, Hash, Emitter, TagToken, Context, TagImplOptions } from '../../types'
|
|
||||||
import { quoted, value, quotedLine } from '../../parser/lexical'
|
|
||||||
import BlockMode from '../../context/block-mode'
|
import BlockMode from '../../context/block-mode'
|
||||||
|
|
||||||
const rFile = new RegExp(`^(${quoted.source}|[^\\s,]+)(?:\\s+with\\s+(${value.source}))?`)
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
parse: function (token: TagToken) {
|
parse: function (token: TagToken) {
|
||||||
const match = rFile.exec(token.args)
|
const args = token.args
|
||||||
if (!match) {
|
const tokenizer = new Tokenizer(args)
|
||||||
throw new Error(`illegal argument "${token.args}"`)
|
this.file = this.liquid.options.dynamicPartials
|
||||||
}
|
? tokenizer.readValue()
|
||||||
this.file = match[1]
|
: tokenizer.readFileName()
|
||||||
this.hash = new Hash(token.args.slice(match[0].length))
|
assert(this.file, () => `illegal argument "${token.args}"`)
|
||||||
this.withVar = match[2]
|
|
||||||
|
const begin = tokenizer.p
|
||||||
|
const withStr = tokenizer.readWord()
|
||||||
|
if (withStr.content === 'with') {
|
||||||
|
tokenizer.skipBlank()
|
||||||
|
if (tokenizer.peek() !== ':') {
|
||||||
|
this.withVar = tokenizer.readValue()
|
||||||
|
} else tokenizer.p = begin
|
||||||
|
} else tokenizer.p = begin
|
||||||
|
|
||||||
|
this.hash = new Hash(tokenizer.remaining())
|
||||||
},
|
},
|
||||||
render: function * (ctx: Context, emitter: Emitter) {
|
render: function * (ctx: Context, emitter: Emitter) {
|
||||||
const { liquid, hash, withVar, file } = this
|
const { liquid, hash, withVar, file } = this
|
||||||
const { renderer } = liquid
|
const { renderer } = liquid
|
||||||
const filepath = ctx.opts.dynamicPartials
|
const filepath = ctx.opts.dynamicPartials
|
||||||
? (quotedLine.exec(file)
|
? (TypeGuards.isQuotedToken(file)
|
||||||
? yield renderer.renderTemplates(liquid.parse(file.slice(1, -1)), ctx)
|
? yield renderer.renderTemplates(liquid.parse(evalQuotedToken(file)), ctx)
|
||||||
: yield new Expression(file).value(ctx))
|
: yield evalToken(file, ctx))
|
||||||
: file
|
: file.getText()
|
||||||
assert(filepath, `illegal filename "${file}":"${filepath}"`)
|
assert(filepath, () => `illegal filename "${file}":"${filepath}"`)
|
||||||
|
|
||||||
const saved = ctx.saveRegister('blocks', 'blockMode')
|
const saved = ctx.saveRegister('blocks', 'blockMode')
|
||||||
ctx.setRegister('blocks', {})
|
ctx.setRegister('blocks', {})
|
||||||
ctx.setRegister('blockMode', BlockMode.OUTPUT)
|
ctx.setRegister('blockMode', BlockMode.OUTPUT)
|
||||||
const scope = yield hash.render(ctx)
|
const scope = yield hash.render(ctx)
|
||||||
if (withVar) scope[filepath] = yield new Expression(withVar).evaluate(ctx)
|
if (withVar) scope[filepath] = evalToken(withVar, ctx)
|
||||||
const templates = yield liquid._parseFile(filepath, ctx.opts, ctx.sync)
|
const templates = yield liquid._parseFile(filepath, ctx.opts, ctx.sync)
|
||||||
ctx.push(scope)
|
ctx.push(scope)
|
||||||
yield renderer.renderTemplates(templates, ctx, emitter)
|
yield renderer.renderTemplates(templates, ctx, emitter)
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import { assert } from '../../util/assert'
|
|
||||||
import { identifier } from '../../parser/lexical'
|
|
||||||
import { isNumber, stringify } from '../../util/underscore'
|
import { isNumber, stringify } from '../../util/underscore'
|
||||||
import { Emitter, TagToken, Context, TagImplOptions } from '../../types'
|
import { Tokenizer, Emitter, TagToken, Context, TagImplOptions } from '../../types'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
parse: function (token: TagToken) {
|
parse: function (token: TagToken) {
|
||||||
const match = token.args.match(identifier)
|
const tokenizer = new Tokenizer(token.args)
|
||||||
assert(match, `illegal identifier ${token.args}`)
|
this.variable = tokenizer.readWord().content
|
||||||
this.variable = match![0]
|
|
||||||
},
|
},
|
||||||
render: function (context: Context, emitter: Emitter) {
|
render: function (context: Context, emitter: Emitter) {
|
||||||
const scope = context.environments
|
const scope = context.environments
|
||||||
|
|||||||
+13
-17
@@ -1,29 +1,25 @@
|
|||||||
import { assert } from '../../util/assert'
|
import { assert, evalQuotedToken, TypeGuards, evalToken, Tokenizer, Emitter, Hash, TagToken, TopLevelToken, Context, TagImplOptions } from '../../types'
|
||||||
import { quotedLine, quoted } from '../../parser/lexical'
|
|
||||||
import { Emitter, Hash, Expression, TagToken, Token, Context, TagImplOptions } from '../../types'
|
|
||||||
import BlockMode from '../../context/block-mode'
|
import BlockMode from '../../context/block-mode'
|
||||||
|
|
||||||
const rFile = new RegExp(`^(${quoted.source}|[^\\s,]+)`)
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
parse: function (token: TagToken, remainTokens: Token[]) {
|
parse: function (token: TagToken, remainTokens: TopLevelToken[]) {
|
||||||
const match = rFile.exec(token.args)
|
const tokenizer = new Tokenizer(token.args)
|
||||||
if (!match) {
|
const file = this.liquid.options.dynamicPartials ? tokenizer.readValue() : tokenizer.readFileName()
|
||||||
throw new Error(`illegal argument "${token.args}"`)
|
assert(file, () => `illegal argument "${token.args}"`)
|
||||||
}
|
|
||||||
this.file = match[1]
|
this.file = file
|
||||||
this.hash = new Hash(token.args.slice(match[0].length))
|
this.hash = new Hash(tokenizer.remaining())
|
||||||
this.tpls = this.liquid.parser.parse(remainTokens)
|
this.tpls = this.liquid.parser.parse(remainTokens)
|
||||||
},
|
},
|
||||||
render: function * (ctx: Context, emitter: Emitter) {
|
render: function * (ctx: Context, emitter: Emitter) {
|
||||||
const { liquid, hash, file } = this
|
const { liquid, hash, file } = this
|
||||||
const { renderer } = liquid
|
const { renderer } = liquid
|
||||||
const filepath = ctx.opts.dynamicPartials
|
const filepath = ctx.opts.dynamicPartials
|
||||||
? (quotedLine.exec(file)
|
? (TypeGuards.isQuotedToken(file)
|
||||||
? yield renderer.renderTemplates(liquid.parse(file.slice(1, -1)), ctx)
|
? yield renderer.renderTemplates(liquid.parse(evalQuotedToken(file)), ctx)
|
||||||
: yield new Expression(file).value(ctx))
|
: evalToken(this.file, ctx))
|
||||||
: this.file
|
: file.getText()
|
||||||
assert(filepath, `illegal filename "${file}":"${filepath}"`)
|
assert(filepath, () => `illegal filename "${file.getText()}":"${filepath}"`)
|
||||||
|
|
||||||
// render the remaining tokens immediately
|
// render the remaining tokens immediately
|
||||||
ctx.setRegister('blockMode', BlockMode.STORE)
|
ctx.setRegister('blockMode', BlockMode.STORE)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { TagToken, Token, TagImplOptions } from '../../types'
|
import { TagToken, TopLevelToken, TagImplOptions } from '../../types'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
parse: function (tagToken: TagToken, remainTokens: Token[]) {
|
parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) {
|
||||||
this.tokens = []
|
this.tokens = []
|
||||||
|
|
||||||
const stream = this.liquid.parser.parseStream(remainTokens)
|
const stream = this.liquid.parser.parseStream(remainTokens)
|
||||||
@@ -11,11 +11,11 @@ export default {
|
|||||||
else this.tokens.push(token)
|
else this.tokens.push(token)
|
||||||
})
|
})
|
||||||
.on('end', () => {
|
.on('end', () => {
|
||||||
throw new Error(`tag ${tagToken.raw} not closed`)
|
throw new Error(`tag ${tagToken.getText()} not closed`)
|
||||||
})
|
})
|
||||||
stream.start()
|
stream.start()
|
||||||
},
|
},
|
||||||
render: function () {
|
render: function () {
|
||||||
return this.tokens.map((token: Token) => token.raw).join('')
|
return this.tokens.map((token: TopLevelToken) => token.getText()).join('')
|
||||||
}
|
}
|
||||||
} as TagImplOptions
|
} as TagImplOptions
|
||||||
|
|||||||
+45
-32
@@ -1,56 +1,69 @@
|
|||||||
import { assert } from '../../util/assert'
|
import { assert } from '../../util/assert'
|
||||||
import { ForloopDrop } from '../../drop/forloop-drop'
|
import { ForloopDrop } from '../../drop/forloop-drop'
|
||||||
import { toCollection } from '../../util/collection'
|
import { toCollection } from '../../util/collection'
|
||||||
import { Expression, Hash, Emitter, TagToken, Context, TagImplOptions } from '../../types'
|
import { evalQuotedToken, TypeGuards, Tokenizer, evalToken, Hash, Emitter, TagToken, Context, TagImplOptions } from '../../types'
|
||||||
import { identifier, value, quoted, quotedLine } from '../../parser/lexical'
|
|
||||||
|
|
||||||
const rFile = new RegExp(`^(${quoted.source}|[^\\s,]+)`)
|
|
||||||
const rWith = new RegExp(`^\\s+with\\s+(${value.source})(?:\\s+as\\s+(${identifier.source}))?`)
|
|
||||||
const rFor = new RegExp(`^\\s+for\\s+(${value.source})\\s+as\\s+(${identifier.source})`)
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
parse: function (token: TagToken) {
|
parse: function (token: TagToken) {
|
||||||
let args = token.args
|
const args = token.args
|
||||||
let match = rFile.exec(args)
|
const tokenizer = new Tokenizer(args)
|
||||||
|
this.file = this.liquid.options.dynamicPartials
|
||||||
|
? tokenizer.readValue()
|
||||||
|
: tokenizer.readFileName()
|
||||||
|
assert(this.file, () => `illegal argument "${token.args}"`)
|
||||||
|
|
||||||
assert(match, `illegal argument "${token.args}"`)
|
while (!tokenizer.end()) {
|
||||||
this.file = match![1]
|
tokenizer.skipBlank()
|
||||||
args = args.substr(match![0].length)
|
const begin = tokenizer.p
|
||||||
|
const keyword = tokenizer.readWord()
|
||||||
|
if (keyword.content === 'with' || keyword.content === 'for') {
|
||||||
|
tokenizer.skipBlank()
|
||||||
|
if (tokenizer.peek() !== ':') {
|
||||||
|
const value = tokenizer.readValue()
|
||||||
|
if (value) {
|
||||||
|
const beforeAs = tokenizer.p
|
||||||
|
const asStr = tokenizer.readWord()
|
||||||
|
let alias
|
||||||
|
if (asStr.content === 'as') alias = tokenizer.readWord()
|
||||||
|
else tokenizer.p = beforeAs
|
||||||
|
|
||||||
while (true) {
|
this[keyword.content] = { value, alias: alias && alias.content }
|
||||||
if ((match = rWith.exec(args))) {
|
tokenizer.skipBlank()
|
||||||
this.withVar = match[1]
|
if (tokenizer.peek() === ',') tokenizer.advance()
|
||||||
this.withAs = match[2]
|
continue
|
||||||
args = args.substr(match[0].length)
|
}
|
||||||
} else if ((match = rFor.exec(args))) {
|
}
|
||||||
this.forVar = match[1]
|
}
|
||||||
this.forAs = match[2]
|
tokenizer.p = begin
|
||||||
args = args.substr(match[0].length)
|
break
|
||||||
} else break
|
|
||||||
}
|
}
|
||||||
this.hash = new Hash(args)
|
this.hash = new Hash(tokenizer.remaining())
|
||||||
},
|
},
|
||||||
render: function * (ctx: Context, emitter: Emitter) {
|
render: function * (ctx: Context, emitter: Emitter) {
|
||||||
const { liquid, withVar, withAs, forVar, forAs, file, hash } = this
|
const { liquid, file, hash } = this
|
||||||
const { renderer } = liquid
|
const { renderer } = liquid
|
||||||
const filepath = ctx.opts.dynamicPartials
|
const filepath = ctx.opts.dynamicPartials
|
||||||
? (quotedLine.exec(file)
|
? (TypeGuards.isQuotedToken(file)
|
||||||
? yield renderer.renderTemplates(liquid.parse(file.slice(1, -1)), ctx)
|
? yield renderer.renderTemplates(liquid.parse(evalQuotedToken(file)), ctx)
|
||||||
: yield new Expression(file).value(ctx))
|
: evalToken(file, ctx))
|
||||||
: this.file
|
: file.getText()
|
||||||
assert(filepath, `illegal filename "${file}":"${filepath}"`)
|
assert(filepath, () => `illegal filename "${file.getText()}":"${filepath}"`)
|
||||||
|
|
||||||
const childCtx = new Context({}, ctx.opts, ctx.sync)
|
const childCtx = new Context({}, ctx.opts, ctx.sync)
|
||||||
const scope = yield hash.render(ctx)
|
const scope = yield hash.render(ctx)
|
||||||
if (withVar) scope[withAs || filepath] = yield new Expression(withVar).evaluate(ctx)
|
if (this['with']) {
|
||||||
|
const { value, alias } = this['with']
|
||||||
|
scope[alias || filepath] = evalToken(value, ctx)
|
||||||
|
}
|
||||||
childCtx.push(scope)
|
childCtx.push(scope)
|
||||||
|
|
||||||
if (forVar) {
|
if (this['for']) {
|
||||||
let collection = yield new Expression(forVar).value(ctx)
|
const { value, alias } = this['for']
|
||||||
|
let collection = evalToken(value, ctx)
|
||||||
collection = toCollection(collection)
|
collection = toCollection(collection)
|
||||||
scope['forloop'] = new ForloopDrop(collection.length)
|
scope['forloop'] = new ForloopDrop(collection.length)
|
||||||
for (const item of collection) {
|
for (const item of collection) {
|
||||||
scope[forAs] = item
|
scope[alias] = item
|
||||||
const templates = yield liquid._parseFile(filepath, childCtx.opts, childCtx.sync)
|
const templates = yield liquid._parseFile(filepath, childCtx.opts, childCtx.sync)
|
||||||
yield renderer.renderTemplates(templates, childCtx, emitter)
|
yield renderer.renderTemplates(templates, childCtx, emitter)
|
||||||
scope.forloop.next()
|
scope.forloop.next()
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import { assert } from '../../util/assert'
|
|
||||||
import { toCollection } from '../../util/collection'
|
import { toCollection } from '../../util/collection'
|
||||||
import { Expression, Emitter, Hash, TagToken, Token, Context, Template, TagImplOptions, ParseStream } from '../../types'
|
import { assert, evalToken, Emitter, Hash, TagToken, TopLevelToken, Context, Template, TagImplOptions, ParseStream } from '../../types'
|
||||||
import { identifier, value } from '../../parser/lexical'
|
|
||||||
import { TablerowloopDrop } from '../../drop/tablerowloop-drop'
|
import { TablerowloopDrop } from '../../drop/tablerowloop-drop'
|
||||||
|
import { Tokenizer } from '../../parser/tokenizer'
|
||||||
const re = new RegExp(`^(${identifier.source})\\s+in\\s+` +
|
|
||||||
`(${value.source})`)
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
parse: function (tagToken: TagToken, remainTokens: Token[]) {
|
parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) {
|
||||||
const match = re.exec(tagToken.args) as RegExpExecArray
|
const tokenizer = new Tokenizer(tagToken.args)
|
||||||
assert(match, `illegal tag: ${tagToken.raw}`)
|
|
||||||
|
|
||||||
this.variable = match[1]
|
this.variable = tokenizer.readWord()
|
||||||
this.collection = match[2]
|
tokenizer.skipBlank()
|
||||||
|
|
||||||
|
const tmp = tokenizer.readWord()
|
||||||
|
assert(tmp && tmp.content === 'in', () => `illegal tag: ${tagToken.getText()}`)
|
||||||
|
|
||||||
|
this.collection = tokenizer.readValue()
|
||||||
|
this.hash = new Hash(tokenizer.remaining())
|
||||||
this.templates = []
|
this.templates = []
|
||||||
this.hash = new Hash(tagToken.args.slice(match[0].length))
|
|
||||||
|
|
||||||
let p
|
let p
|
||||||
const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
|
const stream: ParseStream = this.liquid.parser.parseStream(remainTokens)
|
||||||
@@ -23,14 +23,14 @@ export default {
|
|||||||
.on('tag:endtablerow', () => stream.stop())
|
.on('tag:endtablerow', () => stream.stop())
|
||||||
.on('template', (tpl: Template) => p.push(tpl))
|
.on('template', (tpl: Template) => p.push(tpl))
|
||||||
.on('end', () => {
|
.on('end', () => {
|
||||||
throw new Error(`tag ${tagToken.raw} not closed`)
|
throw new Error(`tag ${tagToken.getText()} not closed`)
|
||||||
})
|
})
|
||||||
|
|
||||||
stream.start()
|
stream.start()
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function * (ctx: Context, emitter: Emitter) {
|
render: function * (ctx: Context, emitter: Emitter) {
|
||||||
let collection = toCollection(yield new Expression(this.collection).value(ctx))
|
let collection = toCollection(evalToken(this.collection, ctx))
|
||||||
const hash = yield this.hash.render(ctx)
|
const hash = yield this.hash.render(ctx)
|
||||||
const offset = hash.offset || 0
|
const offset = hash.offset || 0
|
||||||
const limit = (hash.limit === undefined) ? collection.length : hash.limit
|
const limit = (hash.limit === undefined) ? collection.length : hash.limit
|
||||||
@@ -44,7 +44,7 @@ export default {
|
|||||||
ctx.push(scope)
|
ctx.push(scope)
|
||||||
|
|
||||||
for (let idx = 0; idx < collection.length; idx++, tablerowloop.next()) {
|
for (let idx = 0; idx < collection.length; idx++, tablerowloop.next()) {
|
||||||
scope[this.variable] = collection[idx]
|
scope[this.variable.content] = collection[idx]
|
||||||
if (tablerowloop.col0() === 0) {
|
if (tablerowloop.col0() === 0) {
|
||||||
if (tablerowloop.row() !== 1) emitter.write('</tr>')
|
if (tablerowloop.row() !== 1) emitter.write('</tr>')
|
||||||
emitter.write(`<tr class="row${tablerowloop.row()}">`)
|
emitter.write(`<tr class="row${tablerowloop.row()}">`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Emitter, Expression, isFalsy, ParseStream, Context, TagImplOptions, Token, TagToken } from '../../types'
|
import { TopLevelToken, Template, Emitter, Expression, isFalsy, ParseStream, Context, TagImplOptions, Token, TagToken } from '../../types'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
parse: function (tagToken: TagToken, remainTokens: Token[]) {
|
parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) {
|
||||||
this.templates = []
|
this.templates = []
|
||||||
this.elseTemplates = []
|
this.elseTemplates = []
|
||||||
let p
|
let p
|
||||||
@@ -12,9 +12,9 @@ export default {
|
|||||||
})
|
})
|
||||||
.on('tag:else', () => (p = this.elseTemplates))
|
.on('tag:else', () => (p = this.elseTemplates))
|
||||||
.on('tag:endunless', () => stream.stop())
|
.on('tag:endunless', () => stream.stop())
|
||||||
.on('template', tpl => p.push(tpl))
|
.on('template', (tpl: Template) => p.push(tpl))
|
||||||
.on('end', () => {
|
.on('end', () => {
|
||||||
throw new Error(`tag ${tagToken.raw} not closed`)
|
throw new Error(`tag ${tagToken.getText()} not closed`)
|
||||||
})
|
})
|
||||||
|
|
||||||
stream.start()
|
stream.start()
|
||||||
|
|||||||
+3
-77
@@ -1,6 +1,5 @@
|
|||||||
import { Drop } from '../drop/drop'
|
import { Drop } from '../drop/drop'
|
||||||
import { __assign } from 'tslib'
|
import { __assign } from 'tslib'
|
||||||
import { assert } from '../util/assert'
|
|
||||||
import { NormalizedFullOptions, defaultOptions } from '../liquid-options'
|
import { NormalizedFullOptions, defaultOptions } from '../liquid-options'
|
||||||
import { Scope } from './scope'
|
import { Scope } from './scope'
|
||||||
import { isArray, isNil, isString, isFunction, toLiquid } from '../util/underscore'
|
import { isArray, isNil, isString, isFunction, toLiquid } from '../util/underscore'
|
||||||
@@ -34,13 +33,12 @@ export class Context {
|
|||||||
return [this.globals, this.environments, ...this.scopes]
|
return [this.globals, this.environments, ...this.scopes]
|
||||||
.reduce((ctx, val) => __assign(ctx, val), {})
|
.reduce((ctx, val) => __assign(ctx, val), {})
|
||||||
}
|
}
|
||||||
public get (path: string) {
|
public get (paths: string[]) {
|
||||||
const paths = this.parseProp(path)
|
|
||||||
const scope = this.findScope(paths[0])
|
const scope = this.findScope(paths[0])
|
||||||
return this.getFromScope(scope, paths)
|
return this.getFromScope(scope, paths)
|
||||||
}
|
}
|
||||||
public getFromScope (scope: object, paths: string[] | string) {
|
public getFromScope (scope: object, paths: string[] | string) {
|
||||||
if (!isArray(paths)) paths = this.parseProp(paths)
|
if (typeof paths === 'string') paths = paths.split('.')
|
||||||
return paths.reduce((scope, path) => {
|
return paths.reduce((scope, path) => {
|
||||||
scope = readProperty(scope, path)
|
scope = readProperty(scope, path)
|
||||||
if (isNil(scope) && this.opts.strictVariables) {
|
if (isNil(scope) && this.opts.strictVariables) {
|
||||||
@@ -61,67 +59,11 @@ export class Context {
|
|||||||
private findScope (key: string) {
|
private findScope (key: string) {
|
||||||
for (let i = this.scopes.length - 1; i >= 0; i--) {
|
for (let i = this.scopes.length - 1; i >= 0; i--) {
|
||||||
const candidate = this.scopes[i]
|
const candidate = this.scopes[i]
|
||||||
if (key in candidate) {
|
if (key in candidate) return candidate
|
||||||
return candidate
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (key in this.environments) return this.environments
|
if (key in this.environments) return this.environments
|
||||||
return this.globals
|
return this.globals
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Parse property access sequence from access string
|
|
||||||
* @example
|
|
||||||
* accessSeq("foo.bar") // ['foo', 'bar']
|
|
||||||
* accessSeq("foo['bar']") // ['foo', 'bar']
|
|
||||||
* accessSeq("foo['b]r']") // ['foo', 'b]r']
|
|
||||||
* accessSeq("foo[bar.coo]") // ['foo', 'bar'], for bar.coo == 'bar'
|
|
||||||
*/
|
|
||||||
private parseProp (str: string) {
|
|
||||||
str = String(str)
|
|
||||||
const seq: string[] = []
|
|
||||||
const push = () => name.length && (seq.push(name), (name = ''))
|
|
||||||
let name = ''
|
|
||||||
let j
|
|
||||||
let i = 0
|
|
||||||
while (i < str.length) {
|
|
||||||
switch (str[i]) {
|
|
||||||
case '[':
|
|
||||||
push()
|
|
||||||
|
|
||||||
const delemiter = str[i + 1]
|
|
||||||
if (/['"]/.test(delemiter)) { // foo["bar"]
|
|
||||||
j = str.indexOf(delemiter, i + 2)
|
|
||||||
assert(j !== -1, `unbalanced ${delemiter}: ${str}`)
|
|
||||||
name = str.slice(i + 2, j)
|
|
||||||
push()
|
|
||||||
i = j + 2
|
|
||||||
} else { // foo[bar.coo]
|
|
||||||
j = matchRightBracket(str, i + 1)
|
|
||||||
assert(j !== -1, `unbalanced []: ${str}`)
|
|
||||||
name = str.slice(i + 1, j)
|
|
||||||
if (!/^[+-]?\d+$/.test(name)) { // foo[bar] vs. foo[1]
|
|
||||||
name = String(this.get(name))
|
|
||||||
}
|
|
||||||
push()
|
|
||||||
i = j + 1
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case '.':// foo.bar, foo[0].bar
|
|
||||||
push()
|
|
||||||
i++
|
|
||||||
break
|
|
||||||
default:// foo.bar
|
|
||||||
name += str[i++]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
push()
|
|
||||||
|
|
||||||
if (!seq.length) {
|
|
||||||
throw new TypeError(`invalid path:"${str}"`)
|
|
||||||
}
|
|
||||||
return seq
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readProperty (obj: Scope, key: string) {
|
export function readProperty (obj: Scope, key: string) {
|
||||||
@@ -152,19 +94,3 @@ function readSize (obj: Scope) {
|
|||||||
if (isArray(obj) || isString(obj)) return obj.length
|
if (isArray(obj) || isString(obj)) return obj.length
|
||||||
return obj['size']
|
return obj['size']
|
||||||
}
|
}
|
||||||
|
|
||||||
function matchRightBracket (str: string, begin: number) {
|
|
||||||
let stack = 1 // count of '[' - count of ']'
|
|
||||||
for (let i = begin; i < str.length; i++) {
|
|
||||||
if (str[i] === '[') {
|
|
||||||
stack++
|
|
||||||
}
|
|
||||||
if (str[i] === ']') {
|
|
||||||
stack--
|
|
||||||
if (stack === 0) {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|||||||
+2
-2
@@ -38,8 +38,8 @@ export class Liquid {
|
|||||||
_.forOwn(builtinFilters, (handler, name) => this.registerFilter(name, handler))
|
_.forOwn(builtinFilters, (handler, name) => this.registerFilter(name, handler))
|
||||||
}
|
}
|
||||||
public parse (html: string, filepath?: string): Template[] {
|
public parse (html: string, filepath?: string): Template[] {
|
||||||
const tokenizer = new Tokenizer(html, filepath, this.options)
|
const tokenizer = new Tokenizer(html, filepath)
|
||||||
const tokens = tokenizer.readTokens()
|
const tokens = tokenizer.readTopLevelTokens(this.options)
|
||||||
return this.parser.parse(tokens)
|
return this.parser.parse(tokens)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { isArray } from '../util/underscore'
|
import { isArray } from '../util/underscore'
|
||||||
|
import { ValueToken } from '../tokens/value-token'
|
||||||
|
|
||||||
type KeyValuePair = [string?, string?]
|
type KeyValuePair = [string?, ValueToken?]
|
||||||
|
|
||||||
export type FilterArg = string|KeyValuePair
|
export type FilterArg = ValueToken | KeyValuePair
|
||||||
|
|
||||||
export function isKeyValuePair (arr: FilterArg): arr is KeyValuePair {
|
export function isKeyValuePair (arr: FilterArg): arr is KeyValuePair { // TODO check
|
||||||
return isArray(arr)
|
return isArray(arr)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { Token } from './token'
|
|
||||||
|
|
||||||
export class HTMLToken extends Token {
|
|
||||||
public constructor (str: string, input: string, line: number, col: number, file?: string) {
|
|
||||||
super(str, input, line, col, file)
|
|
||||||
this.content = str
|
|
||||||
}
|
|
||||||
public static is (token: Token): token is HTMLToken {
|
|
||||||
return token instanceof HTMLToken
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
// quote related
|
|
||||||
const singleQuoted = /'[^']*'/
|
|
||||||
const doubleQuoted = /"[^"]*"/
|
|
||||||
export const quoted = new RegExp(`${singleQuoted.source}|${doubleQuoted.source}`)
|
|
||||||
export const quoteBalanced = new RegExp(`(?:${quoted.source}|[^'"])*`)
|
|
||||||
|
|
||||||
// basic types
|
|
||||||
export const number = /[+-]?(?:\d+\.?\d*|\.?\d+)/
|
|
||||||
export const bool = /true|false/
|
|
||||||
|
|
||||||
// property access
|
|
||||||
export const identifier = /[\w-]+[?]?/
|
|
||||||
export const subscript = new RegExp(`\\[(?:${quoted.source}|[\\w-\\.]+)\\]`)
|
|
||||||
export const literal = new RegExp(`(?:${quoted.source}|${bool.source}|${number.source})`)
|
|
||||||
export const variable = new RegExp(`${identifier.source}(?:\\.${identifier.source}|${subscript.source})*`)
|
|
||||||
|
|
||||||
// range related
|
|
||||||
export const rangeLimit = new RegExp(`(?:${variable.source}|${number.source})`)
|
|
||||||
export const range = new RegExp(`\\(${rangeLimit.source}\\.\\.${rangeLimit.source}\\)`)
|
|
||||||
export const rangeCapture = new RegExp(`\\((${rangeLimit.source})\\.\\.(${rangeLimit.source})\\)`)
|
|
||||||
|
|
||||||
export const value = new RegExp(`(?:${variable.source}|${literal.source}|${range.source})`)
|
|
||||||
|
|
||||||
// full match
|
|
||||||
export const tagLine = new RegExp(`^\\s*(${identifier.source})\\s*([\\s\\S]*?)\\s*$`)
|
|
||||||
export const numberLine = new RegExp(`^${number.source}$`)
|
|
||||||
export const boolLine = new RegExp(`^${bool.source}$`, 'i')
|
|
||||||
export const quotedLine = new RegExp(`^${quoted.source}$`)
|
|
||||||
export const rangeLine = new RegExp(`^${rangeCapture.source}$`)
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { VARIABLE } from '../util/character'
|
||||||
|
|
||||||
|
const trie = {
|
||||||
|
a: { n: { d: { end: true, needBoundary: true } } },
|
||||||
|
o: { r: { end: true, needBoundary: true } },
|
||||||
|
c: { o: { n: { t: { a: { i: { n: { s: { end: true, needBoundary: true } } } } } } } },
|
||||||
|
'=': { '=': { end: true } },
|
||||||
|
'!': { '=': { end: true } },
|
||||||
|
'>': { end: true, '=': { end: true } },
|
||||||
|
'<': { end: true, '=': { end: true } }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function matchOperator (str: string, begin: number, end = str.length) {
|
||||||
|
let node = trie
|
||||||
|
let i = begin
|
||||||
|
let info
|
||||||
|
while (node[str[i]] && i < end) {
|
||||||
|
node = node[str[i++]]
|
||||||
|
if (node['end']) info = node
|
||||||
|
}
|
||||||
|
if (!info) return -1
|
||||||
|
if (info['needBoundary'] && str.charCodeAt(i) & VARIABLE) return -1
|
||||||
|
return i
|
||||||
|
}
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { DelimitedToken } from './delimited-token'
|
|
||||||
import { Token } from './token'
|
|
||||||
import { NormalizedFullOptions } from '../liquid-options'
|
|
||||||
|
|
||||||
export class OutputToken extends DelimitedToken {
|
|
||||||
public constructor (
|
|
||||||
raw: string,
|
|
||||||
value: string,
|
|
||||||
input: string,
|
|
||||||
line: number,
|
|
||||||
pos: number,
|
|
||||||
options: NormalizedFullOptions,
|
|
||||||
file?: string
|
|
||||||
) {
|
|
||||||
super(raw, value, input, line, pos, options.trimOutputLeft, options.trimOutputRight, file)
|
|
||||||
}
|
|
||||||
public static is (token: Token): token is OutputToken {
|
|
||||||
return token instanceof OutputToken
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+11
-10
@@ -1,20 +1,21 @@
|
|||||||
import { Token } from '../parser/token'
|
import { Token } from '../tokens/token'
|
||||||
import { Template } from '../template/template'
|
import { Template } from '../template/template'
|
||||||
import { TagToken } from './tag-token'
|
import { isTagToken } from '../util/type-guards'
|
||||||
|
import { TopLevelToken } from '../tokens/toplevel-token'
|
||||||
|
|
||||||
type ParseToken = ((token: Token, remainTokens: Token[]) => Template)
|
type ParseToken<T extends Token> = ((token: T, remainTokens: T[]) => Template)
|
||||||
|
|
||||||
export class ParseStream {
|
export class ParseStream<T extends Token = TopLevelToken> {
|
||||||
private tokens: Token[]
|
private tokens: T[]
|
||||||
private handlers: {[key: string]: (arg: any) => void} = {}
|
private handlers: {[key: string]: (arg: any) => void} = {}
|
||||||
private stopRequested = false
|
private stopRequested = false
|
||||||
private parseToken: ParseToken
|
private parseToken: ParseToken<T>
|
||||||
|
|
||||||
public constructor (tokens: Token[], parseToken: ParseToken) {
|
public constructor (tokens: T[], parseToken: ParseToken<T>) {
|
||||||
this.tokens = tokens
|
this.tokens = tokens
|
||||||
this.parseToken = parseToken
|
this.parseToken = parseToken
|
||||||
}
|
}
|
||||||
public on<T extends Template | Token | undefined> (name: string, cb: (arg: T) => void): ParseStream {
|
public on<T2 extends Template | T | undefined> (name: string, cb: (arg: T2) => void): ParseStream<T> {
|
||||||
this.handlers[name] = cb
|
this.handlers[name] = cb
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
@@ -24,10 +25,10 @@ export class ParseStream {
|
|||||||
}
|
}
|
||||||
public start () {
|
public start () {
|
||||||
this.trigger('start')
|
this.trigger('start')
|
||||||
let token: Token | undefined
|
let token: T | undefined
|
||||||
while (!this.stopRequested && (token = this.tokens.shift())) {
|
while (!this.stopRequested && (token = this.tokens.shift())) {
|
||||||
if (this.trigger('token', token)) continue
|
if (this.trigger('token', token)) continue
|
||||||
if (TagToken.is(token) && this.trigger(`tag:${token.name}`, token)) {
|
if (isTagToken(token) && this.trigger(`tag:${token.name}`, token)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const template = this.parseToken(token, this.tokens)
|
const template = this.parseToken(token, this.tokens)
|
||||||
|
|||||||
@@ -1,10 +1,3 @@
|
|||||||
import { last } from '../util/underscore'
|
|
||||||
import { NullDrop } from '../drop/null-drop'
|
|
||||||
import { EmptyDrop } from '../drop/empty-drop'
|
|
||||||
import { BlankDrop } from '../drop/blank-drop'
|
|
||||||
|
|
||||||
type literal = true | false | NullDrop | EmptyDrop | BlankDrop | number | string
|
|
||||||
|
|
||||||
const rHex = /[\da-fA-F]/
|
const rHex = /[\da-fA-F]/
|
||||||
const rOct = /[0-7]/
|
const rOct = /[0-7]/
|
||||||
const escapeChar = {
|
const escapeChar = {
|
||||||
@@ -23,18 +16,6 @@ function hexVal (c: string) {
|
|||||||
return code - 48
|
return code - 48
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseLiteral (str: string): literal | undefined {
|
|
||||||
str = str.trim()
|
|
||||||
|
|
||||||
if (str === 'true') return true
|
|
||||||
if (str === 'false') return false
|
|
||||||
if (str === 'nil' || str === 'null') return new NullDrop()
|
|
||||||
if (str === 'empty') return new EmptyDrop()
|
|
||||||
if (str === 'blank') return new BlankDrop()
|
|
||||||
if (!isNaN(Number(str))) return Number(str)
|
|
||||||
if ((str[0] === '"' || str[0] === "'") && str[0] === last(str)) return parseStringLiteral(str)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseStringLiteral (str: string): string {
|
export function parseStringLiteral (str: string): string {
|
||||||
let ret = ''
|
let ret = ''
|
||||||
for (let i = 1; i < str.length - 1; i++) {
|
for (let i = 1; i < str.length - 1; i++) {
|
||||||
@@ -66,3 +47,4 @@ export function parseStringLiteral (str: string): string {
|
|||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import { ParseError } from '../util/error'
|
import { ParseError } from '../util/error'
|
||||||
import { Liquid } from '../liquid'
|
import { Liquid } from '../liquid'
|
||||||
import { ParseStream } from './parse-stream'
|
import { ParseStream } from './parse-stream'
|
||||||
import { Token } from './token'
|
import { isTagToken, isOutputToken } from '../util/type-guards'
|
||||||
import { TagToken } from './tag-token'
|
import { OutputToken } from '../tokens/output-token'
|
||||||
import { OutputToken } from './output-token'
|
|
||||||
import { Tag } from '../template/tag/tag'
|
import { Tag } from '../template/tag/tag'
|
||||||
import { Output } from '../template/output'
|
import { Output } from '../template/output'
|
||||||
import { HTML } from '../template/html'
|
import { HTML } from '../template/html'
|
||||||
import { Template } from '../template/template'
|
import { Template } from '../template/template'
|
||||||
|
import { TopLevelToken } from '../tokens/toplevel-token'
|
||||||
|
|
||||||
export default class Parser {
|
export default class Parser {
|
||||||
private liquid: Liquid
|
private liquid: Liquid
|
||||||
@@ -15,7 +15,7 @@ export default class Parser {
|
|||||||
public constructor (liquid: Liquid) {
|
public constructor (liquid: Liquid) {
|
||||||
this.liquid = liquid
|
this.liquid = liquid
|
||||||
}
|
}
|
||||||
public parse (tokens: Token[]) {
|
public parse (tokens: TopLevelToken[]) {
|
||||||
let token
|
let token
|
||||||
const templates: Template[] = []
|
const templates: Template[] = []
|
||||||
while ((token = tokens.shift())) {
|
while ((token = tokens.shift())) {
|
||||||
@@ -23,12 +23,12 @@ export default class Parser {
|
|||||||
}
|
}
|
||||||
return templates
|
return templates
|
||||||
}
|
}
|
||||||
public parseToken (token: Token, remainTokens: Token[]) {
|
public parseToken (token: TopLevelToken, remainTokens: TopLevelToken[]) {
|
||||||
try {
|
try {
|
||||||
if (TagToken.is(token)) {
|
if (isTagToken(token)) {
|
||||||
return new Tag(token, remainTokens, this.liquid)
|
return new Tag(token, remainTokens, this.liquid)
|
||||||
}
|
}
|
||||||
if (OutputToken.is(token)) {
|
if (isOutputToken(token)) {
|
||||||
return new Output(token as OutputToken, this.liquid.filters)
|
return new Output(token as OutputToken, this.liquid.filters)
|
||||||
}
|
}
|
||||||
return new HTML(token)
|
return new HTML(token)
|
||||||
@@ -36,7 +36,7 @@ export default class Parser {
|
|||||||
throw new ParseError(e, token)
|
throw new ParseError(e, token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public parseStream (tokens: Token[]) {
|
public parseStream (tokens: TopLevelToken[]) {
|
||||||
return new ParseStream(tokens, (token, tokens) => this.parseToken(token, tokens))
|
return new ParseStream(tokens, (token, tokens) => this.parseToken(token, tokens))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
export class Substr {
|
|
||||||
constructor (
|
|
||||||
public str: string,
|
|
||||||
public begin: number,
|
|
||||||
public end: number = begin
|
|
||||||
) {}
|
|
||||||
size () {
|
|
||||||
return this.end - this.begin
|
|
||||||
}
|
|
||||||
toString () {
|
|
||||||
return this.str.slice(this.begin, this.end)
|
|
||||||
}
|
|
||||||
first () {
|
|
||||||
return this.str[this.begin]
|
|
||||||
}
|
|
||||||
last () {
|
|
||||||
return this.str[this.end - 1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { DelimitedToken } from './delimited-token'
|
|
||||||
import { Token } from './token'
|
|
||||||
import { TokenizationError } from '../util/error'
|
|
||||||
import * as lexical from './lexical'
|
|
||||||
import { NormalizedFullOptions } from '../liquid-options'
|
|
||||||
|
|
||||||
export class TagToken extends DelimitedToken {
|
|
||||||
public name: string
|
|
||||||
public args: string
|
|
||||||
public constructor (
|
|
||||||
raw: string,
|
|
||||||
value: string,
|
|
||||||
input: string,
|
|
||||||
line: number,
|
|
||||||
pos: number,
|
|
||||||
options: NormalizedFullOptions,
|
|
||||||
file?: string
|
|
||||||
) {
|
|
||||||
super(raw, value, input, line, pos, options.trimTagLeft, options.trimTagRight, file)
|
|
||||||
const match = this.content.match(lexical.tagLine)
|
|
||||||
if (!match) {
|
|
||||||
throw new TokenizationError(`illegal tag syntax`, this)
|
|
||||||
}
|
|
||||||
this.name = match[1]
|
|
||||||
this.args = match[2]
|
|
||||||
}
|
|
||||||
public static is (token: Token): token is TagToken {
|
|
||||||
return token instanceof TagToken
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
export enum TokenKind {
|
||||||
|
Number,
|
||||||
|
Literal,
|
||||||
|
Tag,
|
||||||
|
Output,
|
||||||
|
HTML,
|
||||||
|
Filter,
|
||||||
|
Hash,
|
||||||
|
PropertyAccess,
|
||||||
|
Word,
|
||||||
|
Range,
|
||||||
|
Quoted,
|
||||||
|
Operator
|
||||||
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
export class Token {
|
|
||||||
public trimLeft = false
|
|
||||||
public trimRight = false
|
|
||||||
public content: string
|
|
||||||
public constructor (
|
|
||||||
public raw: string,
|
|
||||||
public input: string,
|
|
||||||
public line: number,
|
|
||||||
public col: number,
|
|
||||||
public file?: string
|
|
||||||
) {
|
|
||||||
this.content = raw
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+195
-193
@@ -1,158 +1,158 @@
|
|||||||
import { whiteSpaceCtrl } from './whitespace-ctrl'
|
import { whiteSpaceCtrl } from './whitespace-ctrl'
|
||||||
import { Substr } from './substr'
|
import { NumberToken } from '../tokens/number-token'
|
||||||
|
import { WordToken } from '../tokens/word-token'
|
||||||
|
import { literalValues } from '../util/literal'
|
||||||
|
import { LiteralToken } from '../tokens/literal-token'
|
||||||
|
import { OperatorToken } from '../tokens/operator-token'
|
||||||
|
import { PropertyAccessToken } from '../tokens/property-access-token'
|
||||||
|
import { assert } from '../util/assert'
|
||||||
|
import { TopLevelToken } from '../tokens/toplevel-token'
|
||||||
import { FilterArg } from './filter-arg'
|
import { FilterArg } from './filter-arg'
|
||||||
import { FilterToken } from './filter-token'
|
import { FilterToken } from '../tokens/filter-token'
|
||||||
|
import { HashToken } from '../tokens/hash-token'
|
||||||
|
import { QuotedToken } from '../tokens/quoted-token'
|
||||||
import { ellipsis } from '../util/underscore'
|
import { ellipsis } from '../util/underscore'
|
||||||
import { HTMLToken } from './html-token'
|
import { HTMLToken } from '../tokens/html-token'
|
||||||
import { TagToken } from './tag-token'
|
import { TagToken } from '../tokens/tag-token'
|
||||||
import { Token } from './token'
|
import { Token } from '../tokens/token'
|
||||||
import { OutputToken } from './output-token'
|
import { RangeToken } from '../tokens/range-token'
|
||||||
|
import { ValueToken } from '../tokens/value-token'
|
||||||
|
import { OutputToken } from '../tokens/output-token'
|
||||||
import { TokenizationError } from '../util/error'
|
import { TokenizationError } from '../util/error'
|
||||||
import { NormalizedFullOptions, defaultOptions } from '../liquid-options'
|
import { NormalizedFullOptions, defaultOptions } from '../liquid-options'
|
||||||
|
import { TYPES, QUOTE, BLANK, VARIABLE } from '../util/character'
|
||||||
// bitmask character types to boost performance
|
import { matchOperator } from './match-operator'
|
||||||
// generated by bin/char-types.js
|
|
||||||
const TYPES = '00000000044004000000000000000000428000080000010011111111110022210111111111111111111111111110000101111111111111111111111111100000'
|
|
||||||
const VARIABLE = 1
|
|
||||||
const OPERATOR = 2
|
|
||||||
const BLANK = 4
|
|
||||||
const QUOTE = 8
|
|
||||||
|
|
||||||
export class Tokenizer {
|
export class Tokenizer {
|
||||||
private p = 0
|
p = 0
|
||||||
private N: number
|
N: number
|
||||||
private line = 1
|
|
||||||
private col = 1
|
|
||||||
constructor (
|
constructor (
|
||||||
private input: string,
|
private input: string,
|
||||||
private file: string = '',
|
private file: string = ''
|
||||||
private options: NormalizedFullOptions = defaultOptions
|
|
||||||
) {
|
) {
|
||||||
this.N = input.length
|
this.N = input.length
|
||||||
}
|
}
|
||||||
|
|
||||||
* readExpression (): IterableIterator<string> {
|
* readExpression (): IterableIterator<Token> {
|
||||||
|
const operand = this.readValue()
|
||||||
|
if (!operand) return
|
||||||
|
|
||||||
|
yield operand
|
||||||
|
|
||||||
while (this.p < this.N) {
|
while (this.p < this.N) {
|
||||||
|
const operator = this.readOperator()
|
||||||
|
if (!operator) return
|
||||||
|
|
||||||
const operand = this.readValue()
|
const operand = this.readValue()
|
||||||
if (operand.size()) {
|
if (!operand) return
|
||||||
yield operand.toString()
|
|
||||||
continue
|
yield operator
|
||||||
}
|
yield operand
|
||||||
this.readBlank()
|
|
||||||
const operator = new Substr(this.input, this.p)
|
|
||||||
while (OPERATOR & this.peekType()) operator.end = this.read()
|
|
||||||
if (operator.size()) {
|
|
||||||
yield operator.toString()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
this.read()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
readOperator (): OperatorToken | undefined {
|
||||||
readFilterTokens (): FilterToken[] {
|
this.skipBlank()
|
||||||
|
const end = matchOperator(this.input, this.p, this.p + 8)
|
||||||
|
if (end === -1) return
|
||||||
|
return new OperatorToken(this.input, this.p, (this.p = end), this.file)
|
||||||
|
}
|
||||||
|
readFilters (): FilterToken[] {
|
||||||
const filters = []
|
const filters = []
|
||||||
while (true) {
|
while (true) {
|
||||||
const filter = this.readFilterToken()
|
const filter = this.readFilter()
|
||||||
if (!filter) return filters
|
if (!filter) return filters
|
||||||
filters.push(filter)
|
filters.push(filter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
readFilter (): FilterToken | null {
|
||||||
// | foo
|
|
||||||
// | foo: a
|
|
||||||
// | foo: a, b
|
|
||||||
// | foo: a, b: 1
|
|
||||||
readFilterToken (): FilterToken | null {
|
|
||||||
this.readTo('|')
|
this.readTo('|')
|
||||||
const begin = this.p
|
const begin = this.p
|
||||||
const name = this.readVariable().toString()
|
const name = this.readWord()
|
||||||
if (!name) return null
|
if (!name.size()) return null
|
||||||
const args = []
|
const args = []
|
||||||
this.readBlank()
|
this.skipBlank()
|
||||||
if (this.peek() === ':') {
|
if (this.peek() === ':') {
|
||||||
do {
|
do {
|
||||||
this.read()
|
++this.p
|
||||||
const arg = this.readFilterArg()
|
const arg = this.readFilterArg()
|
||||||
arg && args.push(arg)
|
arg && args.push(arg)
|
||||||
while (this.p < this.N && this.peek() !== ',' && this.peek() !== '|') this.read()
|
while (this.p < this.N && this.peek() !== ',' && this.peek() !== '|') ++this.p
|
||||||
} while (this.peek() === ',')
|
} while (this.peek() === ',')
|
||||||
}
|
}
|
||||||
const raw = this.input.slice(begin, this.p)
|
return new FilterToken(name.getText(), args, this.input, begin, this.p, this.file)
|
||||||
return new FilterToken(name, args, raw, this.input, this.line, this.col, this.file)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
readFilterArg (): FilterArg | null {
|
readFilterArg (): FilterArg | undefined {
|
||||||
const key = this.readValue()
|
const key = this.readValue()
|
||||||
if (!key.size()) return null
|
if (!key) return
|
||||||
this.readBlank()
|
this.skipBlank()
|
||||||
if (this.peek() === ':') {
|
if (this.peek() !== ':') return key
|
||||||
this.read()
|
++this.p
|
||||||
return [key.toString(), this.readValue().toString()]
|
const value = this.readValue()
|
||||||
}
|
return [key.getText(), value]
|
||||||
return key.toString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
readTokens (): Token[] {
|
readTopLevelTokens (options: NormalizedFullOptions = defaultOptions): TopLevelToken[] {
|
||||||
const tokens: Token[] = []
|
const tokens: TopLevelToken[] = []
|
||||||
while (this.p < this.N) {
|
while (this.p < this.N) {
|
||||||
const token = this.readToken()
|
const token = this.readTopLevelToken(options)
|
||||||
tokens.push(token)
|
tokens.push(token)
|
||||||
}
|
}
|
||||||
whiteSpaceCtrl(tokens, this.options)
|
whiteSpaceCtrl(tokens, options)
|
||||||
return tokens
|
return tokens
|
||||||
}
|
}
|
||||||
|
|
||||||
readToken (): Token {
|
readTopLevelToken (options: NormalizedFullOptions): TopLevelToken {
|
||||||
const { tagDelimiterLeft, outputDelimiterLeft } = this.options
|
const { tagDelimiterLeft, outputDelimiterLeft } = options
|
||||||
if (this.matchWord(tagDelimiterLeft)) return this.readTagToken()
|
if (this.matchWord(tagDelimiterLeft)) return this.readTagToken(options)
|
||||||
if (this.matchWord(outputDelimiterLeft)) return this.readOutputToken()
|
if (this.matchWord(outputDelimiterLeft)) return this.readOutputToken(options)
|
||||||
return this.readHTMLToken()
|
return this.readHTMLToken(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
readHTMLToken (): HTMLToken {
|
readHTMLToken (options: NormalizedFullOptions): HTMLToken {
|
||||||
const html = new Substr(this.input, this.p)
|
const begin = this.p
|
||||||
while (this.p < this.N) {
|
while (this.p < this.N) {
|
||||||
const { tagDelimiterLeft, outputDelimiterLeft } = this.options
|
const { tagDelimiterLeft, outputDelimiterLeft } = options
|
||||||
if (this.matchWord(tagDelimiterLeft)) break
|
if (this.matchWord(tagDelimiterLeft)) break
|
||||||
if (this.matchWord(outputDelimiterLeft)) break
|
if (this.matchWord(outputDelimiterLeft)) break
|
||||||
html.end = this.read()
|
++this.p
|
||||||
}
|
}
|
||||||
return new HTMLToken(html.toString(), this.input, this.line, this.col, this.file)
|
return new HTMLToken(this.input, begin, this.p, this.file)
|
||||||
}
|
}
|
||||||
|
|
||||||
readTagToken (): TagToken {
|
readTagToken (options: NormalizedFullOptions): TagToken {
|
||||||
const { line, col, file, input, options } = this
|
const { file, input } = this
|
||||||
const { tagDelimiterLeft, tagDelimiterRight } = options
|
const { tagDelimiterRight } = options
|
||||||
const buffer = this.readTo(tagDelimiterRight).toString()
|
const begin = this.p
|
||||||
if (!this.reverseMatchWord(tagDelimiterRight, buffer)) {
|
if (this.readTo(tagDelimiterRight) === -1) {
|
||||||
throw new TokenizationError(
|
this.mkError(`tag "${this.ellipsis(begin)}" not closed`, begin)
|
||||||
`tag "${ellipsis(buffer, 16)}" not closed`,
|
|
||||||
new Token(buffer, input, line, col, file)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
const value = buffer.slice(tagDelimiterLeft.length, -tagDelimiterRight.length)
|
return new TagToken(input, begin, this.p, options, file)
|
||||||
return new TagToken(buffer, value, input, line, col, options, file)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
readOutputToken (): OutputToken {
|
readOutputToken (options: NormalizedFullOptions): OutputToken {
|
||||||
const { line, col, file, input, options } = this
|
const { file, input } = this
|
||||||
const { outputDelimiterLeft, outputDelimiterRight } = options
|
const { outputDelimiterRight } = options
|
||||||
const buffer = this.readTo(outputDelimiterRight).toString()
|
const begin = this.p
|
||||||
if (!this.reverseMatchWord(outputDelimiterRight, buffer)) {
|
if (this.readTo(outputDelimiterRight) === -1) {
|
||||||
throw new TokenizationError(
|
this.mkError(`output "${this.ellipsis(begin)}" not closed`, begin)
|
||||||
`output "${ellipsis(buffer, 16)}" not closed`,
|
|
||||||
new Token(buffer, input, line, col, file)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
const value = buffer.slice(outputDelimiterLeft.length, -outputDelimiterRight.length)
|
return new OutputToken(input, begin, this.p, options, file)
|
||||||
return new OutputToken(buffer, value, input, line, col, options, file)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
readVariable (): Substr {
|
mkError (msg: string, begin: number) {
|
||||||
this.readBlank()
|
throw new TokenizationError(msg, new WordToken(this.input, begin, this.N, this.file))
|
||||||
const ans = new Substr(this.input, this.p)
|
}
|
||||||
while (this.peekType() & VARIABLE) ans.end = this.read()
|
|
||||||
return ans
|
ellipsis (begin: number = this.p) {
|
||||||
|
return ellipsis(this.input.slice(begin), 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
readWord (): WordToken { // rename to identifier
|
||||||
|
this.skipBlank()
|
||||||
|
const begin = this.p
|
||||||
|
while (this.peekType() & VARIABLE) ++this.p
|
||||||
|
return new WordToken(this.input, begin, this.p, this.file)
|
||||||
}
|
}
|
||||||
|
|
||||||
readHashes () {
|
readHashes () {
|
||||||
@@ -164,133 +164,135 @@ export class Tokenizer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readHash () {
|
readHash (): HashToken | undefined {
|
||||||
this.readBlank()
|
this.skipBlank()
|
||||||
if (this.peek() === ',') this.read()
|
if (this.peek() === ',') ++this.p
|
||||||
const name = this.readVariable().toString()
|
const begin = this.p
|
||||||
if (!name) return null
|
const name = this.readWord()
|
||||||
|
if (!name.size()) return
|
||||||
|
let value
|
||||||
|
|
||||||
this.readBlank()
|
this.skipBlank()
|
||||||
let value = ''
|
|
||||||
if (this.peek() === ':') {
|
if (this.peek() === ':') {
|
||||||
this.read()
|
++this.p
|
||||||
value = this.readValue().toString()
|
value = this.readValue()
|
||||||
}
|
}
|
||||||
return [name, value]
|
return new HashToken(this.input, begin, this.p, name, value, this.file)
|
||||||
}
|
}
|
||||||
|
|
||||||
readPropertyAccess (): Substr {
|
remaining () {
|
||||||
this.readBlank()
|
return this.input.slice(this.p)
|
||||||
const ans = new Substr(this.input, this.p)
|
}
|
||||||
let nested = 0
|
|
||||||
|
advance (i = 1) {
|
||||||
|
this.p += i
|
||||||
|
}
|
||||||
|
|
||||||
|
end () {
|
||||||
|
return this.p >= this.N
|
||||||
|
}
|
||||||
|
|
||||||
|
readTo (end: string): number {
|
||||||
while (this.p < this.N) {
|
while (this.p < this.N) {
|
||||||
const c = this.peek()
|
++this.p
|
||||||
const code = this.peekType()
|
if (this.reverseMatchWord(end)) return this.p
|
||||||
if (c === '[') {
|
|
||||||
this.read()
|
|
||||||
ans.end = this.readValue().end
|
|
||||||
nested++
|
|
||||||
} else if (c === ']') {
|
|
||||||
if (!nested) break
|
|
||||||
ans.end = this.read()
|
|
||||||
nested--
|
|
||||||
} else if (c === '.') {
|
|
||||||
if (this.peekType(1) & VARIABLE) {
|
|
||||||
this.read()
|
|
||||||
ans.end = this.readVariable().end
|
|
||||||
} else break
|
|
||||||
} else if (code & VARIABLE) {
|
|
||||||
ans.end = this.read()
|
|
||||||
} else {
|
|
||||||
if (nested) this.read()
|
|
||||||
else break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return ans
|
return -1
|
||||||
}
|
}
|
||||||
readTo (end: string): Substr {
|
|
||||||
const ans = new Substr(this.input, this.p)
|
readValue (): ValueToken | undefined {
|
||||||
while (this.p < this.N) {
|
const value = this.readQuoted() || this.readRange()
|
||||||
ans.end = this.read()
|
if (value) return value
|
||||||
if (this.reverseMatchWord(end)) break
|
|
||||||
|
const variable = this.readWord()
|
||||||
|
if (!variable.size()) return
|
||||||
|
|
||||||
|
let isNumber = variable.isNumber(true)
|
||||||
|
const props: (QuotedToken | WordToken)[] = []
|
||||||
|
while (true) {
|
||||||
|
if (this.peek() === '[') {
|
||||||
|
isNumber = false
|
||||||
|
this.p++
|
||||||
|
const prop = this.readValue() || new WordToken(this.input, this.p, this.p, this.file)
|
||||||
|
this.readTo(']')
|
||||||
|
props.push(prop)
|
||||||
|
} else if (this.peek() === '.' && this.peek(1) !== '.') { // skip range syntax
|
||||||
|
this.p++
|
||||||
|
const prop = this.readWord()
|
||||||
|
if (!prop.size()) break
|
||||||
|
if (!prop.isNumber()) isNumber = false
|
||||||
|
props.push(prop)
|
||||||
|
} else break
|
||||||
}
|
}
|
||||||
return ans
|
if (!props.length && literalValues.hasOwnProperty(variable.content)) {
|
||||||
|
return new LiteralToken(this.input, variable.begin, variable.end, this.file)
|
||||||
|
}
|
||||||
|
if (isNumber) return new NumberToken(variable, props[0] as WordToken)
|
||||||
|
return new PropertyAccessToken(variable, props, this.p)
|
||||||
}
|
}
|
||||||
readValue (): Substr {
|
|
||||||
let val = this.readQuoted()
|
readRange (): RangeToken | undefined {
|
||||||
if (val.size()) return val
|
this.skipBlank()
|
||||||
val = this.readBoolean()
|
const begin = this.p
|
||||||
if (val.size()) return val
|
if (this.peek() !== '(') return
|
||||||
val = this.readPropertyAccess()
|
++this.p
|
||||||
if (val.size()) return val
|
const lhs = this.readValueOrThrow()
|
||||||
return this.readRange()
|
this.p += 2
|
||||||
|
const rhs = this.readValueOrThrow()
|
||||||
|
++this.p
|
||||||
|
return new RangeToken(this.input, begin, this.p, lhs, rhs, this.file)
|
||||||
}
|
}
|
||||||
readRange (): Substr {
|
|
||||||
this.readBlank()
|
readValueOrThrow (): ValueToken {
|
||||||
const ans = new Substr(this.input, this.p)
|
const value = this.readValue()
|
||||||
if (this.peek() !== '(') return ans
|
assert(value, () => `unexpected token "${this.ellipsis()}", value expected`)
|
||||||
this.read()
|
return value!
|
||||||
this.readValue()
|
|
||||||
this.read(2)
|
|
||||||
this.readValue()
|
|
||||||
ans.end = this.read()
|
|
||||||
return ans
|
|
||||||
}
|
}
|
||||||
readBoolean (): Substr {
|
|
||||||
this.readBlank()
|
readQuoted (): QuotedToken | undefined {
|
||||||
const ans = new Substr(this.input, this.p)
|
this.skipBlank()
|
||||||
if (this.matchWord('true') && !(this.peekType(4) & VARIABLE)) ans.end = this.read(4)
|
const begin = this.p
|
||||||
else if (this.matchWord('false') && !(this.peekType(5) & VARIABLE)) ans.end = this.read(5)
|
if (!(this.peekType() & QUOTE)) return
|
||||||
return ans
|
++this.p
|
||||||
}
|
|
||||||
readQuoted (): Substr {
|
|
||||||
this.readBlank()
|
|
||||||
const ans = new Substr(this.input, this.p)
|
|
||||||
if (!(this.peekType() & QUOTE)) return ans
|
|
||||||
ans.end = this.read()
|
|
||||||
let escaped = false
|
let escaped = false
|
||||||
while (this.p < this.N) {
|
while (this.p < this.N) {
|
||||||
ans.end = this.read()
|
++this.p
|
||||||
if (ans.last() === ans.first() && !escaped) break
|
if (this.input[this.p - 1] === this.input[begin] && !escaped) break
|
||||||
if (escaped) escaped = false
|
if (escaped) escaped = false
|
||||||
else if (ans.last() === '\\') escaped = true
|
else if (this.input[this.p - 1] === '\\') escaped = true
|
||||||
}
|
}
|
||||||
return ans
|
return new QuotedToken(this.input, begin, this.p, this.file)
|
||||||
}
|
}
|
||||||
read (n = 1): number {
|
|
||||||
if (n > 1) this.read(n - 1)
|
readFileName (): WordToken {
|
||||||
const c = this.input[this.p++]
|
const begin = this.p
|
||||||
if (c === '\n') {
|
while (!(this.peekType() & BLANK) && this.peek() !== ',' && this.p < this.N) this.p++
|
||||||
this.line++
|
return new WordToken(this.input, begin, this.p, this.file)
|
||||||
this.col = 1
|
|
||||||
} else {
|
|
||||||
this.col++
|
|
||||||
}
|
|
||||||
return this.p
|
|
||||||
}
|
}
|
||||||
|
|
||||||
matchWord (word: string) {
|
matchWord (word: string) {
|
||||||
for (let i = 0; i < word.length; i++) {
|
for (let i = 0; i < word.length; i++) {
|
||||||
if (word[i] !== this.input[this.p + i]) return false
|
if (word[i] !== this.input[this.p + i]) return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
reverseMatchWord (word: string, buffer?: string) {
|
|
||||||
const str = buffer || this.input
|
reverseMatchWord (word: string) {
|
||||||
const end = buffer === undefined ? this.p : buffer.length
|
|
||||||
for (let i = 0; i < word.length; i++) {
|
for (let i = 0; i < word.length; i++) {
|
||||||
if (word[word.length - 1 - i] !== str[end - 1 - i]) return false
|
if (word[word.length - 1 - i] !== this.input[this.p - 1 - i]) return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
peekType (n = 0) {
|
peekType (n = 0) {
|
||||||
return +TYPES[this.input.charCodeAt(this.p + n)]
|
return TYPES[this.input.charCodeAt(this.p + n)]
|
||||||
}
|
}
|
||||||
|
|
||||||
peek (n = 0) {
|
peek (n = 0) {
|
||||||
return this.input[this.p + n]
|
return this.input[this.p + n]
|
||||||
}
|
}
|
||||||
readBlank () {
|
|
||||||
let ans = ''
|
skipBlank () {
|
||||||
while (this.peekType() & BLANK) ans += this.read()
|
while (this.peekType() & BLANK) ++this.p
|
||||||
return ans
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Token } from '../parser/token'
|
import { Token } from '../tokens/token'
|
||||||
import { TagToken } from '../parser/tag-token'
|
import { DelimitedToken } from '../tokens/delimited-token'
|
||||||
import { HTMLToken } from '../parser/html-token'
|
import { isTagToken, isHTMLToken } from '../util/type-guards'
|
||||||
import { NormalizedFullOptions } from '../liquid-options'
|
import { NormalizedFullOptions } from '../liquid-options'
|
||||||
|
import { TYPES, INLINE_BLANK, BLANK } from '../util/character'
|
||||||
|
|
||||||
export function whiteSpaceCtrl (tokens: Token[], options: NormalizedFullOptions) {
|
export function whiteSpaceCtrl (tokens: Token[], options: NormalizedFullOptions) {
|
||||||
options = { greedy: true, ...options }
|
options = { greedy: true, ...options }
|
||||||
@@ -9,11 +10,12 @@ export function whiteSpaceCtrl (tokens: Token[], options: NormalizedFullOptions)
|
|||||||
|
|
||||||
for (let i = 0; i < tokens.length; i++) {
|
for (let i = 0; i < tokens.length; i++) {
|
||||||
const token = tokens[i]
|
const token = tokens[i]
|
||||||
|
if (!(token instanceof DelimitedToken)) continue
|
||||||
if (!inRaw && token.trimLeft) {
|
if (!inRaw && token.trimLeft) {
|
||||||
trimLeft(tokens[i - 1], options.greedy)
|
trimLeft(tokens[i - 1], options.greedy)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TagToken.is(token)) {
|
if (isTagToken(token)) {
|
||||||
if (token.name === 'raw') inRaw = true
|
if (token.name === 'raw') inRaw = true
|
||||||
else if (token.name === 'endraw') inRaw = false
|
else if (token.name === 'endraw') inRaw = false
|
||||||
}
|
}
|
||||||
@@ -25,15 +27,16 @@ export function whiteSpaceCtrl (tokens: Token[], options: NormalizedFullOptions)
|
|||||||
}
|
}
|
||||||
|
|
||||||
function trimLeft (token: Token, greedy: boolean) {
|
function trimLeft (token: Token, greedy: boolean) {
|
||||||
if (!token || !HTMLToken.is(token)) return
|
if (!token || !isHTMLToken(token)) return
|
||||||
|
|
||||||
const rLeft = greedy ? /\s+$/g : /[\t\r ]*$/g
|
const mask = greedy ? BLANK : INLINE_BLANK
|
||||||
token.content = token.content.replace(rLeft, '')
|
while (TYPES[token.input.charCodeAt(token.end - 1 - token.trimRight)] & mask) token.trimRight++
|
||||||
}
|
}
|
||||||
|
|
||||||
function trimRight (token: Token, greedy: boolean) {
|
function trimRight (token: Token, greedy: boolean) {
|
||||||
if (!token || !HTMLToken.is(token)) return
|
if (!token || !isHTMLToken(token)) return
|
||||||
|
|
||||||
const rRight = greedy ? /^\s+/g : /^[\t\r ]*\n?/g
|
const mask = greedy ? BLANK : INLINE_BLANK
|
||||||
token.content = token.content.replace(rRight, '')
|
while (TYPES[token.input.charCodeAt(token.begin + token.trimLeft)] & mask) token.trimLeft++
|
||||||
|
if (token.input.charAt(token.begin + token.trimLeft) === '\n') token.trimLeft++
|
||||||
}
|
}
|
||||||
|
|||||||
+60
-33
@@ -1,47 +1,87 @@
|
|||||||
|
import { QuotedToken } from '../tokens/quoted-token'
|
||||||
|
import { NumberToken } from '../tokens/number-token'
|
||||||
import { assert } from '../util/assert'
|
import { assert } from '../util/assert'
|
||||||
import { rangeLine } from '../parser/lexical'
|
import { literalValues } from '../util/literal'
|
||||||
import { parseLiteral } from '../parser/literal'
|
import { LiteralToken } from '../tokens/literal-token'
|
||||||
|
import * as TypeGuards from '../util/type-guards'
|
||||||
|
import { Token } from '../tokens/token'
|
||||||
|
import { OperatorToken } from '../tokens/operator-token'
|
||||||
|
import { RangeToken } from '../tokens/range-token'
|
||||||
|
import { parseStringLiteral } from '../parser/parse-string-literal'
|
||||||
import { Context } from '../context/context'
|
import { Context } from '../context/context'
|
||||||
import { range, toValue } from '../util/underscore'
|
import { range, toValue } from '../util/underscore'
|
||||||
import { isOperator, precedence, operatorImpls } from './operator'
|
|
||||||
import { Tokenizer } from '../parser/tokenizer'
|
import { Tokenizer } from '../parser/tokenizer'
|
||||||
|
import { operatorImpls } from '../render/operator'
|
||||||
|
|
||||||
export class Expression {
|
export class Expression {
|
||||||
private operands: any[] = []
|
private operands: any[] = []
|
||||||
private postfix: string[]
|
private postfix: IterableIterator<Token>
|
||||||
|
|
||||||
public constructor (str = '') {
|
public constructor (str: string) {
|
||||||
const tokenizer = new Tokenizer(str)
|
const tokenizer = new Tokenizer(str)
|
||||||
this.postfix = [...toPostfix(tokenizer.readExpression())]
|
this.postfix = toPostfix(tokenizer.readExpression())
|
||||||
}
|
}
|
||||||
public * evaluate (ctx: Context): any {
|
public evaluate (ctx: Context): any {
|
||||||
assert(ctx, 'unable to evaluate: context not defined')
|
|
||||||
|
|
||||||
for (const token of this.postfix) {
|
for (const token of this.postfix) {
|
||||||
if (isOperator(token)) {
|
if (TypeGuards.isOperatorToken(token)) {
|
||||||
const r = this.operands.pop()
|
const r = this.operands.pop()
|
||||||
const l = this.operands.pop()
|
const l = this.operands.pop()
|
||||||
const result = operatorImpls[token](l, r)
|
const result = evalOperatorToken(token, l, r)
|
||||||
this.operands.push(result)
|
this.operands.push(result)
|
||||||
} else if (isRange(token)) {
|
|
||||||
this.operands.push(yield rangeValue(token, ctx))
|
|
||||||
} else {
|
} else {
|
||||||
const literal = parseLiteral(token)
|
this.operands.push(evalToken(token, ctx))
|
||||||
this.operands.push(literal !== undefined ? literal : yield ctx.get(token))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this.operands[0]
|
return this.operands[0]
|
||||||
}
|
}
|
||||||
public * value (ctx: Context) {
|
public * value (ctx: Context) {
|
||||||
return toValue(yield this.evaluate(ctx))
|
return toValue(this.evaluate(ctx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function * toPostfix (tokens: IterableIterator<string>): IterableIterator<string> {
|
export function evalToken (token: Token | undefined, ctx: Context): any {
|
||||||
const ops = []
|
assert(ctx, () => 'unable to evaluate: context not defined')
|
||||||
|
if (TypeGuards.isPropertyAccessToken(token)) {
|
||||||
|
const variable = token.variable.getText()
|
||||||
|
const props: string[] = token.props.map(prop => evalToken(prop, ctx))
|
||||||
|
return ctx.get([variable, ...props])
|
||||||
|
}
|
||||||
|
if (TypeGuards.isRangeToken(token)) return evalRangeToken(token, ctx)
|
||||||
|
if (TypeGuards.isLiteralToken(token)) return evalLiteralToken(token)
|
||||||
|
if (TypeGuards.isNumberToken(token)) return evalNumberToken(token)
|
||||||
|
if (TypeGuards.isWordToken(token)) return token.getText()
|
||||||
|
if (TypeGuards.isQuotedToken(token)) return evalQuotedToken(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
function evalNumberToken (token: NumberToken) {
|
||||||
|
const str = token.whole.content + '.' + (token.decimal ? token.decimal.content : '')
|
||||||
|
return Number(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function evalQuotedToken (token: QuotedToken) {
|
||||||
|
return parseStringLiteral(token.getText())
|
||||||
|
}
|
||||||
|
|
||||||
|
function evalOperatorToken (token: OperatorToken, lhs: any, rhs: any) {
|
||||||
|
const impl = operatorImpls[token.operator]
|
||||||
|
return impl(lhs, rhs)
|
||||||
|
}
|
||||||
|
|
||||||
|
function evalLiteralToken (token: LiteralToken) {
|
||||||
|
return literalValues[token.literal]
|
||||||
|
}
|
||||||
|
|
||||||
|
function evalRangeToken (token: RangeToken, ctx: Context) {
|
||||||
|
const low: number = evalToken(token.lhs, ctx)
|
||||||
|
const high: number = evalToken(token.rhs, ctx)
|
||||||
|
return range(+low, +high + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function * toPostfix (tokens: IterableIterator<Token>): IterableIterator<Token> {
|
||||||
|
const ops: OperatorToken[] = []
|
||||||
for (const token of tokens) {
|
for (const token of tokens) {
|
||||||
if (isOperator(token)) {
|
if (TypeGuards.isOperatorToken(token)) {
|
||||||
while (ops.length && precedence[ops[ops.length - 1]] > precedence[token]) {
|
while (ops.length && ops[ops.length - 1].getPrecedence() > token.getPrecedence()) {
|
||||||
yield ops.pop()!
|
yield ops.pop()!
|
||||||
}
|
}
|
||||||
ops.push(token)
|
ops.push(token)
|
||||||
@@ -51,16 +91,3 @@ function * toPostfix (tokens: IterableIterator<string>): IterableIterator<string
|
|||||||
yield ops.pop()!
|
yield ops.pop()!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function * rangeValue (token: string, ctx: Context) {
|
|
||||||
let match
|
|
||||||
if ((match = token.match(rangeLine))) {
|
|
||||||
const low = yield new Expression(match[1]).value(ctx)
|
|
||||||
const high = yield new Expression(match[2]).value(ctx)
|
|
||||||
return range(+low, +high + 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isRange (str: string) {
|
|
||||||
return rangeLine.test(str)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,18 +2,6 @@ import { isComparable } from '../drop/icomparable'
|
|||||||
import { isFunction } from '../util/underscore'
|
import { isFunction } from '../util/underscore'
|
||||||
import { isTruthy } from '../render/boolean'
|
import { isTruthy } from '../render/boolean'
|
||||||
|
|
||||||
export const precedence = {
|
|
||||||
'==': 1,
|
|
||||||
'!=': 1,
|
|
||||||
'>': 1,
|
|
||||||
'<': 1,
|
|
||||||
'>=': 1,
|
|
||||||
'<=': 1,
|
|
||||||
'contains': 1,
|
|
||||||
'and': 0,
|
|
||||||
'or': 0
|
|
||||||
}
|
|
||||||
|
|
||||||
export const operatorImpls: {[key: string]: (lhs: any, rhs: any) => boolean} = {
|
export const operatorImpls: {[key: string]: (lhs: any, rhs: any) => boolean} = {
|
||||||
'==': (l: any, r: any) => {
|
'==': (l: any, r: any) => {
|
||||||
if (isComparable(l)) return l.equals(r)
|
if (isComparable(l)) return l.equals(r)
|
||||||
@@ -51,7 +39,3 @@ export const operatorImpls: {[key: string]: (lhs: any, rhs: any) => boolean} = {
|
|||||||
'and': (l: any, r: any) => isTruthy(l) && isTruthy(r),
|
'and': (l: any, r: any) => isTruthy(l) && isTruthy(r),
|
||||||
'or': (l: any, r: any) => isTruthy(l) || isTruthy(r)
|
'or': (l: any, r: any) => isTruthy(l) || isTruthy(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isOperator (token: string) {
|
|
||||||
return precedence.hasOwnProperty(token)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export class FilterMap {
|
|||||||
|
|
||||||
get (name: string) {
|
get (name: string) {
|
||||||
const impl = this.impls[name]
|
const impl = this.impls[name]
|
||||||
assert(impl || !this.strictFilters, `undefined filter: ${name}`)
|
assert(impl || !this.strictFilters, () => `undefined filter: ${name}`)
|
||||||
return impl
|
return impl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Expression } from '../../render/expression'
|
import { evalToken } from '../../render/expression'
|
||||||
import { Context } from '../../context/context'
|
import { Context } from '../../context/context'
|
||||||
import { identify } from '../../util/underscore'
|
import { identify } from '../../util/underscore'
|
||||||
import { FilterImplOptions } from './filter-impl-options'
|
import { FilterImplOptions } from './filter-impl-options'
|
||||||
@@ -16,9 +16,9 @@ export class Filter {
|
|||||||
}
|
}
|
||||||
public * render (value: any, context: Context) {
|
public * render (value: any, context: Context) {
|
||||||
const argv: any[] = []
|
const argv: any[] = []
|
||||||
for (const arg of this.args) {
|
for (const arg of this.args as FilterArg[]) {
|
||||||
if (isKeyValuePair(arg)) argv.push([arg[0], yield new Expression(arg[1]).evaluate(context)])
|
if (isKeyValuePair(arg)) argv.push([arg[0], yield evalToken(arg[1], context)])
|
||||||
else argv.push(yield new Expression(arg).evaluate(context))
|
else argv.push(yield evalToken(arg, context))
|
||||||
}
|
}
|
||||||
return this.impl.apply({ context }, [value, ...argv])
|
return this.impl.apply({ context }, [value, ...argv])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { TemplateImpl } from '../template/template-impl'
|
import { TemplateImpl } from '../template/template-impl'
|
||||||
import { Template } from '../template/template'
|
import { Template } from '../template/template'
|
||||||
import { HTMLToken } from '../parser/html-token'
|
import { HTMLToken } from '../tokens/html-token'
|
||||||
import { Context } from '../context/context'
|
import { Context } from '../context/context'
|
||||||
import { Emitter } from '../render/emitter'
|
import { Emitter } from '../render/emitter'
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ export class HTML extends TemplateImpl<HTMLToken> implements Template {
|
|||||||
private str: string
|
private str: string
|
||||||
public constructor (token: HTMLToken) {
|
public constructor (token: HTMLToken) {
|
||||||
super(token)
|
super(token)
|
||||||
this.str = token.content
|
this.str = token.getContent()
|
||||||
}
|
}
|
||||||
public * render (ctx: Context, emitter: Emitter): IterableIterator<void> {
|
public * render (ctx: Context, emitter: Emitter): IterableIterator<void> {
|
||||||
emitter.write(this.str)
|
emitter.write(this.str)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { TemplateImpl } from '../template/template-impl'
|
|||||||
import { Template } from '../template/template'
|
import { Template } from '../template/template'
|
||||||
import { Context } from '../context/context'
|
import { Context } from '../context/context'
|
||||||
import { Emitter } from '../render/emitter'
|
import { Emitter } from '../render/emitter'
|
||||||
import { OutputToken } from '../parser/output-token'
|
import { OutputToken } from '../tokens/output-token'
|
||||||
|
|
||||||
export class Output extends TemplateImpl<OutputToken> implements Template {
|
export class Output extends TemplateImpl<OutputToken> implements Template {
|
||||||
private value: Value
|
private value: Value
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Expression } from '../../render/expression'
|
import { evalToken } from '../../render/expression'
|
||||||
import { Context } from '../../context/context'
|
import { Context } from '../../context/context'
|
||||||
import { Tokenizer } from '../../parser/tokenizer'
|
import { Tokenizer } from '../../parser/tokenizer'
|
||||||
|
|
||||||
@@ -11,17 +11,17 @@ import { Tokenizer } from '../../parser/tokenizer'
|
|||||||
* hash['reversed'] === undefined
|
* hash['reversed'] === undefined
|
||||||
*/
|
*/
|
||||||
export class Hash {
|
export class Hash {
|
||||||
[key: string]: any
|
hash: { [key: string]: any } = {}
|
||||||
constructor (markup: string) {
|
constructor (markup: string) {
|
||||||
const tokenizer = new Tokenizer(markup)
|
const tokenizer = new Tokenizer(markup)
|
||||||
for (const [name, value] of tokenizer.readHashes()) {
|
for (const hash of tokenizer.readHashes()) {
|
||||||
this[name] = value
|
this.hash[hash.name.content] = hash.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
* render (ctx: Context) {
|
* render (ctx: Context) {
|
||||||
const hash = {}
|
const hash = {}
|
||||||
for (const key of Object.keys(this)) {
|
for (const key of Object.keys(this.hash)) {
|
||||||
hash[key] = yield new Expression(this[key]).evaluate(ctx)
|
hash[key] = evalToken(this.hash[key], ctx)
|
||||||
}
|
}
|
||||||
return hash
|
return hash
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Context } from '../../context/context'
|
import { Context } from '../../context/context'
|
||||||
import { TagToken } from '../../parser/tag-token'
|
import { TagToken } from '../../tokens/tag-token'
|
||||||
import { Token } from '../../parser/token'
|
import { TopLevelToken } from '../../tokens/toplevel-token'
|
||||||
import { TagImpl } from './tag-impl'
|
import { TagImpl } from './tag-impl'
|
||||||
import { Hash } from '../../template/tag/hash'
|
import { Hash } from '../../template/tag/hash'
|
||||||
import { Emitter } from '../../render/emitter'
|
import { Emitter } from '../../render/emitter'
|
||||||
|
|
||||||
export interface TagImplOptions {
|
export interface TagImplOptions {
|
||||||
parse?: (this: TagImpl, token: TagToken, remainingTokens: Token[]) => void;
|
parse?: (this: TagImpl, token: TagToken, remainingTokens: TopLevelToken[]) => void;
|
||||||
render: (this: TagImpl, ctx: Context, emitter: Emitter, hash: Hash) => any;
|
render: (this: TagImpl, ctx: Context, emitter: Emitter, hash: Hash) => any;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export class TagMap {
|
|||||||
|
|
||||||
get (name: string) {
|
get (name: string) {
|
||||||
const impl = this.impls[name]
|
const impl = this.impls[name]
|
||||||
assert(impl, `tag "${name}" not found`)
|
assert(impl, () => `tag "${name}" not found`)
|
||||||
return impl
|
return impl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { isFunction } from '../../util/underscore'
|
import { isFunction } from '../../util/underscore'
|
||||||
import { Liquid } from '../../liquid'
|
import { Liquid } from '../../liquid'
|
||||||
import { TemplateImpl } from '../../template/template-impl'
|
import { TemplateImpl } from '../../template/template-impl'
|
||||||
import { Emitter, Hash, Context, TagImplOptions, TagToken, Template, Token } from '../../types'
|
import { Emitter, Hash, Context, TagImplOptions, TagToken, Template, TopLevelToken } from '../../types'
|
||||||
import { TagImpl } from './tag-impl'
|
import { TagImpl } from './tag-impl'
|
||||||
|
|
||||||
export class Tag extends TemplateImpl<TagToken> implements Template {
|
export class Tag extends TemplateImpl<TagToken> implements Template {
|
||||||
@@ -9,7 +9,7 @@ export class Tag extends TemplateImpl<TagToken> implements Template {
|
|||||||
private impl: TagImpl
|
private impl: TagImpl
|
||||||
private static impls: { [key: string]: TagImplOptions } = {}
|
private static impls: { [key: string]: TagImplOptions } = {}
|
||||||
|
|
||||||
public constructor (token: TagToken, tokens: Token[], liquid: Liquid) {
|
public constructor (token: TagToken, tokens: TopLevelToken[], liquid: Liquid) {
|
||||||
super(token)
|
super(token)
|
||||||
this.name = token.name
|
this.name = token.name
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Context } from '../context/context'
|
import { Context } from '../context/context'
|
||||||
import { Token } from '../parser/token'
|
import { Token } from '../tokens/token'
|
||||||
import { Emitter } from '../render/emitter'
|
import { Emitter } from '../render/emitter'
|
||||||
|
|
||||||
export interface Template {
|
export interface Template {
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
import { Expression } from '../render/expression'
|
import { evalToken } from '../render/expression'
|
||||||
import { Tokenizer } from '../parser/tokenizer'
|
import { Tokenizer } from '../parser/tokenizer'
|
||||||
import { FilterMap } from '../template/filter/filter-map'
|
import { FilterMap } from '../template/filter/filter-map'
|
||||||
import { Filter } from './filter/filter'
|
import { Filter } from './filter/filter'
|
||||||
import { Context } from '../context/context'
|
import { Context } from '../context/context'
|
||||||
|
import { ValueToken } from '../tokens/value-token'
|
||||||
|
|
||||||
export class Value {
|
export class Value {
|
||||||
public readonly filters: Filter[] = []
|
public readonly filters: Filter[] = []
|
||||||
public readonly initial: string
|
public readonly initial?: ValueToken
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param str the value to be valuated, eg.: "foobar" | truncate: 3
|
* @param str the value to be valuated, eg.: "foobar" | truncate: 3
|
||||||
*/
|
*/
|
||||||
public constructor (str: string, private readonly filterMap: FilterMap) {
|
public constructor (str: string, private readonly filterMap: FilterMap) {
|
||||||
const tokenizer = new Tokenizer(str)
|
const tokenizer = new Tokenizer(str)
|
||||||
this.initial = tokenizer.readValue().toString()
|
this.initial = tokenizer.readValue()
|
||||||
this.filters = tokenizer.readFilterTokens().map(({ name, args }) => new Filter(name, this.filterMap.get(name), args))
|
this.filters = tokenizer.readFilters().map(({ name, args }) => new Filter(name, this.filterMap.get(name), args))
|
||||||
}
|
}
|
||||||
public * value (ctx: Context) {
|
public * value (ctx: Context) {
|
||||||
let val = yield new Expression(this.initial).evaluate(ctx)
|
let val = yield evalToken(this.initial, ctx)
|
||||||
for (const filter of this.filters) {
|
for (const filter of this.filters) {
|
||||||
val = yield filter.render(val, ctx)
|
val = yield filter.render(val, ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
import { Token } from './token'
|
import { Token } from './token'
|
||||||
|
import { TokenKind } from '../parser/token-kind'
|
||||||
import { last } from '../util/underscore'
|
import { last } from '../util/underscore'
|
||||||
|
|
||||||
export class DelimitedToken extends Token {
|
export abstract class DelimitedToken extends Token {
|
||||||
|
public trimLeft = false
|
||||||
|
public trimRight = false
|
||||||
|
public content: string
|
||||||
public constructor (
|
public constructor (
|
||||||
raw: string,
|
kind: TokenKind,
|
||||||
content: string,
|
content: string,
|
||||||
input: string,
|
input: string,
|
||||||
line: number,
|
begin: number,
|
||||||
pos: number,
|
end: number,
|
||||||
trimLeft: boolean,
|
trimLeft: boolean,
|
||||||
trimRight: boolean,
|
trimRight: boolean,
|
||||||
file?: string
|
file?: string
|
||||||
) {
|
) {
|
||||||
super(raw, input, line, pos, file)
|
super(kind, input, begin, end, file)
|
||||||
|
this.content = this.getText()
|
||||||
const tl = content[0] === '-'
|
const tl = content[0] === '-'
|
||||||
const tr = last(content) === '-'
|
const tr = last(content) === '-'
|
||||||
this.content = content
|
this.content = content
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
import { Token } from './token'
|
import { Token } from './token'
|
||||||
import { FilterArg } from './filter-arg'
|
import { FilterArg } from '../parser/filter-arg'
|
||||||
|
import { TokenKind } from '../parser/token-kind'
|
||||||
|
|
||||||
export class FilterToken extends Token {
|
export class FilterToken extends Token {
|
||||||
public constructor (
|
public constructor (
|
||||||
public name: string,
|
public name: string,
|
||||||
public args: FilterArg[],
|
public args: FilterArg[],
|
||||||
raw: string,
|
|
||||||
input: string,
|
input: string,
|
||||||
line: number,
|
begin: number,
|
||||||
col: number,
|
end: number,
|
||||||
file?: string
|
file?: string
|
||||||
) {
|
) {
|
||||||
super(raw, input, line, col, file)
|
super(TokenKind.Filter, input, begin, end, file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { Token } from './token'
|
||||||
|
import { ValueToken } from './value-token'
|
||||||
|
import { WordToken } from './word-token'
|
||||||
|
import { TokenKind } from '../parser/token-kind'
|
||||||
|
|
||||||
|
export class HashToken extends Token {
|
||||||
|
constructor (
|
||||||
|
public input: string,
|
||||||
|
public begin: number,
|
||||||
|
public end: number,
|
||||||
|
public name: WordToken,
|
||||||
|
public value?: ValueToken,
|
||||||
|
public file?: string
|
||||||
|
) {
|
||||||
|
super(TokenKind.Hash, input, begin, end, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Token } from './token'
|
||||||
|
import { TokenKind } from '../parser/token-kind'
|
||||||
|
|
||||||
|
export class HTMLToken extends Token {
|
||||||
|
trimLeft = 0
|
||||||
|
trimRight = 0
|
||||||
|
constructor (
|
||||||
|
public input: string,
|
||||||
|
public begin: number,
|
||||||
|
public end: number,
|
||||||
|
public file?: string
|
||||||
|
) {
|
||||||
|
super(TokenKind.HTML, input, begin, end, file)
|
||||||
|
}
|
||||||
|
public getContent () {
|
||||||
|
return this.input.slice(this.begin + this.trimLeft, this.end - this.trimRight)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Token } from './token'
|
||||||
|
import { TokenKind } from '../parser/token-kind'
|
||||||
|
|
||||||
|
export class LiteralToken extends Token {
|
||||||
|
public literal: string
|
||||||
|
public constructor (
|
||||||
|
public input: string,
|
||||||
|
public begin: number,
|
||||||
|
public end: number,
|
||||||
|
public file?: string
|
||||||
|
) {
|
||||||
|
super(TokenKind.Literal, input, begin, end, file)
|
||||||
|
this.literal = this.getText()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Token } from './token'
|
||||||
|
import { WordToken } from './word-token'
|
||||||
|
import { TokenKind } from '../parser/token-kind'
|
||||||
|
|
||||||
|
export class NumberToken extends Token {
|
||||||
|
constructor (
|
||||||
|
public whole: WordToken,
|
||||||
|
public decimal?: WordToken
|
||||||
|
) {
|
||||||
|
super(TokenKind.Number, whole.input, whole.begin, decimal ? decimal.end : whole.end, whole.file)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Token } from './token'
|
||||||
|
import { TokenKind } from '../parser/token-kind'
|
||||||
|
|
||||||
|
export const precedence = {
|
||||||
|
'==': 1,
|
||||||
|
'!=': 1,
|
||||||
|
'>': 1,
|
||||||
|
'<': 1,
|
||||||
|
'>=': 1,
|
||||||
|
'<=': 1,
|
||||||
|
'contains': 1,
|
||||||
|
'and': 0,
|
||||||
|
'or': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OperatorToken extends Token {
|
||||||
|
public operator: string
|
||||||
|
public constructor (
|
||||||
|
public input: string,
|
||||||
|
public begin: number,
|
||||||
|
public end: number,
|
||||||
|
public file?: string
|
||||||
|
) {
|
||||||
|
super(TokenKind.Operator, input, begin, end, file)
|
||||||
|
this.operator = this.getText()
|
||||||
|
}
|
||||||
|
getPrecedence () {
|
||||||
|
return precedence[this.getText()]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { DelimitedToken } from './delimited-token'
|
||||||
|
import { NormalizedFullOptions } from '../liquid-options'
|
||||||
|
import { TokenKind } from '../parser/token-kind'
|
||||||
|
|
||||||
|
export class OutputToken extends DelimitedToken {
|
||||||
|
public constructor (
|
||||||
|
input: string,
|
||||||
|
begin: number,
|
||||||
|
end: number,
|
||||||
|
options: NormalizedFullOptions,
|
||||||
|
file?: string
|
||||||
|
) {
|
||||||
|
const { trimOutputLeft, trimOutputRight, outputDelimiterLeft, outputDelimiterRight } = options
|
||||||
|
const value = input.slice(begin + outputDelimiterLeft.length, end - outputDelimiterRight.length)
|
||||||
|
super(TokenKind.Output, value, input, begin, end, trimOutputLeft, trimOutputRight, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Token } from './token'
|
||||||
|
import { WordToken } from './word-token'
|
||||||
|
import { QuotedToken } from './quoted-token'
|
||||||
|
import { TokenKind } from '../parser/token-kind'
|
||||||
|
|
||||||
|
export class PropertyAccessToken extends Token {
|
||||||
|
constructor (
|
||||||
|
public variable: WordToken,
|
||||||
|
public props: (WordToken | QuotedToken | PropertyAccessToken)[],
|
||||||
|
end: number
|
||||||
|
) {
|
||||||
|
super(TokenKind.PropertyAccess, variable.input, variable.begin, end, variable.file)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Token } from './token'
|
||||||
|
import { TokenKind } from '../parser/token-kind'
|
||||||
|
|
||||||
|
export class QuotedToken extends Token {
|
||||||
|
constructor (
|
||||||
|
public input: string,
|
||||||
|
public begin: number,
|
||||||
|
public end: number,
|
||||||
|
public file?: string
|
||||||
|
) {
|
||||||
|
super(TokenKind.Quoted, input, begin, end, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { Token } from './token'
|
||||||
|
import { ValueToken } from './value-token'
|
||||||
|
import { TokenKind } from '../parser/token-kind'
|
||||||
|
|
||||||
|
export class RangeToken extends Token {
|
||||||
|
constructor (
|
||||||
|
public input: string,
|
||||||
|
public begin: number,
|
||||||
|
public end: number,
|
||||||
|
public lhs: ValueToken,
|
||||||
|
public rhs: ValueToken,
|
||||||
|
public file?: string
|
||||||
|
) {
|
||||||
|
super(TokenKind.Range, input, begin, end, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { DelimitedToken } from './delimited-token'
|
||||||
|
import { BLANK, TYPES, VARIABLE } from '../util/character'
|
||||||
|
import { TokenizationError } from '../util/error'
|
||||||
|
import { NormalizedFullOptions } from '../liquid-options'
|
||||||
|
import { TokenKind } from '../parser/token-kind'
|
||||||
|
|
||||||
|
export class TagToken extends DelimitedToken {
|
||||||
|
public name: string
|
||||||
|
public args: string
|
||||||
|
public constructor (
|
||||||
|
input: string,
|
||||||
|
begin: number,
|
||||||
|
end: number,
|
||||||
|
options: NormalizedFullOptions,
|
||||||
|
file?: string
|
||||||
|
) {
|
||||||
|
const { trimTagLeft, trimTagRight, tagDelimiterLeft, tagDelimiterRight } = options
|
||||||
|
const value = input.slice(begin + tagDelimiterLeft.length, end - tagDelimiterRight.length)
|
||||||
|
super(TokenKind.Tag, value, input, begin, end, trimTagLeft, trimTagRight, file)
|
||||||
|
|
||||||
|
let nameEnd = 0
|
||||||
|
while (TYPES[this.content.charCodeAt(nameEnd)] & VARIABLE) nameEnd++
|
||||||
|
this.name = this.content.slice(0, nameEnd)
|
||||||
|
if (!this.name) throw new TokenizationError(`illegal tag syntax`, this)
|
||||||
|
|
||||||
|
let argsBegin = nameEnd
|
||||||
|
while (TYPES[this.content.charCodeAt(argsBegin)] & BLANK) argsBegin++
|
||||||
|
this.args = this.content.slice(argsBegin)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { TokenKind } from '../parser/token-kind'
|
||||||
|
|
||||||
|
export abstract class Token {
|
||||||
|
public constructor (
|
||||||
|
public kind: TokenKind,
|
||||||
|
public input: string,
|
||||||
|
public begin: number,
|
||||||
|
public end: number,
|
||||||
|
public file?: string
|
||||||
|
) {}
|
||||||
|
public getText () {
|
||||||
|
return this.input.slice(this.begin, this.end)
|
||||||
|
}
|
||||||
|
public getPosition () {
|
||||||
|
let [row, col] = [1, 1]
|
||||||
|
for (let i = 0; i < this.begin; i++) {
|
||||||
|
if (this.input[i] === '\n') {
|
||||||
|
row++
|
||||||
|
col = 1
|
||||||
|
} else col++
|
||||||
|
}
|
||||||
|
return [row, col]
|
||||||
|
}
|
||||||
|
public size () {
|
||||||
|
return this.end - this.begin
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { TagToken } from './tag-token'
|
||||||
|
import { HTMLToken } from './html-token'
|
||||||
|
import { OutputToken } from './output-token'
|
||||||
|
|
||||||
|
export type TopLevelToken = TagToken | OutputToken | HTMLToken
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { RangeToken } from './range-token'
|
||||||
|
import { LiteralToken } from './literal-token'
|
||||||
|
import { QuotedToken } from './quoted-token'
|
||||||
|
import { PropertyAccessToken } from './property-access-token'
|
||||||
|
|
||||||
|
export type ValueToken = RangeToken | LiteralToken | QuotedToken | PropertyAccessToken
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Token } from './token'
|
||||||
|
import { NUMBER, TYPES, SIGN } from '../util/character'
|
||||||
|
import { TokenKind } from '../parser/token-kind'
|
||||||
|
|
||||||
|
// a word can be an identifier, a number, a keyword or a single-word-literal
|
||||||
|
export class WordToken extends Token {
|
||||||
|
public content: string
|
||||||
|
constructor (
|
||||||
|
public input: string,
|
||||||
|
public begin: number,
|
||||||
|
public end: number,
|
||||||
|
public file?: string
|
||||||
|
) {
|
||||||
|
super(TokenKind.Word, input, begin, end, file)
|
||||||
|
this.content = this.getText()
|
||||||
|
}
|
||||||
|
isNumber (allowSign = false) {
|
||||||
|
const begin = allowSign && TYPES[this.input.charCodeAt(this.begin)] & SIGN
|
||||||
|
? this.begin + 1
|
||||||
|
: this.begin
|
||||||
|
for (let i = begin; i < this.end; i++) {
|
||||||
|
if (!(TYPES[this.input.charCodeAt(i)] & NUMBER)) return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-2
@@ -1,13 +1,18 @@
|
|||||||
|
import * as TypeGuards from './util/type-guards'
|
||||||
|
export { TypeGuards }
|
||||||
export { ParseError, TokenizationError, AssertionError } from './util/error'
|
export { ParseError, TokenizationError, AssertionError } from './util/error'
|
||||||
|
export { assert } from './util/assert'
|
||||||
export { Drop } from './drop/drop'
|
export { Drop } from './drop/drop'
|
||||||
export { Emitter } from './render/emitter'
|
export { Emitter } from './render/emitter'
|
||||||
export { Expression } from './render/expression'
|
export { Expression } from './render/expression'
|
||||||
export { isFalsy, isTruthy } from './render/boolean'
|
export { isFalsy, isTruthy } from './render/boolean'
|
||||||
export { TagToken } from './parser/tag-token'
|
export { TagToken } from './tokens/tag-token'
|
||||||
export { Context } from './context/context'
|
export { Context } from './context/context'
|
||||||
export { Template } from './template/template'
|
export { Template } from './template/template'
|
||||||
export { TagImplOptions } from './template/tag/tag-impl-options'
|
export { TagImplOptions } from './template/tag/tag-impl-options'
|
||||||
export { ParseStream } from './parser/parse-stream'
|
export { ParseStream } from './parser/parse-stream'
|
||||||
export { Token } from './parser/token'
|
export { Token } from './tokens/token'
|
||||||
|
export { TopLevelToken } from './tokens/toplevel-token'
|
||||||
export { Tokenizer } from './parser/tokenizer'
|
export { Tokenizer } from './parser/tokenizer'
|
||||||
export { Hash } from './template/tag/hash'
|
export { Hash } from './template/tag/hash'
|
||||||
|
export { evalToken, evalQuotedToken } from './render/expression'
|
||||||
|
|||||||
+3
-3
@@ -1,8 +1,8 @@
|
|||||||
import { AssertionError } from './error'
|
import { AssertionError } from './error'
|
||||||
|
|
||||||
export function assert <T> (predicate: T | null | undefined, message?: string) {
|
export function assert <T> (predicate: T | null | undefined, message?: () => string) {
|
||||||
if (!predicate) {
|
if (!predicate) {
|
||||||
message = message || `expect ${predicate} to be true`
|
const msg = message ? message() : `expect ${predicate} to be true`
|
||||||
throw new AssertionError(message)
|
throw new AssertionError(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { isFunction } from './underscore'
|
import { isFunction } from './underscore'
|
||||||
|
|
||||||
type resolver = (x?: any) => Thenable
|
type resolver = (x?: any) => any
|
||||||
|
|
||||||
interface Thenable {
|
interface Thenable {
|
||||||
then (resolve: resolver, reject?: resolver): Thenable;
|
then (resolve: resolver, reject?: resolver): Thenable;
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// bitmask character types to boost performance
|
||||||
|
// generated by bin/char-types.js
|
||||||
|
export const TYPES = [0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 4, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 2, 8, 0, 0, 0, 0, 8, 0, 0, 0, 64, 0, 65, 0, 0, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 0, 0, 2, 2, 2, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0]
|
||||||
|
export const VARIABLE = 1
|
||||||
|
export const OPERATOR = 2
|
||||||
|
export const BLANK = 4
|
||||||
|
export const QUOTE = 8
|
||||||
|
export const INLINE_BLANK = 16
|
||||||
|
export const NUMBER = 32
|
||||||
|
export const SIGN = 64
|
||||||
+7
-5
@@ -1,5 +1,5 @@
|
|||||||
import * as _ from './underscore'
|
import * as _ from './underscore'
|
||||||
import { Token } from '../parser/token'
|
import { Token } from '../tokens/token'
|
||||||
import { Template } from '../template/template'
|
import { Template } from '../template/template'
|
||||||
|
|
||||||
abstract class LiquidError extends Error {
|
abstract class LiquidError extends Error {
|
||||||
@@ -57,14 +57,15 @@ export class AssertionError extends Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function mkContext (token: Token) {
|
function mkContext (token: Token) {
|
||||||
|
const [line] = token.getPosition()
|
||||||
const lines = token.input.split('\n')
|
const lines = token.input.split('\n')
|
||||||
const begin = Math.max(token.line - 2, 1)
|
const begin = Math.max(line - 2, 1)
|
||||||
const end = Math.min(token.line + 3, lines.length)
|
const end = Math.min(line + 3, lines.length)
|
||||||
|
|
||||||
const context = _
|
const context = _
|
||||||
.range(begin, end + 1)
|
.range(begin, end + 1)
|
||||||
.map(lineNumber => {
|
.map(lineNumber => {
|
||||||
const indicator = (lineNumber === token.line) ? '>> ' : ' '
|
const indicator = (lineNumber === line) ? '>> ' : ' '
|
||||||
const num = _.padStart(String(lineNumber), String(end).length)
|
const num = _.padStart(String(lineNumber), String(end).length)
|
||||||
const text = lines[lineNumber - 1]
|
const text = lines[lineNumber - 1]
|
||||||
return `${indicator}${num}| ${text}`
|
return `${indicator}${num}| ${text}`
|
||||||
@@ -76,6 +77,7 @@ function mkContext (token: Token) {
|
|||||||
|
|
||||||
function mkMessage (msg: string, token: Token) {
|
function mkMessage (msg: string, token: Token) {
|
||||||
if (token.file) msg += `, file:${token.file}`
|
if (token.file) msg += `, file:${token.file}`
|
||||||
msg += `, line:${token.line}, col:${token.col}`
|
const [line, col] = token.getPosition()
|
||||||
|
msg += `, line:${line}, col:${col}`
|
||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { NullDrop } from '../drop/null-drop'
|
||||||
|
import { EmptyDrop } from '../drop/empty-drop'
|
||||||
|
import { BlankDrop } from '../drop/blank-drop'
|
||||||
|
|
||||||
|
export const literalValues = {
|
||||||
|
'true': true,
|
||||||
|
'false': false,
|
||||||
|
'nil': new NullDrop(),
|
||||||
|
'null': new NullDrop(),
|
||||||
|
'empty': new EmptyDrop(),
|
||||||
|
'blank': new BlankDrop()
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { OperatorToken } from '../tokens/operator-token'
|
||||||
|
import { WordToken } from '../tokens/word-token'
|
||||||
|
import { TagToken } from '../tokens/tag-token'
|
||||||
|
import { HTMLToken } from '../tokens/html-token'
|
||||||
|
import { OutputToken } from '../tokens/output-token'
|
||||||
|
import { PropertyAccessToken } from '../tokens/property-access-token'
|
||||||
|
import { LiteralToken } from '../tokens/literal-token'
|
||||||
|
import { QuotedToken } from '../tokens/quoted-token'
|
||||||
|
import { NumberToken } from '../tokens/number-token'
|
||||||
|
import { RangeToken } from '../tokens/range-token'
|
||||||
|
import { TokenKind } from '../parser/token-kind'
|
||||||
|
|
||||||
|
export function isOperatorToken (val: any): val is OperatorToken {
|
||||||
|
return getKind(val) === TokenKind.Operator
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isHTMLToken (val: any): val is HTMLToken {
|
||||||
|
return getKind(val) === TokenKind.HTML
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isOutputToken (val: any): val is OutputToken {
|
||||||
|
return getKind(val) === TokenKind.Output
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isTagToken (val: any): val is TagToken {
|
||||||
|
return getKind(val) === TokenKind.Tag
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isQuotedToken (val: any): val is QuotedToken {
|
||||||
|
return getKind(val) === TokenKind.Quoted
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isLiteralToken (val: any): val is LiteralToken {
|
||||||
|
return getKind(val) === TokenKind.Literal
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isNumberToken (val: any): val is NumberToken {
|
||||||
|
return getKind(val) === TokenKind.Number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPropertyAccessToken (val: any): val is PropertyAccessToken {
|
||||||
|
return getKind(val) === TokenKind.PropertyAccess
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isWordToken (val: any): val is WordToken {
|
||||||
|
return getKind(val) === TokenKind.Word
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isRangeToken (val: any): val is RangeToken {
|
||||||
|
return getKind(val) === TokenKind.Range
|
||||||
|
}
|
||||||
|
|
||||||
|
function getKind (val: any) {
|
||||||
|
return val ? val.kind : -1
|
||||||
|
}
|
||||||
@@ -6,6 +6,6 @@ describe('#evalValueSync()', function () {
|
|||||||
beforeEach(() => { engine = new Liquid() })
|
beforeEach(() => { engine = new Liquid() })
|
||||||
|
|
||||||
it('should throw when scope undefined', async function () {
|
it('should throw when scope undefined', async function () {
|
||||||
return expect(() => engine.evalValueSync('{{"foo"}}', null as any)).to.throw(/context not defined/)
|
return expect(() => engine.evalValueSync('foo', null as any)).to.throw(/context not defined/)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { Liquid } from '../..'
|
import { Liquid } from '../..'
|
||||||
|
|
||||||
describe('.evalValue()', function () {
|
describe('#evalValue()', function () {
|
||||||
var engine: Liquid
|
var engine: Liquid
|
||||||
beforeEach(() => { engine = new Liquid() })
|
beforeEach(() => { engine = new Liquid() })
|
||||||
|
|
||||||
it('should throw when scope undefined', async function () {
|
it('should throw when scope undefined', async function () {
|
||||||
return expect(engine.evalValue('{{"foo"}}', null as any)).to.be.rejectedWith(/context not defined/)
|
return expect(engine.evalValue('"foo"', null as any)).to.be.rejectedWith(/context not defined/)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,11 +6,6 @@ use(chaiAsPromised)
|
|||||||
|
|
||||||
describe('tags/decrement', function () {
|
describe('tags/decrement', function () {
|
||||||
const liquid = new Liquid()
|
const liquid = new Liquid()
|
||||||
it('should throw when variable expression illegal', function () {
|
|
||||||
const src = '{% decrement / %}{{var}}'
|
|
||||||
const ctx = {}
|
|
||||||
return expect(liquid.parseAndRender(src, ctx)).to.be.rejectedWith(/illegal/)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should decrement undefined variable', async function () {
|
it('should decrement undefined variable', async function () {
|
||||||
const src = '{% decrement var %}{% decrement var %}{% decrement var %}'
|
const src = '{% decrement var %}{% decrement var %}{% decrement var %}'
|
||||||
|
|||||||
@@ -46,6 +46,12 @@ describe('tags/for', function () {
|
|||||||
.to.be.rejectedWith(/tag .* not closed/)
|
.to.be.rejectedWith(/tag .* not closed/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should reject when for in not found', function () {
|
||||||
|
const src = '{%for c alpha%}{{c}}'
|
||||||
|
return expect(liquid.parseAndRender(src, scope))
|
||||||
|
.to.be.rejectedWith('illegal tag: {%for c alpha%}, line:1, col:1')
|
||||||
|
})
|
||||||
|
|
||||||
it('should reject when inner templates rejected', function () {
|
it('should reject when inner templates rejected', function () {
|
||||||
const src = '{%for c in alpha%}{%throwingTag%}{%endfor%}'
|
const src = '{%for c in alpha%}{%throwingTag%}{%endfor%}'
|
||||||
return expect(liquid.parseAndRender(src, scope))
|
return expect(liquid.parseAndRender(src, scope))
|
||||||
|
|||||||
@@ -83,6 +83,22 @@ describe('tags/include', function () {
|
|||||||
const html = await liquid.renderFile('with.html')
|
const html = await liquid.renderFile('with.html')
|
||||||
return expect(html).to.equal('color:red, shape:rect')
|
return expect(html).to.equal('color:red, shape:rect')
|
||||||
})
|
})
|
||||||
|
it('should ignore if with value not specified', async function () {
|
||||||
|
mock({
|
||||||
|
'/with.html': '{% include "color" with, shape: "rect" %}',
|
||||||
|
'/color.html': 'color:{{color}}, shape:{{shape}}'
|
||||||
|
})
|
||||||
|
const html = await liquid.renderFile('with.html')
|
||||||
|
return expect(html).to.equal('color:, shape:rect')
|
||||||
|
})
|
||||||
|
it('should treat with as a valid key', async function () {
|
||||||
|
mock({
|
||||||
|
'/with.html': '{% include "color" with: "foo" %}',
|
||||||
|
'/color.html': 'with:{{with}}'
|
||||||
|
})
|
||||||
|
const html = await liquid.renderFile('with.html')
|
||||||
|
return expect(html).to.equal('with:foo')
|
||||||
|
})
|
||||||
it('should support include: with as Drop', async function () {
|
it('should support include: with as Drop', async function () {
|
||||||
class ColorDrop extends Drop {
|
class ColorDrop extends Drop {
|
||||||
public valueOf (): string {
|
public valueOf (): string {
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { Liquid } from '../../../../src/liquid'
|
import { Liquid } from '../../../../src/liquid'
|
||||||
import { expect } from 'chai'
|
import { expect, use } from 'chai'
|
||||||
import { mock, restore } from '../../../stub/mockfs'
|
import { mock, restore } from '../../../stub/mockfs'
|
||||||
|
import * as chaiAsPromised from 'chai-as-promised'
|
||||||
|
|
||||||
|
use(chaiAsPromised)
|
||||||
|
|
||||||
describe('tags/layout', function () {
|
describe('tags/layout', function () {
|
||||||
let liquid: Liquid
|
let liquid: Liquid
|
||||||
@@ -29,6 +32,15 @@ describe('tags/layout', function () {
|
|||||||
expect(e.message).to.match(/illegal argument ""/)
|
expect(e.message).to.match(/illegal argument ""/)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
it('should throw when filename resolved to falsy', function () {
|
||||||
|
mock({
|
||||||
|
'/parent.html': '{%layout foo%}'
|
||||||
|
})
|
||||||
|
return liquid.renderFile('/parent.html').catch(function (e) {
|
||||||
|
expect(e.name).to.equal('RenderError')
|
||||||
|
expect(e.message).to.match(/illegal filename "foo":"undefined"/)
|
||||||
|
})
|
||||||
|
})
|
||||||
describe('anonymous block', function () {
|
describe('anonymous block', function () {
|
||||||
it('should handle anonymous block', async function () {
|
it('should handle anonymous block', async function () {
|
||||||
mock({
|
mock({
|
||||||
|
|||||||
@@ -121,9 +121,33 @@ describe('tags/render', function () {
|
|||||||
const html = await liquid.renderFile('index.html', { colors: ['red', 'green'] })
|
const html = await liquid.renderFile('index.html', { colors: ['red', 'green'] })
|
||||||
expect(html).to.equal('1: red\n2: green\n')
|
expect(html).to.equal('1: red\n2: green\n')
|
||||||
})
|
})
|
||||||
|
it('should support for <non-array> as', async function () {
|
||||||
|
mock({
|
||||||
|
'/index.html': '{% render "item" for "green" as color %}',
|
||||||
|
'/item.html': '{{forloop.index}}: {{color}}\n'
|
||||||
|
})
|
||||||
|
const html = await liquid.renderFile('index.html')
|
||||||
|
expect(html).to.equal('1: green\n')
|
||||||
|
})
|
||||||
|
it('should support for without as', async function () {
|
||||||
|
mock({
|
||||||
|
'/index.html': '{% render "item" for colors %}',
|
||||||
|
'/item.html': '{{forloop.index}}: {{color}}\n'
|
||||||
|
})
|
||||||
|
const html = await liquid.renderFile('index.html', { colors: ['red', 'green'] })
|
||||||
|
expect(html).to.equal('1: \n2: \n')
|
||||||
|
})
|
||||||
it('should support for...as with other parameters', async function () {
|
it('should support for...as with other parameters', async function () {
|
||||||
mock({
|
mock({
|
||||||
'/index.html': '{% render "item" for colors as color with ".\n" as tail, sep: ". "%}',
|
'/index.html': '{% render "item" for colors as color with ".\n" as tail sep: ". "%}',
|
||||||
|
'/item.html': '{{forloop.index}}{{sep}}{{color}}{{tail}}'
|
||||||
|
})
|
||||||
|
const html = await liquid.renderFile('index.html', { colors: ['red', 'green'] })
|
||||||
|
expect(html).to.equal('1. red.\n2. green.\n')
|
||||||
|
})
|
||||||
|
it('should support for...as with other parameters (comma separated)', async function () {
|
||||||
|
mock({
|
||||||
|
'/index.html': '{% render "item" for colors as color, with ".\n" as tail, sep: ". "%}',
|
||||||
'/item.html': '{{forloop.index}}{{sep}}{{color}}{{tail}}'
|
'/item.html': '{{forloop.index}}{{sep}}{{color}}{{tail}}'
|
||||||
})
|
})
|
||||||
const html = await liquid.renderFile('index.html', { colors: ['red', 'green'] })
|
const html = await liquid.renderFile('index.html', { colors: ['red', 'green'] })
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { Liquid } from '../../../../src/liquid'
|
import { Liquid } from '../../../../src/liquid'
|
||||||
import { expect } from 'chai'
|
import { expect, use } from 'chai'
|
||||||
|
import * as chaiAsPromised from 'chai-as-promised'
|
||||||
|
|
||||||
|
use(chaiAsPromised)
|
||||||
|
|
||||||
describe('tags/tablerow', function () {
|
describe('tags/tablerow', function () {
|
||||||
const liquid = new Liquid()
|
const liquid = new Liquid()
|
||||||
@@ -50,6 +53,12 @@ describe('tags/tablerow', function () {
|
|||||||
.to.be.rejectedWith(/tag .* not closed/)
|
.to.be.rejectedWith(/tag .* not closed/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should throw when x in y not found', function () {
|
||||||
|
const src = '{% tablerow i (1..3) %}{{ i }}'
|
||||||
|
return expect(liquid.parseAndRender(src))
|
||||||
|
.to.be.rejectedWith('illegal tag: {% tablerow i (1..3) %}, line:1, col:1')
|
||||||
|
})
|
||||||
|
|
||||||
it('should support tablerow with range', async function () {
|
it('should support tablerow with range', async function () {
|
||||||
const src = '{% tablerow i in (1..5) cols:2 %}{{ i }}{% endtablerow %}'
|
const src = '{% tablerow i in (1..5) cols:2 %}{{ i }}{% endtablerow %}'
|
||||||
const dst =
|
const dst =
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ describe('Liquid', function () {
|
|||||||
})
|
})
|
||||||
const tpls = await engine.getTemplate('mocha')
|
const tpls = await engine.getTemplate('mocha')
|
||||||
expect(tpls.length).to.gte(1)
|
expect(tpls.length).to.gte(1)
|
||||||
expect(tpls[0].token.raw).to.contain('module.exports')
|
expect(tpls[0].token.getText()).to.contain('module.exports')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
describe('#evalValue', function () {
|
describe('#evalValue', function () {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ describe('liquid#registerTag()', function () {
|
|||||||
it('should have access to ctx in render()', async () => {
|
it('should have access to ctx in render()', async () => {
|
||||||
const liquid = new Liquid()
|
const liquid = new Liquid()
|
||||||
liquid.registerTag('dynamic-string', {
|
liquid.registerTag('dynamic-string', {
|
||||||
render: async (ctx) => ctx.get('c')
|
render: async (ctx) => ctx.get(['c'])
|
||||||
})
|
})
|
||||||
const html = await liquid.parseAndRender(`A{% dynamic-string %}C`, {
|
const html = await liquid.parseAndRender(`A{% dynamic-string %}C`, {
|
||||||
c: 'B'
|
c: 'B'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { normalize } from '../../../src/liquid-options'
|
import { normalize } from '../../../src/liquid-options'
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
|
|
||||||
describe('LiquidOptions', function () {
|
describe('LiquidOptions#root', function () {
|
||||||
describe('#normalize ()', function () {
|
describe('#normalize ()', function () {
|
||||||
it('should normalize string typed root array', function () {
|
it('should normalize string typed root array', function () {
|
||||||
const options = normalize({ root: 'foo' })
|
const options = normalize({ root: 'foo' })
|
||||||
@@ -45,7 +45,7 @@ describe('LiquidOptions#trimming', function () {
|
|||||||
const html = await engine.parseAndRender(src, ctx)
|
const html = await engine.parseAndRender(src, ctx)
|
||||||
return expect(html).to.equal('aharttle')
|
return expect(html).to.equal('aharttle')
|
||||||
})
|
})
|
||||||
it('should respect to greedy:false by default', async function () {
|
it('should allow greedy:false', async function () {
|
||||||
const engine = new Liquid({ greedy: false } as any)
|
const engine = new Liquid({ greedy: false } as any)
|
||||||
const html = await engine.parseAndRender(src, ctx)
|
const html = await engine.parseAndRender(src, ctx)
|
||||||
return expect(html).to.equal('\n a \nharttle ')
|
return expect(html).to.equal('\n a \nharttle ')
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { Liquid } from '../..'
|
import { Liquid } from '../../../src/liquid'
|
||||||
|
|
||||||
const liquid = new Liquid()
|
const liquid = new Liquid()
|
||||||
|
|
||||||
@@ -41,11 +41,6 @@ describe('error', function () {
|
|||||||
const err = await expect(engine.parseAndRender(html)).be.rejected
|
const err = await expect(engine.parseAndRender(html)).be.rejected
|
||||||
expect(err.token.input).to.equal(html)
|
expect(err.token.input).to.equal(html)
|
||||||
})
|
})
|
||||||
it('should contain line number in err.token.line', async function () {
|
|
||||||
const err = await expect(engine.parseAndRender('1\n2\n{% . a %}\n4')).be.rejected
|
|
||||||
expect(err.name).to.equal('TokenizationError')
|
|
||||||
expect(err.token.line).to.equal(3)
|
|
||||||
})
|
|
||||||
it('should contain stack in err.stack', async function () {
|
it('should contain stack in err.stack', async function () {
|
||||||
const err = await expect(engine.parseAndRender('{% . a %}')).be.rejected
|
const err = await expect(engine.parseAndRender('{% . a %}')).be.rejected
|
||||||
expect(err.message).to.contain('illegal tag syntax')
|
expect(err.message).to.contain('illegal tag syntax')
|
||||||
@@ -58,11 +53,11 @@ describe('error', function () {
|
|||||||
expect(err.stack).to.not.contain('at Object.parse')
|
expect(err.stack).to.not.contain('at Object.parse')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
it('should throw error with line and pos if tag unmatched', async function () {
|
it('should throw error with [line, col] if tag unmatched', async function () {
|
||||||
const err = await expect(engine.parseAndRender('1\n2\nfoo{% assign a = 4 }\n4')).be.rejected
|
const err = await expect(engine.parseAndRender('1\n2\nfoo{% assign a = 4 }\n4')).be.rejected
|
||||||
|
console.log(err.stack)
|
||||||
expect(err.name).to.equal('TokenizationError')
|
expect(err.name).to.equal('TokenizationError')
|
||||||
expect(err.token.line).to.equal(3)
|
expect(err.message).to.equal('tag "{% assign a =..." not closed, line:3, col:4')
|
||||||
expect(err.token.col).to.equal(4)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -175,12 +170,6 @@ describe('error', function () {
|
|||||||
expect(err.stack).to.contain(message.join('\n'))
|
expect(err.stack).to.contain(message.join('\n'))
|
||||||
expect(err.name).to.equal('RenderError')
|
expect(err.name).to.equal('RenderError')
|
||||||
})
|
})
|
||||||
it('should contain line number in err.token.line', async function () {
|
|
||||||
const src = '1\n2\n{{1|throwingFilter}}\n4'
|
|
||||||
const err = await expect(engine.parseAndRender(src)).be.rejected
|
|
||||||
expect(err.token.line).to.equal(3)
|
|
||||||
expect(err.name).to.equal('RenderError')
|
|
||||||
})
|
|
||||||
it('should contain stack in err.stack', async function () {
|
it('should contain stack in err.stack', async function () {
|
||||||
const err = await expect(engine.parseAndRender('{%rejectingTag%}')).be.rejected
|
const err = await expect(engine.parseAndRender('{%rejectingTag%}')).be.rejected
|
||||||
expect(err.message).to.contain('intended render reject')
|
expect(err.message).to.contain('intended render reject')
|
||||||
@@ -256,12 +245,6 @@ describe('error', function () {
|
|||||||
expect(err.stack).to.contain(message.join('\n'))
|
expect(err.stack).to.contain(message.join('\n'))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should contain line number in err.token.line', async function () {
|
|
||||||
const html = '<html>\n<head>\n\n{% raw %}\n\n'
|
|
||||||
const err = await expect(engine.parseAndRender(html)).be.rejected
|
|
||||||
expect(err.token.line).to.equal(4)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should contain stack in err.stack', async function () {
|
it('should contain stack in err.stack', async function () {
|
||||||
const err = await expect(engine.parseAndRender('{% -a %}')).be.rejected
|
const err = await expect(engine.parseAndRender('{% -a %}')).be.rejected
|
||||||
expect(err.stack).to.contain('ParseError: tag "-a" not found')
|
expect(err.stack).to.contain('ParseError: tag "-a" not found')
|
||||||
|
|||||||
+30
-104
@@ -24,123 +24,49 @@ describe('Context', function () {
|
|||||||
ctx = new Context(scope)
|
ctx = new Context(scope)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('#propertyAccessSeq()', function () {
|
|
||||||
it('should handle dot syntax', async function () {
|
|
||||||
expect(ctx.parseProp('foo.bar'))
|
|
||||||
.to.deep.equal(['foo', 'bar'])
|
|
||||||
})
|
|
||||||
it('should handle [<String>] syntax', async function () {
|
|
||||||
expect(ctx.parseProp('foo["bar"]'))
|
|
||||||
.to.deep.equal(['foo', 'bar'])
|
|
||||||
})
|
|
||||||
it('should handle [<Identifier>] syntax', async function () {
|
|
||||||
expect(ctx.parseProp('foo[foo]'))
|
|
||||||
.to.deep.equal(['foo', 'zoo'])
|
|
||||||
})
|
|
||||||
it('should handle nested access 1', async function () {
|
|
||||||
expect(ctx.parseProp('foo[bar.zoo]'))
|
|
||||||
.to.deep.equal(['foo', 'coo'])
|
|
||||||
})
|
|
||||||
it('should handle nested access 2', async function () {
|
|
||||||
expect(ctx.parseProp('foo[bar["zoo"]]'))
|
|
||||||
.to.deep.equal(['foo', 'coo'])
|
|
||||||
})
|
|
||||||
it('should handle nested access 3', async function () {
|
|
||||||
expect(ctx.parseProp('bar["foo"].zoo'))
|
|
||||||
.to.deep.equal(['bar', 'foo', 'zoo'])
|
|
||||||
})
|
|
||||||
it('should handle nested access 4', async function () {
|
|
||||||
expect(ctx.parseProp('foo[0].bar'))
|
|
||||||
.to.deep.equal(['foo', '0', 'bar'])
|
|
||||||
})
|
|
||||||
it('should handle nested access 5', async function () {
|
|
||||||
expect(ctx.parseProp('foo[one].bar'))
|
|
||||||
.to.deep.equal(['foo', '1', 'bar'])
|
|
||||||
})
|
|
||||||
it('should handle nested access 6', async function () {
|
|
||||||
expect(ctx.parseProp('foo[two].bar'))
|
|
||||||
.to.deep.equal(['foo', 'undefined', 'bar'])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#get()', function () {
|
describe('#get()', function () {
|
||||||
it('should get direct property', async function () {
|
it('should get direct property', async function () {
|
||||||
expect(ctx.get('foo')).equal('zoo')
|
expect(ctx.get(['foo'])).equal('zoo')
|
||||||
|
})
|
||||||
|
it('should read nested property', async function () {
|
||||||
|
expect(ctx.get(['obj', 'first'])).to.equal('f')
|
||||||
|
expect(ctx.get(['obj', 'last'])).to.equal('l')
|
||||||
|
expect(ctx.get(['obj', 'size'])).to.equal(undefined)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('undefined property should yield undefined', async function () {
|
it('undefined property should yield undefined', async function () {
|
||||||
expect(ctx.get('notdefined')).to.equal(undefined)
|
expect(ctx.get(['notdefined'])).to.equal(undefined)
|
||||||
expect(ctx.get(false as any)).to.equal(undefined)
|
expect(ctx.get([false as any])).to.equal(undefined)
|
||||||
})
|
|
||||||
|
|
||||||
it('should throw for invalid path', async function () {
|
|
||||||
expect(() => ctx.get('')).to.throw('invalid path:""')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should throw when [] unbalanced', async function () {
|
|
||||||
expect(() => ctx.get('foo[bar')).to.throw(/unbalanced \[\]/)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should throw when "" unbalanced', async function () {
|
|
||||||
expect(() => ctx.get('foo["bar]')).to.throw(/unbalanced "/)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should throw when '' unbalanced", async function () {
|
|
||||||
expect(() => ctx.get("foo['bar]")).to.throw(/unbalanced '/)
|
|
||||||
})
|
})
|
||||||
it('should respect to toLiquid', async function () {
|
it('should respect to toLiquid', async function () {
|
||||||
const scope = new Context({ foo: {
|
const scope = new Context({ foo: {
|
||||||
toLiquid: () => ({ bar: 'BAR' }),
|
toLiquid: () => ({ bar: 'BAR' }),
|
||||||
bar: 'bar'
|
bar: 'bar'
|
||||||
} })
|
} })
|
||||||
expect(scope.get('foo.bar')).to.equal('BAR')
|
expect(scope.get(['foo', 'bar'])).to.equal('BAR')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should access child property via dot syntax', async function () {
|
|
||||||
expect(ctx.get('bar.zoo')).to.equal('coo')
|
|
||||||
expect(ctx.get('bar.arr')).to.deep.equal(['a', 'b'])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should access child property via [<String>] syntax', async function () {
|
|
||||||
expect(ctx.get('bar["zoo"]')).to.equal('coo')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should access child property via [<Number>] syntax', async function () {
|
|
||||||
expect(ctx.get('bar.arr[0]')).to.equal('a')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should access child property via [<Identifier>] syntax', async function () {
|
|
||||||
expect(ctx.get('bar[foo]')).to.equal('coo')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return undefined when not exist', async function () {
|
it('should return undefined when not exist', async function () {
|
||||||
expect(ctx.get('foo.foo.foo')).to.be.undefined
|
expect(ctx.get(['foo', 'foo', 'foo'])).to.be.undefined
|
||||||
})
|
})
|
||||||
it('should return string length as size', async function () {
|
it('should return string length as size', async function () {
|
||||||
expect(ctx.get('foo.size')).to.equal(3)
|
expect(ctx.get(['foo', 'size'])).to.equal(3)
|
||||||
})
|
})
|
||||||
it('should return array length as size', async function () {
|
it('should return array length as size', async function () {
|
||||||
expect(ctx.get('bar.arr.size')).to.equal(2)
|
expect(ctx.get(['bar', 'arr', 'size'])).to.equal(2)
|
||||||
})
|
|
||||||
it('should return size property if exists', async function () {
|
|
||||||
expect(ctx.get('zoo.size')).to.equal(4)
|
|
||||||
})
|
|
||||||
it('should return undefined if do not have size and length', async function () {
|
|
||||||
expect(ctx.get('one.size')).to.equal(undefined)
|
|
||||||
})
|
})
|
||||||
it('should read .first of array', async function () {
|
it('should read .first of array', async function () {
|
||||||
expect(ctx.get('bar.arr.first')).to.equal('a')
|
expect(ctx.get(['bar', 'arr', 'first'])).to.equal('a')
|
||||||
})
|
|
||||||
it('should read .first of object', async function () {
|
|
||||||
expect(ctx.get('obj.first')).to.equal('f')
|
|
||||||
})
|
})
|
||||||
it('should read .last of array', async function () {
|
it('should read .last of array', async function () {
|
||||||
expect(ctx.get('bar.arr.last')).to.equal('b')
|
expect(ctx.get(['bar', 'arr', 'last'])).to.equal('b')
|
||||||
})
|
|
||||||
it('should read .last of object', async function () {
|
|
||||||
expect(ctx.get('obj.last')).to.equal('l')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('#getFromScope()', function () {
|
||||||
|
it('should support string', () => {
|
||||||
|
expect(ctx.getFromScope({ obj: { foo: 'FOO' } }, 'obj.foo')).to.equal('FOO')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('strictVariables', async function () {
|
describe('strictVariables', async function () {
|
||||||
let ctx: Context
|
let ctx: Context
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
@@ -149,22 +75,22 @@ describe('Context', function () {
|
|||||||
} as any)
|
} as any)
|
||||||
})
|
})
|
||||||
it('should throw when variable not defined', function () {
|
it('should throw when variable not defined', function () {
|
||||||
return expect(() => ctx.get('notdefined')).to.throw(/undefined variable: notdefined/)
|
return expect(() => ctx.get(['notdefined'])).to.throw(/undefined variable: notdefined/)
|
||||||
})
|
})
|
||||||
it('should throw when deep variable not exist', async function () {
|
it('should throw when deep variable not exist', async function () {
|
||||||
ctx.push({ foo: 'FOO' })
|
ctx.push({ foo: 'FOO' })
|
||||||
return expect(() => ctx.get('foo.bar.not.defined')).to.throw(/undefined variable: bar/)
|
return expect(() => ctx.get(['foo', 'bar', 'not', 'defined'])).to.throw(/undefined variable: bar/)
|
||||||
})
|
})
|
||||||
it('should throw when itself not defined', async function () {
|
it('should throw when itself not defined', async function () {
|
||||||
ctx.push({ foo: 'FOO' })
|
ctx.push({ foo: 'FOO' })
|
||||||
return expect(() => ctx.get('foo.BAR')).to.throw(/undefined variable: BAR/)
|
return expect(() => ctx.get(['foo', 'BAR'])).to.throw(/undefined variable: BAR/)
|
||||||
})
|
})
|
||||||
it('should find variable in parent scope', async function () {
|
it('should find variable in parent scope', async function () {
|
||||||
ctx.push({ 'foo': 'foo' })
|
ctx.push({ 'foo': 'foo' })
|
||||||
ctx.push({
|
ctx.push({
|
||||||
'bar': 'bar'
|
'bar': 'bar'
|
||||||
})
|
})
|
||||||
expect(ctx.get('foo')).to.equal('foo')
|
expect(ctx.get(['foo'])).to.equal('foo')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -180,14 +106,14 @@ describe('Context', function () {
|
|||||||
ctx.push({
|
ctx.push({
|
||||||
foo: 'foo'
|
foo: 'foo'
|
||||||
})
|
})
|
||||||
expect(ctx.get('foo')).to.equal('foo')
|
expect(ctx.get(['foo'])).to.equal('foo')
|
||||||
expect(ctx.get('bar')).to.equal('bar')
|
expect(ctx.get(['bar'])).to.equal('bar')
|
||||||
})
|
})
|
||||||
it('should hide deep properties by push', async function () {
|
it('should hide deep properties by push', async function () {
|
||||||
ctx.push({ bar: { bar: 'bar' } })
|
ctx.push({ bar: { bar: 'bar' } })
|
||||||
ctx.push({ bar: { foo: 'foo' } })
|
ctx.push({ bar: { foo: 'foo' } })
|
||||||
expect(ctx.get('bar.foo')).to.equal('foo')
|
expect(ctx.get(['bar', 'foo'])).to.equal('foo')
|
||||||
expect(ctx.get('bar.bar')).to.equal(undefined)
|
expect(ctx.get(['bar', 'bar'])).to.equal(undefined)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
describe('.pop()', function () {
|
describe('.pop()', function () {
|
||||||
@@ -196,7 +122,7 @@ describe('Context', function () {
|
|||||||
foo: 'foo'
|
foo: 'foo'
|
||||||
})
|
})
|
||||||
ctx.pop()
|
ctx.pop()
|
||||||
expect(ctx.get('foo')).to.equal('zoo')
|
expect(ctx.get(['foo'])).to.equal('zoo')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { expect } from 'chai'
|
||||||
|
import { matchOperator } from '../../../src/parser/match-operator'
|
||||||
|
|
||||||
|
describe('parser/matchOperator()', function () {
|
||||||
|
it('should match contains', () => {
|
||||||
|
expect(matchOperator('contains', 0)).to.equal(8)
|
||||||
|
})
|
||||||
|
it('should match comparision', () => {
|
||||||
|
expect(matchOperator('>', 0)).to.equal(1)
|
||||||
|
expect(matchOperator('>=', 0)).to.equal(2)
|
||||||
|
expect(matchOperator('<', 0)).to.equal(1)
|
||||||
|
expect(matchOperator('<=', 0)).to.equal(2)
|
||||||
|
})
|
||||||
|
it('should match binary logic', () => {
|
||||||
|
expect(matchOperator('and', 0)).to.equal(3)
|
||||||
|
expect(matchOperator('or', 0)).to.equal(2)
|
||||||
|
})
|
||||||
|
it('should not match if word not terminate', () => {
|
||||||
|
expect(matchOperator('true1', 0)).to.equal(-1)
|
||||||
|
expect(matchOperator('containsa', 0)).to.equal(-1)
|
||||||
|
})
|
||||||
|
it('should match if word boundary found', () => {
|
||||||
|
expect(matchOperator('>=1', 0)).to.equal(2)
|
||||||
|
expect(matchOperator('contains b', 0)).to.equal(8)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,30 +1,5 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { parseLiteral, parseStringLiteral } from '../../../src/parser/literal'
|
import { parseStringLiteral } from '../../../src/parser/parse-string-literal'
|
||||||
import { NullDrop } from '../../../src/drop/null-drop'
|
|
||||||
|
|
||||||
describe('parseLiteral()', function () {
|
|
||||||
it('should eval boolean literal', async function () {
|
|
||||||
expect(parseLiteral('true')).to.equal(true)
|
|
||||||
expect(parseLiteral('TrUE')).to.equal(undefined)
|
|
||||||
expect(parseLiteral('false')).to.equal(false)
|
|
||||||
})
|
|
||||||
it('should eval number literal', async function () {
|
|
||||||
expect(parseLiteral('2.3')).to.equal(2.3)
|
|
||||||
expect(parseLiteral('.32')).to.equal(0.32)
|
|
||||||
expect(parseLiteral('-23.')).to.equal(-23)
|
|
||||||
expect(parseLiteral('23')).to.equal(23)
|
|
||||||
})
|
|
||||||
it('should eval string literal', async function () {
|
|
||||||
expect(parseLiteral('"ab\'c"')).to.equal("ab'c")
|
|
||||||
expect(parseLiteral("'ab\"c'")).to.equal('ab"c')
|
|
||||||
})
|
|
||||||
it('should eval nil literal', async function () {
|
|
||||||
expect(parseLiteral('nil')).to.be.instanceOf(NullDrop)
|
|
||||||
})
|
|
||||||
it('should eval null literal', async function () {
|
|
||||||
expect(parseLiteral('null')).to.be.instanceOf(NullDrop)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('parseStringLiteral()', function () {
|
describe('parseStringLiteral()', function () {
|
||||||
it('should parse octal escape', () => {
|
it('should parse octal escape', () => {
|
||||||
@@ -36,7 +11,7 @@ describe('parseStringLiteral()', function () {
|
|||||||
it('should skip invalid octal escape', () => {
|
it('should skip invalid octal escape', () => {
|
||||||
expect(parseStringLiteral(String.raw`"\9"`)).to.equal('9')
|
expect(parseStringLiteral(String.raw`"\9"`)).to.equal('9')
|
||||||
})
|
})
|
||||||
it('should parse \n, \t, \r', () => {
|
it('should parse \\n, \\t, \\r', () => {
|
||||||
expect(parseStringLiteral(String.raw`"fo\no"`)).to.equal('fo\no')
|
expect(parseStringLiteral(String.raw`"fo\no"`)).to.equal('fo\no')
|
||||||
expect(parseStringLiteral(String.raw`'fo\to'`)).to.equal('fo\to')
|
expect(parseStringLiteral(String.raw`'fo\to'`)).to.equal('fo\to')
|
||||||
expect(parseStringLiteral(String.raw`'fo\ro'`)).to.equal('fo\ro')
|
expect(parseStringLiteral(String.raw`'fo\ro'`)).to.equal('fo\ro')
|
||||||
+356
-135
@@ -1,210 +1,431 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
|
import { WordToken } from '../../../src/tokens/word-token'
|
||||||
|
import { NumberToken } from '../../../src/tokens/number-token'
|
||||||
|
import { PropertyAccessToken } from '../../../src/tokens/property-access-token'
|
||||||
|
import { RangeToken } from '../../../src/tokens/range-token'
|
||||||
|
import { OperatorToken } from '../../../src/tokens/operator-token'
|
||||||
import { Tokenizer } from '../../../src/parser/tokenizer'
|
import { Tokenizer } from '../../../src/parser/tokenizer'
|
||||||
import { TagToken } from '../../../src/parser/tag-token'
|
import { TagToken } from '../../../src/tokens/tag-token'
|
||||||
import { OutputToken } from '../../../src/parser/output-token'
|
import { QuotedToken } from '../../../src/tokens/quoted-token'
|
||||||
import { HTMLToken } from '../../../src/parser/html-token'
|
import { OutputToken } from '../../../src/tokens/output-token'
|
||||||
|
import { HTMLToken } from '../../../src/tokens/html-token'
|
||||||
|
|
||||||
describe('Tokenize', function () {
|
describe('Tokenize', function () {
|
||||||
it('should read quoted', () => {
|
it('should read quoted', () => {
|
||||||
expect(new Tokenizer('"foo" ff').readQuoted().toString()).to.equal('"foo"')
|
expect(new Tokenizer('"foo" ff').readQuoted()!.getText()).to.equal('"foo"')
|
||||||
expect(new Tokenizer(' "foo"ff').readQuoted().toString()).to.equal('"foo"')
|
expect(new Tokenizer(' "foo"ff').readQuoted()!.getText()).to.equal('"foo"')
|
||||||
})
|
|
||||||
it('should read property access', () => {
|
|
||||||
expect(new Tokenizer('a[ b][ "c d" ]').readPropertyAccess().toString()).to.equal('a[ b][ "c d" ]')
|
|
||||||
expect(new Tokenizer('a.b[c[d.e]]').readPropertyAccess().toString()).to.equal('a.b[c[d.e]]')
|
|
||||||
})
|
})
|
||||||
it('should read value', () => {
|
it('should read value', () => {
|
||||||
expect(new Tokenizer('2.33.2').readValue().toString()).to.equal('2.33.2')
|
expect(new Tokenizer('a[ b][ "c d" ]').readValueOrThrow().getText()).to.equal('a[ b][ "c d" ]')
|
||||||
expect(new Tokenizer('"foo"a').readValue().toString()).to.equal('"foo"')
|
expect(new Tokenizer('a.b[c[d.e]]').readValueOrThrow().getText()).to.equal('a.b[c[d.e]]')
|
||||||
expect(new Tokenizer('a[b]["c d"]').readValue().toString()).to.equal('a[b]["c d"]')
|
})
|
||||||
|
it('should read number value', () => {
|
||||||
|
const token: NumberToken = new Tokenizer('2.33.2').readValueOrThrow() as any
|
||||||
|
expect(token).to.be.instanceOf(NumberToken)
|
||||||
|
expect(token.whole.getText()).to.equal('2')
|
||||||
|
expect(token.decimal!.getText()).to.equal('33')
|
||||||
|
expect(token.getText()).to.equal('2.33')
|
||||||
|
})
|
||||||
|
it('should read quoted value', () => {
|
||||||
|
const value = new Tokenizer('"foo"a').readValue()
|
||||||
|
expect(value).to.be.instanceOf(QuotedToken)
|
||||||
|
expect(value!.getText()).to.equal('"foo"')
|
||||||
|
})
|
||||||
|
it('should read property access value', () => {
|
||||||
|
expect(new Tokenizer('a[b]["c d"]').readValueOrThrow().getText()).to.equal('a[b]["c d"]')
|
||||||
})
|
})
|
||||||
it('should read hash', () => {
|
it('should read hash', () => {
|
||||||
expect(new Tokenizer('foo: 3').readHash()).to.deep.equal(['foo', '3'])
|
const hash1 = new Tokenizer('foo: 3').readHash()
|
||||||
expect(new Tokenizer(', foo: a[ "bar"]').readHash()).to.deep.equal(['foo', 'a[ "bar"]'])
|
expect(hash1!.name.content).to.equal('foo')
|
||||||
|
expect(hash1!.value!.getText()).to.equal('3')
|
||||||
|
|
||||||
|
const hash2 = new Tokenizer(', foo: a[ "bar"]').readHash()
|
||||||
|
expect(hash2!.name.content).to.equal('foo')
|
||||||
|
expect(hash2!.value!.getText()).to.equal('a[ "bar"]')
|
||||||
})
|
})
|
||||||
it('should read hashs', () => {
|
it('should read multiple hashs', () => {
|
||||||
expect(new Tokenizer(', limit: 3 reverse offset:off').readHashes())
|
const hashes = new Tokenizer(', limit: 3 reverse offset:off').readHashes()
|
||||||
.to.deep.equal([['limit', '3'], ['reverse', ''], ['offset', 'off']])
|
expect(hashes).to.have.lengthOf(3)
|
||||||
expect(new Tokenizer('cols: 2, rows: data["rows"]').readHashes())
|
const [limit, reverse, offset] = hashes
|
||||||
.to.deep.equal([['cols', '2'], ['rows', 'data["rows"]']])
|
expect(limit.name.content).to.equal('limit')
|
||||||
|
expect(limit.value!.getText()).to.equal('3')
|
||||||
|
|
||||||
|
expect(reverse.name.content).to.equal('reverse')
|
||||||
|
expect(reverse.value).to.be.undefined
|
||||||
|
|
||||||
|
expect(offset.name.content).to.equal('offset')
|
||||||
|
expect(offset.value!.getText()).to.equal('off')
|
||||||
|
})
|
||||||
|
it('should read hash value with property access', () => {
|
||||||
|
const hashes = new Tokenizer('cols: 2, rows: data["rows"]').readHashes()
|
||||||
|
expect(hashes).to.have.lengthOf(2)
|
||||||
|
const [cols, rols] = hashes
|
||||||
|
|
||||||
|
expect(cols.name.content).to.equal('cols')
|
||||||
|
expect(cols.value!.getText()).to.equal('2')
|
||||||
|
|
||||||
|
expect(rols.name.content).to.equal('rows')
|
||||||
|
expect(rols.value!.getText()).to.equal('data["rows"]')
|
||||||
})
|
})
|
||||||
it('should read HTML token', function () {
|
it('should read HTML token', function () {
|
||||||
const html = '<html><body><p>Lorem Ipsum</p></body></html>'
|
const html = '<html><body><p>Lorem Ipsum</p></body></html>'
|
||||||
const tokenizer = new Tokenizer(html)
|
const tokenizer = new Tokenizer(html)
|
||||||
const tokens = tokenizer.readTokens()
|
const tokens = tokenizer.readTopLevelTokens()
|
||||||
|
|
||||||
expect(tokens.length).to.equal(1)
|
expect(tokens.length).to.equal(1)
|
||||||
expect(tokens[0].content).to.equal(html)
|
|
||||||
expect(tokens[0]).instanceOf(HTMLToken)
|
expect(tokens[0]).instanceOf(HTMLToken)
|
||||||
|
expect((tokens[0] as HTMLToken).getContent()).to.equal(html)
|
||||||
})
|
})
|
||||||
it('should read tag token', function () {
|
it('should read tag token', function () {
|
||||||
const html = '<p>{% for p in a[1]%}</p>'
|
const html = '<p>{% for p in a[1]%}</p>'
|
||||||
const tokenizer = new Tokenizer(html)
|
const tokenizer = new Tokenizer(html)
|
||||||
const tokens = tokenizer.readTokens()
|
const tokens = tokenizer.readTopLevelTokens()
|
||||||
|
|
||||||
expect(tokens.length).to.equal(3)
|
expect(tokens.length).to.equal(3)
|
||||||
expect(tokens[1]).instanceOf(TagToken)
|
const tag = tokens[1] as TagToken
|
||||||
expect(tokens[1].content).to.equal('for p in a[1]')
|
expect(tag).instanceOf(TagToken)
|
||||||
|
expect(tag.name).to.equal('for')
|
||||||
|
expect(tag.args).to.equal('p in a[1]')
|
||||||
})
|
})
|
||||||
it('should read value token', function () {
|
it('should read output token', function () {
|
||||||
const html = '<p>{{foo | date: "%Y-%m-%d"}}</p>'
|
const html = '<p>{{foo | date: "%Y-%m-%d"}}</p>'
|
||||||
const tokenizer = new Tokenizer(html)
|
const tokenizer = new Tokenizer(html)
|
||||||
const tokens = tokenizer.readTokens()
|
const tokens = tokenizer.readTopLevelTokens()
|
||||||
|
|
||||||
expect(tokens.length).to.equal(3)
|
expect(tokens.length).to.equal(3)
|
||||||
expect(tokens[1]).instanceOf(OutputToken)
|
const output = tokens[1] as OutputToken
|
||||||
expect(tokens[1].content).to.equal('foo | date: "%Y-%m-%d"')
|
expect(output).instanceOf(OutputToken)
|
||||||
|
expect(output.content).to.equal('foo | date: "%Y-%m-%d"')
|
||||||
})
|
})
|
||||||
it('should handle consecutive value and tags', function () {
|
it('should handle consecutive value and tags', function () {
|
||||||
const html = '{{foo}}{{bar}}{%foo%}{%bar%}'
|
const html = '{{foo}}{{bar}}{%foo%}{%bar%}'
|
||||||
const tokenizer = new Tokenizer(html)
|
const tokenizer = new Tokenizer(html)
|
||||||
const tokens = tokenizer.readTokens()
|
const tokens = tokenizer.readTopLevelTokens()
|
||||||
|
|
||||||
expect(tokens.length).to.equal(4)
|
expect(tokens.length).to.equal(4)
|
||||||
expect(tokens[0]).instanceOf(OutputToken)
|
const o1 = tokens[0] as OutputToken
|
||||||
expect(tokens[2]).instanceOf(TagToken)
|
const o2 = tokens[1] as OutputToken
|
||||||
|
const t1 = tokens[2] as TagToken
|
||||||
|
const t2 = tokens[3] as TagToken
|
||||||
|
expect(o1).instanceOf(OutputToken)
|
||||||
|
expect(o2).instanceOf(OutputToken)
|
||||||
|
expect(t1).instanceOf(TagToken)
|
||||||
|
expect(t2).instanceOf(TagToken)
|
||||||
|
|
||||||
expect(tokens[1].content).to.equal('bar')
|
expect(o1.content).to.equal('foo')
|
||||||
expect(tokens[2].content).to.equal('foo')
|
expect(o2.content).to.equal('bar')
|
||||||
|
expect(t1.name).to.equal('foo')
|
||||||
|
expect(t1.args).to.equal('')
|
||||||
|
expect(t2.name).to.equal('bar')
|
||||||
|
expect(t2.args).to.equal('')
|
||||||
})
|
})
|
||||||
it('should keep white spaces and newlines', function () {
|
it('should keep white spaces and newlines', function () {
|
||||||
const html = '{%foo%}\n{%bar %} \n {%alice%}'
|
const html = '{%foo%}\n{%bar %} \n {%alice%}'
|
||||||
const tokenizer = new Tokenizer(html)
|
const tokenizer = new Tokenizer(html)
|
||||||
const tokens = tokenizer.readTokens()
|
const tokens = tokenizer.readTopLevelTokens()
|
||||||
expect(tokens.length).to.equal(5)
|
expect(tokens.length).to.equal(5)
|
||||||
expect(tokens[1]).instanceOf(HTMLToken)
|
expect(tokens[1]).instanceOf(HTMLToken)
|
||||||
expect(tokens[1].raw).to.equal('\n')
|
expect(tokens[1].getText()).to.equal('\n')
|
||||||
expect(tokens[3]).instanceOf(HTMLToken)
|
expect(tokens[3]).instanceOf(HTMLToken)
|
||||||
expect(tokens[3].raw).to.equal(' \n ')
|
expect(tokens[3].getText()).to.equal(' \n ')
|
||||||
})
|
})
|
||||||
it('should handle multiple lines tag', function () {
|
it('should handle multiple lines tag', function () {
|
||||||
const html = '{%foo\na:a\nb:1.23\n%}'
|
const html = '{%foo\na:a\nb:1.23\n%}'
|
||||||
const tokenizer = new Tokenizer(html)
|
const tokenizer = new Tokenizer(html)
|
||||||
const tokens = tokenizer.readTokens()
|
const tokens = tokenizer.readTopLevelTokens()
|
||||||
expect(tokens.length).to.equal(1)
|
expect(tokens.length).to.equal(1)
|
||||||
expect(tokens[0]).instanceOf(TagToken)
|
expect(tokens[0]).instanceOf(TagToken)
|
||||||
expect((tokens[0] as TagToken).args).to.equal('a:a\nb:1.23')
|
expect((tokens[0] as TagToken).args).to.equal('a:a\nb:1.23')
|
||||||
expect(tokens[0].raw).to.equal('{%foo\na:a\nb:1.23\n%}')
|
expect(tokens[0].getText()).to.equal('{%foo\na:a\nb:1.23\n%}')
|
||||||
})
|
})
|
||||||
it('should handle multiple lines value', function () {
|
it('should handle multiple lines value', function () {
|
||||||
const html = '{{foo\n|date:\n"%Y-%m-%d"\n}}'
|
const html = '{{foo\n|date:\n"%Y-%m-%d"\n}}'
|
||||||
const tokenizer = new Tokenizer(html)
|
const tokenizer = new Tokenizer(html)
|
||||||
const tokens = tokenizer.readTokens()
|
const tokens = tokenizer.readTopLevelTokens()
|
||||||
expect(tokens.length).to.equal(1)
|
expect(tokens.length).to.equal(1)
|
||||||
expect(tokens[0]).instanceOf(OutputToken)
|
expect(tokens[0]).instanceOf(OutputToken)
|
||||||
expect(tokens[0].raw).to.equal('{{foo\n|date:\n"%Y-%m-%d"\n}}')
|
expect(tokens[0].getText()).to.equal('{{foo\n|date:\n"%Y-%m-%d"\n}}')
|
||||||
})
|
})
|
||||||
it('should handle complex object property access', function () {
|
it('should handle complex object property access', function () {
|
||||||
const html = '{{ obj["my:property with anything"] }}'
|
const html = '{{ obj["my:property with anything"] }}'
|
||||||
const tokenizer = new Tokenizer(html)
|
const tokenizer = new Tokenizer(html)
|
||||||
const tokens = tokenizer.readTokens()
|
const tokens = tokenizer.readTopLevelTokens()
|
||||||
expect(tokens.length).to.equal(1)
|
expect(tokens.length).to.equal(1)
|
||||||
expect(tokens[0]).instanceOf(OutputToken)
|
const output = tokens[0] as OutputToken
|
||||||
expect(tokens[0].content).to.equal('obj["my:property with anything"]')
|
expect(output).instanceOf(OutputToken)
|
||||||
|
expect(output.content).to.equal('obj["my:property with anything"]')
|
||||||
})
|
})
|
||||||
it('should throw if tag not closed', function () {
|
it('should throw if tag not closed', function () {
|
||||||
const html = '{% assign foo = bar {{foo}}'
|
const html = '{% assign foo = bar {{foo}}'
|
||||||
const tokenizer = new Tokenizer(html)
|
const tokenizer = new Tokenizer(html)
|
||||||
expect(() => tokenizer.readTokens()).to.throw(/tag "{% assign foo..." not closed/)
|
expect(() => tokenizer.readTopLevelTokens()).to.throw(/tag "{% assign foo..." not closed/)
|
||||||
})
|
})
|
||||||
it('should throw if output not closed', function () {
|
it('should throw if output not closed', function () {
|
||||||
const tokenizer = new Tokenizer('{{name}')
|
const tokenizer = new Tokenizer('{{name}')
|
||||||
expect(() => tokenizer.readTokens()).to.throw(/output "{{name}" not closed/)
|
expect(() => tokenizer.readTopLevelTokens()).to.throw(/output "{{name}" not closed/)
|
||||||
})
|
})
|
||||||
it('should read a simple filter', function () {
|
describe('#readRange()', () => {
|
||||||
const tokenizer = new Tokenizer('| plus')
|
it('should read `(1..3)`', () => {
|
||||||
const token = tokenizer.readFilterToken()
|
const range = new Tokenizer('(1..3)').readRange()
|
||||||
expect(token).to.have.property('name', 'plus')
|
expect(range).to.be.instanceOf(RangeToken)
|
||||||
expect(token).to.have.property('args').to.deep.equal([])
|
expect(range!.getText()).to.deep.equal('(1..3)')
|
||||||
|
const { lhs, rhs } = range!
|
||||||
|
expect(lhs).to.be.instanceOf(NumberToken)
|
||||||
|
expect(lhs.getText()).to.equal('1')
|
||||||
|
expect(rhs).to.be.instanceOf(NumberToken)
|
||||||
|
expect(rhs.getText()).to.equal('3')
|
||||||
|
})
|
||||||
|
it('should throw for `(..3)`', () => {
|
||||||
|
expect(() => new Tokenizer('(..3)').readRange()).to.throw('unexpected token "..3)", value expected')
|
||||||
|
})
|
||||||
|
it('should read `(a.b..c["..d"])`', () => {
|
||||||
|
const range = new Tokenizer('(a.b..c["..d"])').readRange()
|
||||||
|
expect(range).to.be.instanceOf(RangeToken)
|
||||||
|
expect(range!.getText()).to.deep.equal('(a.b..c["..d"])')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
it('should read a filter with argument', function () {
|
describe('#readFilter()', () => {
|
||||||
const tokenizer = new Tokenizer(' | plus: 1')
|
it('should read a simple filter', function () {
|
||||||
const token = tokenizer.readFilterToken()
|
const tokenizer = new Tokenizer('| plus')
|
||||||
expect(token).to.have.property('name', 'plus')
|
const token = tokenizer.readFilter()
|
||||||
expect(token).to.have.property('args').to.deep.equal(['1'])
|
expect(token).to.have.property('name', 'plus')
|
||||||
})
|
expect(token).to.have.property('args').to.deep.equal([])
|
||||||
it('should read a filter with colon but no argument', function () {
|
})
|
||||||
const tokenizer = new Tokenizer('| plus:')
|
it('should read a filter with argument', function () {
|
||||||
const token = tokenizer.readFilterToken()
|
const tokenizer = new Tokenizer(' | plus: 1')
|
||||||
expect(token).to.have.property('name', 'plus')
|
const token = tokenizer.readFilter()
|
||||||
expect(token).to.have.property('args').to.deep.equal([])
|
expect(token).to.have.property('name', 'plus')
|
||||||
})
|
expect(token!.args).to.have.lengthOf(1)
|
||||||
it('should read a filter with k/v argument', function () {
|
|
||||||
const tokenizer = new Tokenizer(' | plus: a:1')
|
|
||||||
const token = tokenizer.readFilterToken()
|
|
||||||
expect(token).to.have.property('name', 'plus')
|
|
||||||
expect(token).to.have.property('args').to.deep.equal([['a', '1']])
|
|
||||||
})
|
|
||||||
it('should read a filter with "arr[0]" argument', function () {
|
|
||||||
const tokenizer = new Tokenizer('| plus: arr[0]')
|
|
||||||
const token = tokenizer.readFilterToken()
|
|
||||||
expect(token).to.have.property('name', 'plus')
|
|
||||||
expect(token).to.have.property('args').to.deep.equal(['arr[0]'])
|
|
||||||
})
|
|
||||||
it('should read a filter with obj.foo argument', function () {
|
|
||||||
const tokenizer = new Tokenizer('| plus: obj.foo')
|
|
||||||
const token = tokenizer.readFilterToken()
|
|
||||||
expect(token).to.have.property('name', 'plus')
|
|
||||||
expect(token).to.have.property('args').to.deep.equal(['obj.foo'])
|
|
||||||
})
|
|
||||||
it('should read a filter with obj["foo"] argument', function () {
|
|
||||||
const tokenizer = new Tokenizer('| plus: obj["good luck"]')
|
|
||||||
const token = tokenizer.readFilterToken()
|
|
||||||
expect(token).to.have.property('name', 'plus')
|
|
||||||
expect(token).to.have.property('args').to.deep.equal(['obj["good luck"]'])
|
|
||||||
})
|
|
||||||
it('should read simple filters', function () {
|
|
||||||
const tokenizer = new Tokenizer('| plus: 3 | capitalize')
|
|
||||||
const tokens = tokenizer.readFilterTokens()
|
|
||||||
|
|
||||||
expect(tokens).to.have.lengthOf(2)
|
const one: NumberToken = token!.args[0] as any
|
||||||
expect(tokens[0]).to.have.property('name', 'plus')
|
expect(one).to.be.instanceOf(NumberToken)
|
||||||
expect(tokens[0]).to.have.property('args').to.deep.equal(['3'])
|
expect(one.getText()).to.equal('1')
|
||||||
expect(tokens[1]).to.have.property('name', 'capitalize')
|
})
|
||||||
expect(tokens[1]).to.have.property('args').to.deep.equal([])
|
it('should read a filter with colon but no argument', function () {
|
||||||
})
|
const tokenizer = new Tokenizer('| plus:')
|
||||||
it('should read filters', function () {
|
const token = tokenizer.readFilter()
|
||||||
const tokenizer = new Tokenizer('| plus: 3 | capitalize | append: foo[a.b["c d"]]')
|
expect(token).to.have.property('name', 'plus')
|
||||||
const tokens = tokenizer.readFilterTokens()
|
expect(token).to.have.property('args').to.deep.equal([])
|
||||||
|
})
|
||||||
|
it('should read a filter with k/v argument', function () {
|
||||||
|
const tokenizer = new Tokenizer(' | plus: a:1')
|
||||||
|
const token = tokenizer.readFilter()
|
||||||
|
expect(token).to.have.property('name', 'plus')
|
||||||
|
expect(token!.args).to.have.lengthOf(1)
|
||||||
|
|
||||||
expect(tokens).to.have.lengthOf(3)
|
const [k, v]: [string, NumberToken] = token!.args[0] as any
|
||||||
expect(tokens[0]).to.have.property('name', 'plus')
|
expect(k).to.equal('a')
|
||||||
expect(tokens[0]).to.have.property('args').to.deep.equal(['3'])
|
expect(v).to.be.instanceOf(NumberToken)
|
||||||
expect(tokens[1]).to.have.property('name', 'capitalize')
|
expect(v.getText()).to.equal('1')
|
||||||
expect(tokens[1]).to.have.property('args').to.deep.equal([])
|
})
|
||||||
expect(tokens[2]).to.have.property('name', 'append')
|
it('should read a filter with "arr[0]" argument', function () {
|
||||||
expect(tokens[2]).to.have.property('args').to.deep.equal(['foo[a.b["c d"]]'])
|
const tokenizer = new Tokenizer('| plus: arr[0]')
|
||||||
|
const token = tokenizer.readFilter()
|
||||||
|
expect(token).to.have.property('name', 'plus')
|
||||||
|
expect(token!.args).to.have.lengthOf(1)
|
||||||
|
|
||||||
|
const pa: PropertyAccessToken = token!.args[0] as any
|
||||||
|
expect(token!.args[0]).to.be.instanceOf(PropertyAccessToken)
|
||||||
|
expect(pa.variable.content).to.equal('arr')
|
||||||
|
expect(pa.props).to.have.lengthOf(1)
|
||||||
|
expect(pa.props[0]).to.be.instanceOf(NumberToken)
|
||||||
|
expect(pa.props[0].getText()).to.equal('0')
|
||||||
|
})
|
||||||
|
it('should read a filter with obj.foo argument', function () {
|
||||||
|
const tokenizer = new Tokenizer('| plus: obj.foo')
|
||||||
|
const token = tokenizer.readFilter()
|
||||||
|
expect(token).to.have.property('name', 'plus')
|
||||||
|
expect(token!.args).to.have.lengthOf(1)
|
||||||
|
|
||||||
|
const pa: PropertyAccessToken = token!.args[0] as any
|
||||||
|
expect(token!.args[0]).to.be.instanceOf(PropertyAccessToken)
|
||||||
|
expect(pa.variable.content).to.equal('obj')
|
||||||
|
expect(pa.props).to.have.lengthOf(1)
|
||||||
|
expect(pa.props[0]).to.be.instanceOf(WordToken)
|
||||||
|
expect(pa.props[0].getText()).to.equal('foo')
|
||||||
|
})
|
||||||
|
it('should read a filter with obj["foo"] argument', function () {
|
||||||
|
const tokenizer = new Tokenizer('| plus: obj["good luck"]')
|
||||||
|
const token = tokenizer.readFilter()
|
||||||
|
expect(token).to.have.property('name', 'plus')
|
||||||
|
expect(token!.args).to.have.lengthOf(1)
|
||||||
|
|
||||||
|
const pa: PropertyAccessToken = token!.args[0] as any
|
||||||
|
expect(token!.args[0]).to.be.instanceOf(PropertyAccessToken)
|
||||||
|
expect(pa.getText()).to.equal('obj["good luck"]')
|
||||||
|
expect(pa.variable.content).to.equal('obj')
|
||||||
|
expect(pa.props[0].getText()).to.equal('"good luck"')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
it('should read expression `a==b`', () => {
|
describe('#readFilters()', () => {
|
||||||
const exp = new Tokenizer('a==b').readExpression()
|
it('should read simple filters', function () {
|
||||||
expect([...exp]).to.deep.equal(['a', '==', 'b'])
|
const tokenizer = new Tokenizer('| plus: 3 | capitalize')
|
||||||
|
const tokens = tokenizer.readFilters()
|
||||||
|
|
||||||
|
expect(tokens).to.have.lengthOf(2)
|
||||||
|
expect(tokens[0]).to.have.property('name', 'plus')
|
||||||
|
expect(tokens[0].args).to.have.lengthOf(1)
|
||||||
|
expect(tokens[0].args[0]).to.be.instanceOf(NumberToken)
|
||||||
|
expect((tokens[0].args[0] as any).getText()).to.equal('3')
|
||||||
|
|
||||||
|
expect(tokens[1]).to.have.property('name', 'capitalize')
|
||||||
|
expect(tokens[1].args).to.have.lengthOf(0)
|
||||||
|
})
|
||||||
|
it('should read filters', function () {
|
||||||
|
const tokenizer = new Tokenizer('| plus: a:3 | capitalize | append: foo[a.b["c d"]]')
|
||||||
|
const tokens = tokenizer.readFilters()
|
||||||
|
|
||||||
|
expect(tokens).to.have.lengthOf(3)
|
||||||
|
expect(tokens[0]).to.have.property('name', 'plus')
|
||||||
|
expect(tokens[0].args).to.have.lengthOf(1)
|
||||||
|
const [k, v]: [string, NumberToken] = tokens[0].args[0] as any
|
||||||
|
expect(k).to.equal('a')
|
||||||
|
expect(v).to.be.instanceOf(NumberToken)
|
||||||
|
expect(v.getText()).to.equal('3')
|
||||||
|
|
||||||
|
expect(tokens[1]).to.have.property('name', 'capitalize')
|
||||||
|
expect(tokens[1].args).to.have.lengthOf(0)
|
||||||
|
|
||||||
|
expect(tokens[2]).to.have.property('name', 'append')
|
||||||
|
expect(tokens[2].args).to.have.lengthOf(1)
|
||||||
|
expect(tokens[2].args[0]).to.be.instanceOf(PropertyAccessToken)
|
||||||
|
expect((tokens[2].args[0] as any).getText()).to.equal('foo[a.b["c d"]]')
|
||||||
|
expect((tokens[2].args[0] as any).props[0].getText()).to.equal('a.b["c d"]')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
it('should read expression `^`', () => {
|
describe('#readExpression()', () => {
|
||||||
const exp = new Tokenizer('^').readExpression()
|
it('should read expression `a `', () => {
|
||||||
expect([...exp]).to.deep.equal([])
|
const exp = [...new Tokenizer('a ').readExpression()]
|
||||||
})
|
|
||||||
it('should read expression `a == b`', () => {
|
expect(exp).to.have.lengthOf(1)
|
||||||
const exp = new Tokenizer('a == b').readExpression()
|
expect(exp[0]).to.be.instanceOf(PropertyAccessToken)
|
||||||
expect([...exp]).to.deep.equal(['a', '==', 'b'])
|
expect(exp[0].getText()).to.deep.equal('a')
|
||||||
})
|
})
|
||||||
it('should read expression `(1..3) contains 3`', () => {
|
it('should read expression `a[][b]`', () => {
|
||||||
const exp = new Tokenizer('(1..3) contains 3').readExpression()
|
const exp = [...new Tokenizer('a[][b]').readExpression()]
|
||||||
expect([...exp]).to.deep.equal(['(1..3)', 'contains', '3'])
|
|
||||||
})
|
expect(exp).to.have.lengthOf(1)
|
||||||
it('should read expression `a[b] = c`', () => {
|
const pa = exp[0] as PropertyAccessToken
|
||||||
const exp = new Tokenizer('a[b] = c').readExpression()
|
expect(pa).to.be.instanceOf(PropertyAccessToken)
|
||||||
expect([...exp]).to.deep.equal(['a[b]', '=', 'c'])
|
expect(pa.variable.content).to.deep.equal('a')
|
||||||
})
|
expect(pa.props).to.have.lengthOf(2)
|
||||||
it('should read expression `c[a["b"]] >= c`', () => {
|
|
||||||
const exp = new Tokenizer('c[a["b"]] >= c').readExpression()
|
const [p1, p2] = pa.props
|
||||||
expect([...exp]).to.deep.equal(['c[a["b"]]', '>=', 'c'])
|
expect(p1).to.be.instanceOf(WordToken)
|
||||||
})
|
expect(p1.getText()).to.equal('')
|
||||||
it('should read expression `"][" == var`', () => {
|
expect(p2).to.be.instanceOf(PropertyAccessToken)
|
||||||
const exp = new Tokenizer('"][" == var').readExpression()
|
expect(p2.getText()).to.equal('b')
|
||||||
expect([...exp]).to.deep.equal(['"]["', '==', 'var'])
|
})
|
||||||
})
|
it('should read expression `a.`', () => {
|
||||||
it('should read expression `"\\\'" == "\\""`', () => {
|
const exp = [...new Tokenizer('a.').readExpression()]
|
||||||
const exp = new Tokenizer('"\\\'" == "\\""').readExpression()
|
|
||||||
expect([...exp]).to.deep.equal(['"\\\'"', '==', '"\\""'])
|
expect(exp).to.have.lengthOf(1)
|
||||||
|
const pa = exp[0] as PropertyAccessToken
|
||||||
|
expect(pa).to.be.instanceOf(PropertyAccessToken)
|
||||||
|
expect(pa.variable.content).to.deep.equal('a')
|
||||||
|
expect(pa.props).to.have.lengthOf(0)
|
||||||
|
})
|
||||||
|
it('should read expression `a ==`', () => {
|
||||||
|
const exp = [...new Tokenizer('a ==').readExpression()]
|
||||||
|
|
||||||
|
expect(exp).to.have.lengthOf(1)
|
||||||
|
expect(exp[0]).to.be.instanceOf(PropertyAccessToken)
|
||||||
|
expect(exp[0].getText()).to.deep.equal('a')
|
||||||
|
})
|
||||||
|
it('should read expression `a==b`', () => {
|
||||||
|
const exp = new Tokenizer('a==b').readExpression()
|
||||||
|
const [a, equals, b] = exp
|
||||||
|
|
||||||
|
expect(a).to.be.instanceOf(PropertyAccessToken)
|
||||||
|
expect(a.getText()).to.deep.equal('a')
|
||||||
|
|
||||||
|
expect(equals).to.be.instanceOf(OperatorToken)
|
||||||
|
expect(equals.getText()).to.equal('==')
|
||||||
|
|
||||||
|
expect(b).to.be.instanceOf(PropertyAccessToken)
|
||||||
|
expect(b.getText()).to.deep.equal('b')
|
||||||
|
})
|
||||||
|
it('should read expression `^`', () => {
|
||||||
|
const exp = new Tokenizer('^').readExpression()
|
||||||
|
expect([...exp]).to.deep.equal([])
|
||||||
|
})
|
||||||
|
it('should read expression `a == b`', () => {
|
||||||
|
const exp = new Tokenizer('a == b').readExpression()
|
||||||
|
const [a, equals, b] = exp
|
||||||
|
|
||||||
|
expect(a).to.be.instanceOf(PropertyAccessToken)
|
||||||
|
expect(a.getText()).to.deep.equal('a')
|
||||||
|
|
||||||
|
expect(equals).to.be.instanceOf(OperatorToken)
|
||||||
|
expect(equals.getText()).to.equal('==')
|
||||||
|
|
||||||
|
expect(b).to.be.instanceOf(PropertyAccessToken)
|
||||||
|
expect(b.getText()).to.deep.equal('b')
|
||||||
|
})
|
||||||
|
it('should read expression `(1..3) contains 3`', () => {
|
||||||
|
const exp = new Tokenizer('(1..3) contains 3').readExpression()
|
||||||
|
const [range, contains, rhs] = exp
|
||||||
|
|
||||||
|
expect(range).to.be.instanceOf(RangeToken)
|
||||||
|
expect(range.getText()).to.deep.equal('(1..3)')
|
||||||
|
|
||||||
|
expect(contains).to.be.instanceOf(OperatorToken)
|
||||||
|
expect(contains.getText()).to.equal('contains')
|
||||||
|
|
||||||
|
expect(rhs).to.be.instanceOf(NumberToken)
|
||||||
|
expect(rhs.getText()).to.deep.equal('3')
|
||||||
|
})
|
||||||
|
it('should read expression `a[b] == c`', () => {
|
||||||
|
const exp = new Tokenizer('a[b] == c').readExpression()
|
||||||
|
const [lhs, contains, rhs] = exp
|
||||||
|
|
||||||
|
expect(lhs).to.be.instanceOf(PropertyAccessToken)
|
||||||
|
expect(lhs.getText()).to.deep.equal('a[b]')
|
||||||
|
|
||||||
|
expect(contains).to.be.instanceOf(OperatorToken)
|
||||||
|
expect(contains.getText()).to.equal('==')
|
||||||
|
|
||||||
|
expect(rhs).to.be.instanceOf(PropertyAccessToken)
|
||||||
|
expect(rhs.getText()).to.deep.equal('c')
|
||||||
|
})
|
||||||
|
it('should read expression `c[a["b"]] >= c`', () => {
|
||||||
|
const exp = new Tokenizer('c[a["b"]] >= c').readExpression()
|
||||||
|
const [lhs, op, rhs] = exp
|
||||||
|
|
||||||
|
expect(lhs).to.be.instanceOf(PropertyAccessToken)
|
||||||
|
expect(lhs.getText()).to.deep.equal('c[a["b"]]')
|
||||||
|
|
||||||
|
expect(op).to.be.instanceOf(OperatorToken)
|
||||||
|
expect(op.getText()).to.equal('>=')
|
||||||
|
|
||||||
|
expect(rhs).to.be.instanceOf(PropertyAccessToken)
|
||||||
|
expect(rhs.getText()).to.deep.equal('c')
|
||||||
|
})
|
||||||
|
it('should read expression `"][" == var`', () => {
|
||||||
|
const exp = new Tokenizer('"][" == var').readExpression()
|
||||||
|
const [lhs, equals, rhs] = exp
|
||||||
|
|
||||||
|
expect(lhs).to.be.instanceOf(QuotedToken)
|
||||||
|
expect(lhs.getText()).to.deep.equal('"]["')
|
||||||
|
|
||||||
|
expect(equals).to.be.instanceOf(OperatorToken)
|
||||||
|
expect(equals.getText()).to.equal('==')
|
||||||
|
|
||||||
|
expect(rhs).to.be.instanceOf(PropertyAccessToken)
|
||||||
|
expect(rhs.getText()).to.deep.equal('var')
|
||||||
|
})
|
||||||
|
it('should read expression `"\\\'" == "\\""`', () => {
|
||||||
|
const exp = new Tokenizer('"\\\'" == "\\""').readExpression()
|
||||||
|
const [lhs, equals, rhs] = exp
|
||||||
|
|
||||||
|
expect(lhs).to.be.instanceOf(QuotedToken)
|
||||||
|
expect(lhs.getText()).to.deep.equal('"\\\'"')
|
||||||
|
|
||||||
|
expect(equals).to.be.instanceOf(OperatorToken)
|
||||||
|
expect(equals.getText()).to.equal('==')
|
||||||
|
|
||||||
|
expect(rhs).to.be.instanceOf(QuotedToken)
|
||||||
|
expect(rhs.getText()).to.deep.equal('"\\""')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,59 +4,115 @@ import { Context } from '../../../src/context/context'
|
|||||||
import { toThenable } from '../../../src/util/async'
|
import { toThenable } from '../../../src/util/async'
|
||||||
|
|
||||||
describe('Expression', function () {
|
describe('Expression', function () {
|
||||||
let ctx: Context
|
const ctx = new Context({})
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
ctx = new Context({
|
|
||||||
one: 1,
|
|
||||||
two: 2,
|
|
||||||
empty: '',
|
|
||||||
quote: '"',
|
|
||||||
space: ' ',
|
|
||||||
x: 'XXX',
|
|
||||||
y: undefined,
|
|
||||||
z: null,
|
|
||||||
obj: {
|
|
||||||
']': 'right bracket'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should throw when context not defined', done => {
|
it('should throw when context not defined', done => {
|
||||||
toThenable(new Expression().value(undefined!)).catch(err => {
|
toThenable(new Expression('foo').value(undefined!))
|
||||||
expect(err.message).to.match(/context not defined/)
|
.then(() => done(new Error('should not resolved')))
|
||||||
done()
|
.catch(err => {
|
||||||
return 0 as any
|
expect(err.message).to.match(/context not defined/)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('single value', function () {
|
||||||
|
it('should eval literal', async function () {
|
||||||
|
expect(await toThenable(new Expression('2.4').value(ctx))).to.equal(2.4)
|
||||||
|
expect(await toThenable(new Expression('"foo"').value(ctx))).to.equal('foo')
|
||||||
|
expect(await toThenable(new Expression('false').value(ctx))).to.equal(false)
|
||||||
|
})
|
||||||
|
it('should eval range expression', async function () {
|
||||||
|
const ctx = new Context({ two: 2 })
|
||||||
|
expect(await toThenable(new Expression('(2..4)').value(ctx))).to.deep.equal([2, 3, 4])
|
||||||
|
expect(await toThenable(new Expression('(two..4)').value(ctx))).to.deep.equal([2, 3, 4])
|
||||||
|
})
|
||||||
|
it('should eval literal', async function () {
|
||||||
|
expect(await toThenable(new Expression('2.4').value(ctx))).to.equal(2.4)
|
||||||
|
expect(await toThenable(new Expression('"foo"').value(ctx))).to.equal('foo')
|
||||||
|
expect(await toThenable(new Expression('false').value(ctx))).to.equal(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should eval property access', async function () {
|
||||||
|
const ctx = new Context({
|
||||||
|
foo: { bar: 'BAR' },
|
||||||
|
coo: 'bar',
|
||||||
|
doo: { foo: 'bar', bar: { foo: 'bar' } }
|
||||||
|
})
|
||||||
|
expect(await toThenable(new Expression('foo.bar').value(ctx))).to.equal('BAR')
|
||||||
|
expect(await toThenable(new Expression('foo["bar"]').value(ctx))).to.equal('BAR')
|
||||||
|
expect(await toThenable(new Expression('foo[coo]').value(ctx))).to.equal('BAR')
|
||||||
|
expect(await toThenable(new Expression('foo[doo.foo]').value(ctx))).to.equal('BAR')
|
||||||
|
expect(await toThenable(new Expression('foo[doo["foo"]]').value(ctx))).to.equal('BAR')
|
||||||
|
expect(await toThenable(new Expression('doo[coo].foo').value(ctx))).to.equal('bar')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should eval simple expression', async function () {
|
describe('simple expression', function () {
|
||||||
expect(await toThenable(new Expression('1==2').value(ctx))).to.equal(false)
|
it('should return false for "1==2"', async () => {
|
||||||
expect(await toThenable(new Expression('1<2').value(ctx))).to.equal(true)
|
expect(await toThenable(new Expression('1==2').value(ctx))).to.equal(false)
|
||||||
expect(await toThenable(new Expression('1 < 2').value(ctx))).to.equal(true)
|
})
|
||||||
expect(await toThenable(new Expression('1 < 2').value(ctx))).to.equal(true)
|
it('should return true for "1<2"', async () => {
|
||||||
expect(await toThenable(new Expression('2 <= 2').value(ctx))).to.equal(true)
|
expect(await toThenable(new Expression('1<2').value(ctx))).to.equal(true)
|
||||||
expect(await toThenable(new Expression('one <= two').value(ctx))).to.equal(true)
|
})
|
||||||
expect(await toThenable(new Expression('x contains "x"').value(ctx))).to.equal(false)
|
it('should return true for "1 < 2"', async () => {
|
||||||
expect(await toThenable(new Expression('x contains "X"').value(ctx))).to.equal(true)
|
expect(await toThenable(new Expression('1 < 2').value(ctx))).to.equal(true)
|
||||||
expect(await toThenable(new Expression('1 contains "x"').value(ctx))).to.equal(false)
|
})
|
||||||
expect(await toThenable(new Expression('y contains "x"').value(ctx))).to.equal(false)
|
it('should return true for "1 < 2"', async () => {
|
||||||
expect(await toThenable(new Expression('z contains "x"').value(ctx))).to.equal(false)
|
expect(await toThenable(new Expression('1 < 2').value(ctx))).to.equal(true)
|
||||||
expect(await toThenable(new Expression('(1..5) contains 3').value(ctx))).to.equal(true)
|
})
|
||||||
expect(await toThenable(new Expression('(1..5) contains 6').value(ctx))).to.equal(false)
|
it('should return true for "2 <= 2"', async () => {
|
||||||
expect(await toThenable(new Expression('"<=" == "<="').value(ctx))).to.equal(true)
|
expect(await toThenable(new Expression('2 <= 2').value(ctx))).to.equal(true)
|
||||||
|
})
|
||||||
|
it('should return true for "one <= two"', async () => {
|
||||||
|
const ctx = new Context({ one: 1, two: 2 })
|
||||||
|
expect(await toThenable(new Expression('one <= two').value(ctx))).to.equal(true)
|
||||||
|
})
|
||||||
|
it('should return false for "x contains "x""', async () => {
|
||||||
|
const ctx = new Context({ x: 'XXX' })
|
||||||
|
expect(await toThenable(new Expression('x contains "x"').value(ctx))).to.equal(false)
|
||||||
|
})
|
||||||
|
it('should return true for "x contains "X""', async () => {
|
||||||
|
const ctx = new Context({ x: 'XXX' })
|
||||||
|
expect(await toThenable(new Expression('x contains "X"').value(ctx))).to.equal(true)
|
||||||
|
})
|
||||||
|
it('should return false for "1 contains "x""', async () => {
|
||||||
|
const ctx = new Context({ x: 'XXX' })
|
||||||
|
expect(await toThenable(new Expression('1 contains "x"').value(ctx))).to.equal(false)
|
||||||
|
})
|
||||||
|
it('should return false for "y contains "x""', async () => {
|
||||||
|
const ctx = new Context({ x: 'XXX' })
|
||||||
|
expect(await toThenable(new Expression('y contains "x"').value(ctx))).to.equal(false)
|
||||||
|
})
|
||||||
|
it('should return false for "z contains "x""', async () => {
|
||||||
|
const ctx = new Context({ x: 'XXX' })
|
||||||
|
expect(await toThenable(new Expression('z contains "x"').value(ctx))).to.equal(false)
|
||||||
|
})
|
||||||
|
it('should return true for "(1..5) contains 3"', async () => {
|
||||||
|
const ctx = new Context({ x: 'XXX' })
|
||||||
|
expect(await toThenable(new Expression('(1..5) contains 3').value(ctx))).to.equal(true)
|
||||||
|
})
|
||||||
|
it('should return false for "(1..5) contains 6"', async () => {
|
||||||
|
const ctx = new Context({ x: 'XXX' })
|
||||||
|
expect(await toThenable(new Expression('(1..5) contains 6').value(ctx))).to.equal(false)
|
||||||
|
})
|
||||||
|
it('should return true for ""<=" == "<=""', async () => {
|
||||||
|
expect(await toThenable(new Expression('"<=" == "<="').value(ctx))).to.equal(true)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should allow space in quoted value', async function () {
|
it('should allow space in quoted value', async function () {
|
||||||
|
const ctx = new Context({ space: ' ' })
|
||||||
expect(await toThenable(new Expression('" " == space').value(ctx))).to.equal(true)
|
expect(await toThenable(new Expression('" " == space').value(ctx))).to.equal(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('escape', () => {
|
describe('escape', () => {
|
||||||
it('should escape quote', async function () {
|
it('should escape quote', async function () {
|
||||||
|
const ctx = new Context({ quote: '"' })
|
||||||
expect(await toThenable(new Expression('"\\"" == quote').value(ctx))).to.equal(true)
|
expect(await toThenable(new Expression('"\\"" == quote').value(ctx))).to.equal(true)
|
||||||
})
|
})
|
||||||
it('should escape square bracket', async function () {
|
it('should escape square bracket', async function () {
|
||||||
expect(await toThenable(new Expression('obj["]"] == "right bracket"').value(ctx))).to.equal(true)
|
const ctx = new Context({ obj: { ']': 'bracket' } })
|
||||||
|
expect(await toThenable(new Expression('obj["]"] == "bracket"').value(ctx))).to.equal(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -71,6 +127,7 @@ describe('Expression', function () {
|
|||||||
expect(await toThenable(new Expression('1 < 2 or x contains "x"').value(ctx))).to.equal(true)
|
expect(await toThenable(new Expression('1 < 2 or x contains "x"').value(ctx))).to.equal(true)
|
||||||
})
|
})
|
||||||
it('should support value and !=', async function () {
|
it('should support value and !=', async function () {
|
||||||
|
const ctx = new Context({ empty: '' })
|
||||||
expect(await toThenable(new Expression('empty and empty != ""').value(ctx))).to.equal(false)
|
expect(await toThenable(new Expression('empty and empty != ""').value(ctx))).to.equal(false)
|
||||||
})
|
})
|
||||||
it('should recognize quoted value', async function () {
|
it('should recognize quoted value', async function () {
|
||||||
@@ -84,10 +141,9 @@ describe('Expression', function () {
|
|||||||
const ctx = new Context({ obj: { foo: true } })
|
const ctx = new Context({ obj: { foo: true } })
|
||||||
expect(await toThenable(new Expression('obj["foo"] and true').value(ctx))).to.equal(true)
|
expect(await toThenable(new Expression('obj["foo"] and true').value(ctx))).to.equal(true)
|
||||||
})
|
})
|
||||||
})
|
it('should allow nested property access', async function () {
|
||||||
|
const ctx = new Context({ obj: { foo: 'FOO' }, keys: { "what's this": 'foo' } })
|
||||||
it('should eval range expression', async function () {
|
expect(await toThenable(new Expression('obj[keys["what\'s this"]]').value(ctx))).to.equal('FOO')
|
||||||
expect(await toThenable(new Expression('(2..4)').value(ctx))).to.deep.equal([2, 3, 4])
|
})
|
||||||
expect(await toThenable(new Expression('(two..4)').value(ctx))).to.deep.equal([2, 3, 4])
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { Context } from '../../../src/context/context'
|
import { Context } from '../../../src/context/context'
|
||||||
import { Token } from '../../../src/parser/token'
|
import { HTMLToken } from '../../../src/tokens/html-token'
|
||||||
import { Render } from '../../../src/render/render'
|
import { Render } from '../../../src/render/render'
|
||||||
import { HTML } from '../../../src/template/html'
|
import { HTML } from '../../../src/template/html'
|
||||||
import { toThenable } from '../../../src/util/async'
|
import { toThenable } from '../../../src/util/async'
|
||||||
@@ -14,7 +14,7 @@ describe('render', function () {
|
|||||||
describe('.renderTemplates()', function () {
|
describe('.renderTemplates()', function () {
|
||||||
it('should render html', async function () {
|
it('should render html', async function () {
|
||||||
const scope = new Context()
|
const scope = new Context()
|
||||||
const token = { content: '<p>' } as Token
|
const token = { getContent: () => '<p>' } as HTMLToken
|
||||||
const html = await toThenable(render.renderTemplates([new HTML(token)], scope))
|
const html = await toThenable(render.renderTemplates([new HTML(token)], scope))
|
||||||
return expect(html).to.equal('<p>')
|
return expect(html).to.equal('<p>')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import * as sinon from 'sinon'
|
|||||||
import * as sinonChai from 'sinon-chai'
|
import * as sinonChai from 'sinon-chai'
|
||||||
import { Context } from '../../../../src/context/context'
|
import { Context } from '../../../../src/context/context'
|
||||||
import { toThenable } from '../../../../src/util/async'
|
import { toThenable } from '../../../../src/util/async'
|
||||||
|
import { NumberToken } from '../../../../src/tokens/number-token'
|
||||||
|
import { QuotedToken } from '../../../../src/tokens/quoted-token'
|
||||||
|
import { WordToken } from '../../../../src/tokens/word-token'
|
||||||
import { FilterMap } from '../../../../src/template/filter/filter-map'
|
import { FilterMap } from '../../../../src/template/filter/filter-map'
|
||||||
|
|
||||||
chai.use(sinonChai)
|
chai.use(sinonChai)
|
||||||
@@ -27,13 +30,15 @@ describe('filter', function () {
|
|||||||
it('should call filter impl with correct arguments', async function () {
|
it('should call filter impl with correct arguments', async function () {
|
||||||
const spy = sinon.spy()
|
const spy = sinon.spy()
|
||||||
filters.set('foo', spy)
|
filters.set('foo', spy)
|
||||||
await toThenable(filters.create('foo', ['33']).render('foo', ctx))
|
const thirty = new NumberToken(new WordToken('30', 0, 2), undefined)
|
||||||
expect(spy).to.have.been.calledWith('foo', 33)
|
await toThenable(filters.create('foo', [thirty]).render('foo', ctx))
|
||||||
|
expect(spy).to.have.been.calledWith('foo', 30)
|
||||||
})
|
})
|
||||||
it('should call filter impl with correct this arg', async function () {
|
it('should call filter impl with correct this arg', async function () {
|
||||||
const spy = sinon.spy()
|
const spy = sinon.spy()
|
||||||
filters.set('foo', spy)
|
filters.set('foo', spy)
|
||||||
await toThenable(filters.create('foo', ['33']).render('foo', ctx))
|
const thirty = new NumberToken(new WordToken('33', 0, 2), undefined)
|
||||||
|
await toThenable(filters.create('foo', [thirty]).render('foo', ctx))
|
||||||
expect(spy).to.have.been.calledOn(sinon.match.has('context', ctx))
|
expect(spy).to.have.been.calledOn(sinon.match.has('context', ctx))
|
||||||
})
|
})
|
||||||
it('should render a simple filter', async function () {
|
it('should render a simple filter', async function () {
|
||||||
@@ -43,12 +48,15 @@ describe('filter', function () {
|
|||||||
|
|
||||||
it('should render filters with argument', async function () {
|
it('should render filters with argument', async function () {
|
||||||
filters.set('add', (a, b) => a + b)
|
filters.set('add', (a, b) => a + b)
|
||||||
expect(await toThenable(filters.create('add', ['2']).render(3, ctx))).to.equal(5)
|
const two = new NumberToken(new WordToken('2', 0, 1), undefined)
|
||||||
|
expect(await toThenable(filters.create('add', [two]).render(3, ctx))).to.equal(5)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render filters with multiple arguments', async function () {
|
it('should render filters with multiple arguments', async function () {
|
||||||
filters.set('add', (a, b, c) => a + b + c)
|
filters.set('add', (a, b, c) => a + b + c)
|
||||||
expect(await toThenable(filters.create('add', ['2', '"c"']).render(3, ctx))).to.equal('5c')
|
const two = new NumberToken(new WordToken('2', 0, 1), undefined)
|
||||||
|
const c = new QuotedToken('"c"', 0, 3)
|
||||||
|
expect(await toThenable(filters.create('add', [two, c]).render(3, ctx))).to.equal('5c')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should pass Objects/Drops as it is', async function () {
|
it('should pass Objects/Drops as it is', async function () {
|
||||||
@@ -65,6 +73,7 @@ describe('filter', function () {
|
|||||||
|
|
||||||
it('should support key value pairs', async function () {
|
it('should support key value pairs', async function () {
|
||||||
filters.set('add', (a, b) => b[0] + ':' + (a + b[1]))
|
filters.set('add', (a, b) => b[0] + ':' + (a + b[1]))
|
||||||
expect(await toThenable((filters.create('add', [['num', '2']]).render(3, ctx)))).to.equal('num:5')
|
const two = new NumberToken(new WordToken('2', 0, 1), undefined)
|
||||||
|
expect(await toThenable((filters.create('add', [['num', two]]).render(3, ctx)))).to.equal('num:5')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ describe('Hash', function () {
|
|||||||
expect(hash.num).to.equal(2.3)
|
expect(hash.num).to.equal(2.3)
|
||||||
})
|
})
|
||||||
it('should parse "num:bar.coo"', async function () {
|
it('should parse "num:bar.coo"', async function () {
|
||||||
const hash = await toThenable(new Hash('num:bar.coo').render(new Context({ bar: { coo: 3 } })))
|
const pending = new Hash('num:bar.coo').render(new Context({ bar: { coo: 3 } }))
|
||||||
|
const hash = await toThenable(pending)
|
||||||
expect(hash.num).to.equal(3)
|
expect(hash.num).to.equal(3)
|
||||||
})
|
})
|
||||||
it('should parse "num1:2.3 reverse,num2:bar.coo\n num3: arr[0]"', async function () {
|
it('should parse "num1:2.3 reverse,num2:bar.coo\n num3: arr[0]"', async function () {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import * as chai from 'chai'
|
|||||||
import { toThenable } from '../../../src/util/async'
|
import { toThenable } from '../../../src/util/async'
|
||||||
import { Context } from '../../../src/context/context'
|
import { Context } from '../../../src/context/context'
|
||||||
import { Output } from '../../../src/template/output'
|
import { Output } from '../../../src/template/output'
|
||||||
import { OutputToken } from '../../../src/parser/output-token'
|
import { OutputToken } from '../../../src/tokens/output-token'
|
||||||
import { FilterMap } from '../../../src/template/filter/filter-map'
|
import { FilterMap } from '../../../src/template/filter/filter-map'
|
||||||
|
|
||||||
const expect = chai.expect
|
const expect = chai.expect
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Tag } from '../../../src/template/tag/tag'
|
|||||||
import { Context } from '../../../src/context/context'
|
import { Context } from '../../../src/context/context'
|
||||||
import * as sinon from 'sinon'
|
import * as sinon from 'sinon'
|
||||||
import * as sinonChai from 'sinon-chai'
|
import * as sinonChai from 'sinon-chai'
|
||||||
import { TagToken } from '../../../src/parser/tag-token'
|
import { TagToken } from '../../../src/tokens/tag-token'
|
||||||
import { toThenable } from '../../../src/util/async'
|
import { toThenable } from '../../../src/util/async'
|
||||||
|
|
||||||
chai.use(sinonChai)
|
chai.use(sinonChai)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as chai from 'chai'
|
import * as chai from 'chai'
|
||||||
|
import { QuotedToken } from '../../../src/tokens/quoted-token'
|
||||||
import { toThenable } from '../../../src/util/async'
|
import { toThenable } from '../../../src/util/async'
|
||||||
import { FilterMap } from '../../../src/template/filter/filter-map'
|
import { FilterMap } from '../../../src/template/filter/filter-map'
|
||||||
import * as sinonChai from 'sinon-chai'
|
import * as sinonChai from 'sinon-chai'
|
||||||
@@ -15,71 +16,17 @@ describe('Value', function () {
|
|||||||
const filterMap = new FilterMap(false)
|
const filterMap = new FilterMap(false)
|
||||||
it('should parse "foo', function () {
|
it('should parse "foo', function () {
|
||||||
const tpl = new Value('foo', filterMap)
|
const tpl = new Value('foo', filterMap)
|
||||||
expect(tpl.initial).to.equal('foo')
|
expect(tpl.initial!.getText()).to.equal('foo')
|
||||||
expect(tpl.filters).to.deep.equal([])
|
expect(tpl.filters).to.deep.equal([])
|
||||||
})
|
})
|
||||||
|
it('should parse filters in value content', function () {
|
||||||
it('should parse "foo | add"', function () {
|
|
||||||
const tpl = new Value('foo | add', filterMap)
|
|
||||||
expect(tpl.initial).to.equal('foo')
|
|
||||||
expect(tpl.filters.length).to.equal(1)
|
|
||||||
expect(tpl.filters[0].args).to.eql([])
|
|
||||||
})
|
|
||||||
it('should parse "foo,foo | add"', function () {
|
|
||||||
const tpl = new Value('foo,foo | add', filterMap)
|
|
||||||
expect(tpl.initial).to.equal('foo')
|
|
||||||
expect(tpl.filters.length).to.equal(1)
|
|
||||||
expect(tpl.filters[0].args).to.eql([])
|
|
||||||
})
|
|
||||||
it('should parse "foo | add: 3, false"', function () {
|
|
||||||
const tpl = new Value('foo | add: 3, "foo"', filterMap)
|
|
||||||
expect(tpl.initial).to.equal('foo')
|
|
||||||
expect(tpl.filters.length).to.equal(1)
|
|
||||||
expect(tpl.filters[0].args).to.eql(['3', '"foo"'])
|
|
||||||
})
|
|
||||||
it('should parse "foo | add: "foo" bar, 3"', function () {
|
|
||||||
const tpl = new Value('foo | add: "foo" bar, 3', filterMap)
|
|
||||||
expect(tpl.initial).to.equal('foo')
|
|
||||||
expect(tpl.filters.length).to.equal(1)
|
|
||||||
expect(tpl.filters[0].name).to.eql('add')
|
|
||||||
expect(tpl.filters[0].args).to.eql(['"foo"', '3'])
|
|
||||||
})
|
|
||||||
it('should parse "foo | add: "|", 3', function () {
|
|
||||||
const tpl = new Value('foo | add: "|", 3', filterMap)
|
|
||||||
expect(tpl.initial).to.equal('foo')
|
|
||||||
expect(tpl.filters.length).to.equal(1)
|
|
||||||
expect(tpl.filters[0].args).to.eql(['"|"', '3'])
|
|
||||||
})
|
|
||||||
it('should parse "foo | add: "|", 3', function () {
|
|
||||||
const tpl = new Value('foo | add: "|", 3', filterMap)
|
|
||||||
expect(tpl.initial).to.equal('foo')
|
|
||||||
expect(tpl.filters.length).to.equal(1)
|
|
||||||
expect(tpl.filters[0].args).to.eql(['"|"', '3'])
|
|
||||||
})
|
|
||||||
it('should support arguments as named key/values', function () {
|
|
||||||
const f = new Value('o | foo: key1: "literal1", key2: value2', filterMap)
|
|
||||||
expect(f.filters[0].name).to.equal('foo')
|
|
||||||
expect(f.filters[0].args).to.eql([['key1', '"literal1"'], ['key2', 'value2']])
|
|
||||||
})
|
|
||||||
it('should support arguments as named key/values with inline literals', function () {
|
|
||||||
const f = new Value('o | foo: "test0", key1: "literal1", key2: value2', filterMap)
|
|
||||||
expect(f.filters[0].name).to.equal('foo')
|
|
||||||
expect(f.filters[0].args).to.deep.equal(['"test0"', ['key1', '"literal1"'], ['key2', 'value2']])
|
|
||||||
})
|
|
||||||
it('should support arguments as named key/values with inline values', function () {
|
|
||||||
const f = new Value('o | foo: test0, key1: "literal1", key2: value2', filterMap)
|
|
||||||
expect(f.filters[0].name).to.equal('foo')
|
|
||||||
expect(f.filters[0].args).to.deep.equal(['test0', ['key1', '"literal1"'], ['key2', 'value2']])
|
|
||||||
})
|
|
||||||
it('should support argument values named same as keys', function () {
|
|
||||||
const f = new Value('o | foo: a: a', filterMap)
|
|
||||||
expect(f.filters[0].name).to.equal('foo')
|
|
||||||
expect(f.filters[0].args).to.deep.equal([['a', 'a']])
|
|
||||||
})
|
|
||||||
it('should support argument literals named same as keys', function () {
|
|
||||||
const f = new Value('o | foo: a: "a"', filterMap)
|
const f = new Value('o | foo: a: "a"', filterMap)
|
||||||
expect(f.filters[0].name).to.equal('foo')
|
expect(f.filters[0].name).to.equal('foo')
|
||||||
expect(f.filters[0].args).to.deep.equal([['a', '"a"']])
|
expect(f.filters[0].args).to.have.lengthOf(1)
|
||||||
|
const [k, v] = f.filters[0].args[0] as any
|
||||||
|
expect(k).to.equal('a')
|
||||||
|
expect(v).to.be.instanceOf(QuotedToken)
|
||||||
|
expect((v as QuotedToken).getText()).to.equal('"a"')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ const expect = chai.expect
|
|||||||
|
|
||||||
describe('assert', function () {
|
describe('assert', function () {
|
||||||
it('should not throw if predicate is truthy', function () {
|
it('should not throw if predicate is truthy', function () {
|
||||||
const fn = () => assert('foo', 'bar')
|
const fn = () => assert('foo', () => 'bar')
|
||||||
expect(fn).to.not.throw()
|
expect(fn).to.not.throw()
|
||||||
})
|
})
|
||||||
it('should not throw if predicate is truthy', function () {
|
it('should not throw if predicate is truthy', function () {
|
||||||
const fn = () => assert('', 'bar')
|
const fn = () => assert('', () => 'bar')
|
||||||
expect(fn).to.throw(/bar/)
|
expect(fn).to.throw(/bar/)
|
||||||
})
|
})
|
||||||
it('should populate default message', function () {
|
it('should populate default message', function () {
|
||||||
|
|||||||
Reference in New Issue
Block a user