perf: parse filenames in parse() insteadof render()

This commit is contained in:
harttle
2021-09-30 22:15:09 +08:00
committed by Jun Yang
parent 24f5346084
commit 8273c17dab
13 changed files with 80 additions and 68 deletions
+8 -13
View File
@@ -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', {})
+8 -12
View File
@@ -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
View File
@@ -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)
}
+1 -1
View File
@@ -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 = ''
) { ) {
+1 -1
View File
@@ -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:
+1 -2
View File
@@ -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))
+4 -8
View File
@@ -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())
}
} }
} }
+5 -8
View File
@@ -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[] {
+1 -1
View File
@@ -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"/)
}) })
}) })
+1 -1
View File
@@ -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 () {
+1 -1
View File
@@ -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"/)
}) })
}) })
+1 -2
View File
@@ -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 = [
+3 -3
View File
@@ -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')
}) })
}) })
}) })