mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-15 04:10:40 -07:00
perf: parse filenames in parse() insteadof render()
This commit is contained in:
@@ -1,14 +1,14 @@
|
|||||||
import { assert, evalQuotedToken, TypeGuards, Tokenizer, evalToken, Hash, Emitter, TagToken, Context, TagImplOptions } from '../../types'
|
import { assert, Tokenizer, evalToken, Hash, Emitter, TagToken, Context, TagImplOptions } from '../../types'
|
||||||
import BlockMode from '../../context/block-mode'
|
import BlockMode from '../../context/block-mode'
|
||||||
|
import { parseFilePath, renderFilePath } from './render'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
parseFilePath,
|
||||||
|
renderFilePath,
|
||||||
parse: function (token: TagToken) {
|
parse: function (token: TagToken) {
|
||||||
const args = token.args
|
const args = token.args
|
||||||
const tokenizer = new Tokenizer(args, this.liquid.options.operatorsTrie)
|
const tokenizer = new Tokenizer(args, this.liquid.options.operatorsTrie)
|
||||||
this.file = this.liquid.options.dynamicPartials
|
this['file'] = this.parseFilePath(tokenizer, this.liquid)
|
||||||
? tokenizer.readValue()
|
|
||||||
: tokenizer.readFileName()
|
|
||||||
assert(this.file, () => `illegal argument "${token.args}"`)
|
|
||||||
|
|
||||||
const begin = tokenizer.p
|
const begin = tokenizer.p
|
||||||
const withStr = tokenizer.readIdentifier()
|
const withStr = tokenizer.readIdentifier()
|
||||||
@@ -22,15 +22,10 @@ export default {
|
|||||||
this.hash = new Hash(tokenizer.remaining())
|
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 } = this
|
||||||
const { renderer } = liquid
|
const { renderer } = liquid
|
||||||
// TODO try move all liquid.parse calls into parse() section
|
const filepath = yield this.renderFilePath(this['file'], ctx, liquid)
|
||||||
const filepath = ctx.opts.dynamicPartials
|
assert(filepath, () => `illegal filename "${filepath}"`)
|
||||||
? (TypeGuards.isQuotedToken(file)
|
|
||||||
? yield renderer.renderTemplates(liquid.parse(evalQuotedToken(file)), ctx)
|
|
||||||
: yield evalToken(file, ctx))
|
|
||||||
: file.getText()
|
|
||||||
assert(filepath, () => `illegal filename "${file.getText()}":"${filepath}"`)
|
|
||||||
|
|
||||||
const saved = ctx.saveRegister('blocks', 'blockMode')
|
const saved = ctx.saveRegister('blocks', 'blockMode')
|
||||||
ctx.setRegister('blocks', {})
|
ctx.setRegister('blocks', {})
|
||||||
|
|||||||
@@ -1,31 +1,27 @@
|
|||||||
import { assert, evalQuotedToken, TypeGuards, evalToken, Tokenizer, Emitter, Hash, TagToken, TopLevelToken, Context, TagImplOptions } from '../../types'
|
import { assert, Tokenizer, Emitter, Hash, TagToken, TopLevelToken, Context, TagImplOptions } from '../../types'
|
||||||
import BlockMode from '../../context/block-mode'
|
import BlockMode from '../../context/block-mode'
|
||||||
|
import { parseFilePath, renderFilePath } from './render'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
parseFilePath,
|
||||||
|
renderFilePath,
|
||||||
parse: function (token: TagToken, remainTokens: TopLevelToken[]) {
|
parse: function (token: TagToken, remainTokens: TopLevelToken[]) {
|
||||||
const tokenizer = new Tokenizer(token.args, this.liquid.options.operatorsTrie)
|
const tokenizer = new Tokenizer(token.args, this.liquid.options.operatorsTrie)
|
||||||
const file = this.liquid.options.dynamicPartials ? tokenizer.readValue() : tokenizer.readFileName()
|
this['file'] = this.parseFilePath(tokenizer, this.liquid)
|
||||||
assert(file, () => `illegal argument "${token.args}"`)
|
|
||||||
|
|
||||||
this.file = file
|
|
||||||
this.hash = new Hash(tokenizer.remaining())
|
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
|
||||||
if (file.getText() === 'none') {
|
if (file === null) {
|
||||||
ctx.setRegister('blockMode', BlockMode.OUTPUT)
|
ctx.setRegister('blockMode', BlockMode.OUTPUT)
|
||||||
const html = yield renderer.renderTemplates(this.tpls, ctx)
|
const html = yield renderer.renderTemplates(this.tpls, ctx)
|
||||||
emitter.write(html)
|
emitter.write(html)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const filepath = ctx.opts.dynamicPartials
|
const filepath = yield this.renderFilePath(this['file'], ctx, liquid)
|
||||||
? (TypeGuards.isQuotedToken(file)
|
assert(filepath, () => `illegal filename "${filepath}"`)
|
||||||
? yield renderer.renderTemplates(liquid.parse(evalQuotedToken(file)), ctx)
|
|
||||||
: evalToken(this.file, ctx))
|
|
||||||
: file.getText()
|
|
||||||
assert(filepath, () => `file "${file.getText()}"("${filepath}") not available`)
|
|
||||||
const templates = yield liquid.parseFileImpl(filepath, ctx.sync)
|
const templates = yield liquid.parseFileImpl(filepath, ctx.sync)
|
||||||
|
|
||||||
// render remaining contents and store rendered results
|
// render remaining contents and store rendered results
|
||||||
|
|||||||
+45
-15
@@ -1,16 +1,16 @@
|
|||||||
import { assert } from '../../util/assert'
|
import { assert } from '../../util/assert'
|
||||||
import { ForloopDrop } from '../../drop/forloop-drop'
|
import { ForloopDrop } from '../../drop/forloop-drop'
|
||||||
import { toEnumerable } from '../../util/collection'
|
import { toEnumerable } from '../../util/collection'
|
||||||
import { evalQuotedToken, TypeGuards, Tokenizer, evalToken, Hash, Emitter, TagToken, Context, TagImplOptions } from '../../types'
|
import { Liquid } from '../../liquid'
|
||||||
|
import { Token, Template, evalQuotedToken, TypeGuards, Tokenizer, evalToken, Hash, Emitter, TagToken, Context, TagImplOptions } from '../../types'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
parseFilePath,
|
||||||
|
renderFilePath,
|
||||||
parse: function (token: TagToken) {
|
parse: function (token: TagToken) {
|
||||||
const args = token.args
|
const args = token.args
|
||||||
const tokenizer = new Tokenizer(args, this.liquid.options.operatorsTrie)
|
const tokenizer = new Tokenizer(args, this.liquid.options.operatorsTrie)
|
||||||
this.file = this.liquid.options.dynamicPartials
|
this['file'] = this.parseFilePath(tokenizer, this.liquid)
|
||||||
? tokenizer.readValue()
|
|
||||||
: tokenizer.readFileName()
|
|
||||||
assert(this.file, () => `illegal argument "${token.args}"`)
|
|
||||||
|
|
||||||
while (!tokenizer.end()) {
|
while (!tokenizer.end()) {
|
||||||
tokenizer.skipBlank()
|
tokenizer.skipBlank()
|
||||||
@@ -40,14 +40,9 @@ export default {
|
|||||||
this.hash = new Hash(tokenizer.remaining())
|
this.hash = new Hash(tokenizer.remaining())
|
||||||
},
|
},
|
||||||
render: function * (ctx: Context, emitter: Emitter) {
|
render: function * (ctx: Context, emitter: Emitter) {
|
||||||
const { liquid, file, hash } = this
|
const { liquid, hash } = this
|
||||||
const { renderer } = liquid
|
const filepath = yield this.renderFilePath(this['file'], ctx, liquid)
|
||||||
const filepath = ctx.opts.dynamicPartials
|
assert(filepath, () => `illegal filename "${filepath}"`)
|
||||||
? (TypeGuards.isQuotedToken(file)
|
|
||||||
? yield renderer.renderTemplates(liquid.parse(evalQuotedToken(file)), ctx)
|
|
||||||
: evalToken(file, ctx))
|
|
||||||
: file.getText()
|
|
||||||
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)
|
||||||
@@ -65,12 +60,47 @@ export default {
|
|||||||
for (const item of collection) {
|
for (const item of collection) {
|
||||||
scope[alias] = item
|
scope[alias] = item
|
||||||
const templates = yield liquid.parseFileImpl(filepath, childCtx.sync)
|
const templates = yield liquid.parseFileImpl(filepath, childCtx.sync)
|
||||||
yield renderer.renderTemplates(templates, childCtx, emitter)
|
yield liquid.renderer.renderTemplates(templates, childCtx, emitter)
|
||||||
scope.forloop.next()
|
scope.forloop.next()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const templates = yield liquid.parseFileImpl(filepath, childCtx.sync)
|
const templates = yield liquid.parseFileImpl(filepath, childCtx.sync)
|
||||||
yield renderer.renderTemplates(templates, childCtx, emitter)
|
yield liquid.renderer.renderTemplates(templates, childCtx, emitter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} as TagImplOptions
|
} as TagImplOptions
|
||||||
|
|
||||||
|
type ParsedFileName = Template[] | Token | string | undefined
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return null for "none",
|
||||||
|
* @return Template[] for quoted with tags and/or filters
|
||||||
|
* @return Token for expression (not quoted)
|
||||||
|
* @throws TypeError if cannot read next token
|
||||||
|
*/
|
||||||
|
export function parseFilePath (tokenizer: Tokenizer, liquid: Liquid): ParsedFileName | null {
|
||||||
|
if (liquid.options.dynamicPartials) {
|
||||||
|
const file = tokenizer.readValue()
|
||||||
|
if (file === undefined) throw new TypeError(`illegal argument "${tokenizer.input}"`)
|
||||||
|
if (file.getText() === 'none') return null
|
||||||
|
// for filenames like "files/{{file}}", eval as liquid template
|
||||||
|
if (TypeGuards.isQuotedToken(file)) {
|
||||||
|
const tpls = liquid.parse(evalQuotedToken(file))
|
||||||
|
// for filenames like "files/file.liquid", extract the string directly
|
||||||
|
if (tpls.length === 1) {
|
||||||
|
const first = tpls[0]
|
||||||
|
if (TypeGuards.isHTMLToken(first)) return first.getText()
|
||||||
|
}
|
||||||
|
return tpls
|
||||||
|
}
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
const filepath = tokenizer.readFileName().getText()
|
||||||
|
return filepath === 'none' ? null : filepath
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderFilePath (file: ParsedFileName, ctx: Context, liquid: Liquid) {
|
||||||
|
if (typeof file === 'string') return file
|
||||||
|
if (Array.isArray(file)) return liquid.renderer.renderTemplates(file, ctx)
|
||||||
|
return evalToken(file, ctx)
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export class Tokenizer {
|
|||||||
private rawBeginAt = -1
|
private rawBeginAt = -1
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private input: string,
|
public input: string,
|
||||||
private trie: Trie,
|
private trie: Trie,
|
||||||
private file: string = ''
|
private file: string = ''
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export class Emitter {
|
|||||||
if (this.keepOutputType === true) {
|
if (this.keepOutputType === true) {
|
||||||
html = toValue(html)
|
html = toValue(html)
|
||||||
} else {
|
} else {
|
||||||
html = stringify(toValue(html))
|
html = stringify(html)
|
||||||
}
|
}
|
||||||
// This will only preserve the type if the value is isolated.
|
// This will only preserve the type if the value is isolated.
|
||||||
// I.E:
|
// I.E:
|
||||||
|
|||||||
@@ -47,10 +47,9 @@ export function evalToken (token: Token | undefined, ctx: Context, lenient = fal
|
|||||||
}
|
}
|
||||||
|
|
||||||
function evalPropertyAccessToken (token: PropertyAccessToken, ctx: Context, lenient: boolean) {
|
function evalPropertyAccessToken (token: PropertyAccessToken, ctx: Context, lenient: boolean) {
|
||||||
const variable = token.getVariableAsText()
|
|
||||||
const props: string[] = token.props.map(prop => evalToken(prop, ctx, false))
|
const props: string[] = token.props.map(prop => evalToken(prop, ctx, false))
|
||||||
try {
|
try {
|
||||||
return ctx.get([variable, ...props])
|
return ctx.get([token.propertyName, ...props])
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (lenient && e.name === 'InternalUndefinedVariableError') return null
|
if (lenient && e.name === 'InternalUndefinedVariableError') return null
|
||||||
throw (new UndefinedVariableError(e, token))
|
throw (new UndefinedVariableError(e, token))
|
||||||
|
|||||||
@@ -5,19 +5,15 @@ import { TokenKind } from '../parser/token-kind'
|
|||||||
import { parseStringLiteral } from '../parser/parse-string-literal'
|
import { parseStringLiteral } from '../parser/parse-string-literal'
|
||||||
|
|
||||||
export class PropertyAccessToken extends Token {
|
export class PropertyAccessToken extends Token {
|
||||||
|
public propertyName: string
|
||||||
constructor (
|
constructor (
|
||||||
public variable: IdentifierToken | QuotedToken,
|
public variable: IdentifierToken | QuotedToken,
|
||||||
public props: (IdentifierToken | QuotedToken | PropertyAccessToken)[],
|
public props: (IdentifierToken | QuotedToken | PropertyAccessToken)[],
|
||||||
end: number
|
end: number
|
||||||
) {
|
) {
|
||||||
super(TokenKind.PropertyAccess, variable.input, variable.begin, end, variable.file)
|
super(TokenKind.PropertyAccess, variable.input, variable.begin, end, variable.file)
|
||||||
}
|
this.propertyName = this.variable instanceof IdentifierToken
|
||||||
|
? this.variable.getText()
|
||||||
getVariableAsText () {
|
: parseStringLiteral(this.variable.getText())
|
||||||
if (this.variable instanceof IdentifierToken) {
|
|
||||||
return this.variable.getText()
|
|
||||||
} else {
|
|
||||||
return parseStringLiteral(this.variable.getText())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,8 @@ import { Drop } from '../drop/drop'
|
|||||||
const toStr = Object.prototype.toString
|
const toStr = Object.prototype.toString
|
||||||
const toLowerCase = String.prototype.toLowerCase
|
const toLowerCase = String.prototype.toLowerCase
|
||||||
|
|
||||||
/*
|
|
||||||
* Checks if value is classified as a String primitive or object.
|
|
||||||
* @param {any} value The value to check.
|
|
||||||
* @return {Boolean} Returns true if value is a string, else false.
|
|
||||||
*/
|
|
||||||
export function isString (value: any): value is string {
|
export function isString (value: any): value is string {
|
||||||
return toStr.call(value) === '[object String]'
|
return typeof value === 'string'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isFunction (value: any): value is Function {
|
export function isFunction (value: any): value is Function {
|
||||||
@@ -30,7 +25,9 @@ export function promisify (fn: any) {
|
|||||||
|
|
||||||
export function stringify (value: any): string {
|
export function stringify (value: any): string {
|
||||||
value = toValue(value)
|
value = toValue(value)
|
||||||
return isNil(value) ? '' : String(value)
|
if (isString(value)) return value
|
||||||
|
if (isNil(value)) return ''
|
||||||
|
return String(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toValue (value: any): any {
|
export function toValue (value: any): any {
|
||||||
@@ -47,7 +44,7 @@ export function toLiquid (value: any): any {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isNil (value: any): boolean {
|
export function isNil (value: any): boolean {
|
||||||
return value === null || value === undefined
|
return value == null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isArray (value: any): value is any[] {
|
export function isArray (value: any): value is any[] {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ describe('tags/include', function () {
|
|||||||
})
|
})
|
||||||
return liquid.renderFile('/parent.html').catch(function (e) {
|
return liquid.renderFile('/parent.html').catch(function (e) {
|
||||||
expect(e.name).to.equal('RenderError')
|
expect(e.name).to.equal('RenderError')
|
||||||
expect(e.message).to.match(/illegal filename "not-exist"/)
|
expect(e.message).to.match(/illegal filename "undefined"/)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ describe('tags/layout', function () {
|
|||||||
})
|
})
|
||||||
return liquid.renderFile('/parent.html').catch(function (e) {
|
return liquid.renderFile('/parent.html').catch(function (e) {
|
||||||
expect(e.name).to.equal('RenderError')
|
expect(e.name).to.equal('RenderError')
|
||||||
expect(e.message).to.contain('file "foo"("undefined") not available')
|
expect(e.message).to.contain('illegal filename "undefined"')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
it('should handle layout none', async function () {
|
it('should handle layout none', async function () {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ describe('tags/render', function () {
|
|||||||
})
|
})
|
||||||
return liquid.renderFile('/parent.html').catch(function (e) {
|
return liquid.renderFile('/parent.html').catch(function (e) {
|
||||||
expect(e.name).to.equal('RenderError')
|
expect(e.name).to.equal('RenderError')
|
||||||
expect(e.message).to.match(/illegal filename "not-exist":"undefined"/)
|
expect(e.message).to.match(/illegal filename "undefined"/)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -269,9 +269,8 @@ describe('error', function () {
|
|||||||
.to.throw(RenderError, /intended render error/)
|
.to.throw(RenderError, /intended render error/)
|
||||||
})
|
})
|
||||||
it('should contain original error info for {% include %}', function () {
|
it('should contain original error info for {% include %}', function () {
|
||||||
const origin = ['1st', '2nd', '3rd', 'X{%throwingTag%} Y', '5th', '6th', '7th']
|
|
||||||
mock({
|
mock({
|
||||||
'/throwing-tag.html': origin.join('\n')
|
'/throwing-tag.html': ['1st', '2nd', '3rd', 'X{%throwingTag%} Y', '5th', '6th', '7th'].join('\n')
|
||||||
})
|
})
|
||||||
const html = '{%include "throwing-tag.html"%}'
|
const html = '{%include "throwing-tag.html"%}'
|
||||||
const message = [
|
const message = [
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ chai.use(sinonChai)
|
|||||||
const expect = chai.expect
|
const expect = chai.expect
|
||||||
|
|
||||||
describe('PropertyAccessToken', function () {
|
describe('PropertyAccessToken', function () {
|
||||||
describe('getVariableAsText', function () {
|
describe('#propertyName', function () {
|
||||||
it('should return correct value for IdentifierToken', function () {
|
it('should return correct value for IdentifierToken', function () {
|
||||||
const token = new PropertyAccessToken(new IdentifierToken('foo', 0, 3), [], 3)
|
const token = new PropertyAccessToken(new IdentifierToken('foo', 0, 3), [], 3)
|
||||||
expect(token.getVariableAsText()).to.equal('foo')
|
expect(token.propertyName).to.equal('foo')
|
||||||
})
|
})
|
||||||
it('should return correct value for QuotedToken', function () {
|
it('should return correct value for QuotedToken', function () {
|
||||||
const token = new PropertyAccessToken(new QuotedToken('"foo bar"', 0, 9), [], 9)
|
const token = new PropertyAccessToken(new QuotedToken('"foo bar"', 0, 9), [], 9)
|
||||||
expect(token.getVariableAsText()).to.equal('foo bar')
|
expect(token.propertyName).to.equal('foo bar')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user