Skip to content

Commit

Permalink
Merge 3dd87a1 into f7313fe
Browse files Browse the repository at this point in the history
  • Loading branch information
harttle committed Jun 27, 2021
2 parents f7313fe + 3dd87a1 commit 59d2366
Show file tree
Hide file tree
Showing 19 changed files with 172 additions and 214 deletions.
1 change: 1 addition & 0 deletions benchmark/engines/liquid.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const { join } = require('path')

const liquid = new Liquid({
root: join(__dirname, '../templates'),
cache: true,
extname: '.liquid'
})

Expand Down
7 changes: 1 addition & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,8 @@
"scripts": {
"lint": "eslint \"**/*.ts\" .",
"check": "npm test && npm run lint",
"unit": "mocha \"test/unit/**/*.ts\"",
"integration": "mocha \"test/integration/**/*.ts\"",
"e2e": "mocha \"test/e2e/**/*.ts\"",
"test": "nyc mocha \"test/**/*.ts\"",
"benchmark:prepare": "cd benchmark && npm ci",
"benchmark": "cd benchmark && npm start",
"benchmark:engines": "cd benchmark && npm run engines",
"benchmark": "cd benchmark && npm ci && npm start",
"build": "npm run build:dist && npm run build:docs",
"build:dist": "rm -rf dist && rollup -c rollup.config.ts && ls -lh dist",
"build:docs": "bin/build-docs.sh"
Expand Down
23 changes: 9 additions & 14 deletions src/builtin/tags/include.ts
Original file line number Diff line number Diff line change
@@ -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 { parseFilePath, renderFilePath } from './render'

export default {
parseFilePath,
renderFilePath,
parse: function (token: TagToken) {
const args = token.args
const tokenizer = new Tokenizer(args, this.liquid.options.operatorsTrie)
this.file = this.liquid.options.dynamicPartials
? tokenizer.readValue()
: tokenizer.readFileName()
assert(this.file, () => `illegal argument "${token.args}"`)
this['file'] = this.parseFilePath(tokenizer, this.liquid)

const begin = tokenizer.p
const withStr = tokenizer.readIdentifier()
Expand All @@ -22,22 +22,17 @@ export default {
this.hash = new Hash(tokenizer.remaining())
},
render: function * (ctx: Context, emitter: Emitter) {
const { liquid, hash, withVar, file } = this
const { liquid, hash, withVar } = this
const { renderer } = liquid
// TODO try move all liquid.parse calls into parse() section
const filepath = ctx.opts.dynamicPartials
? (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 filepath = yield this.renderFilePath(this['file'], ctx, liquid)
assert(filepath, () => `illegal filename "${filepath}"`)

const saved = ctx.saveRegister('blocks', 'blockMode')
ctx.setRegister('blocks', {})
ctx.setRegister('blockMode', BlockMode.OUTPUT)
const scope = yield hash.render(ctx)
if (withVar) scope[filepath] = evalToken(withVar, ctx)
const templates = yield liquid._parseFile(filepath, ctx.opts, ctx.sync)
const templates = yield liquid.parseFileImpl(filepath, ctx.sync)
ctx.push(scope)
yield renderer.renderTemplates(templates, ctx, emitter)
ctx.pop()
Expand Down
22 changes: 9 additions & 13 deletions src/builtin/tags/layout.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,28 @@
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 { parseFilePath, renderFilePath } from './render'

export default {
parseFilePath,
renderFilePath,
parse: function (token: TagToken, remainTokens: TopLevelToken[]) {
const tokenizer = new Tokenizer(token.args, this.liquid.options.operatorsTrie)
const file = this.liquid.options.dynamicPartials ? tokenizer.readValue() : tokenizer.readFileName()
assert(file, () => `illegal argument "${token.args}"`)

this.file = file
this['file'] = this.parseFilePath(tokenizer, this.liquid)
this.hash = new Hash(tokenizer.remaining())
this.tpls = this.liquid.parser.parse(remainTokens)
},
render: function * (ctx: Context, emitter: Emitter) {
const { liquid, hash, file } = this
const { renderer } = liquid
if (file.getText() === 'none') {
if (file === null) {
ctx.setRegister('blockMode', BlockMode.OUTPUT)
const html = yield renderer.renderTemplates(this.tpls, ctx)
emitter.write(html)
return
}
const filepath = ctx.opts.dynamicPartials
? (TypeGuards.isQuotedToken(file)
? 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._parseFile(filepath, ctx.opts, ctx.sync)
const filepath = yield this.renderFilePath(this['file'], ctx, liquid)
assert(filepath, () => `illegal filename "${filepath}"`)
const templates = yield liquid.parseFileImpl(filepath, ctx.sync)

// render remaining contents and store rendered results
ctx.setRegister('blockMode', BlockMode.STORE)
Expand Down
64 changes: 47 additions & 17 deletions src/builtin/tags/render.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { assert } from '../../util/assert'
import { ForloopDrop } from '../../drop/forloop-drop'
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 {
parseFilePath,
renderFilePath,
parse: function (token: TagToken) {
const args = token.args
const tokenizer = new Tokenizer(args, this.liquid.options.operatorsTrie)
this.file = this.liquid.options.dynamicPartials
? tokenizer.readValue()
: tokenizer.readFileName()
assert(this.file, () => `illegal argument "${token.args}"`)
this['file'] = this.parseFilePath(tokenizer, this.liquid)

while (!tokenizer.end()) {
tokenizer.skipBlank()
Expand Down Expand Up @@ -40,14 +40,9 @@ export default {
this.hash = new Hash(tokenizer.remaining())
},
render: function * (ctx: Context, emitter: Emitter) {
const { liquid, file, hash } = this
const { renderer } = liquid
const filepath = ctx.opts.dynamicPartials
? (TypeGuards.isQuotedToken(file)
? yield renderer.renderTemplates(liquid.parse(evalQuotedToken(file)), ctx)
: evalToken(file, ctx))
: file.getText()
assert(filepath, () => `illegal filename "${file.getText()}":"${filepath}"`)
const { liquid, hash } = this
const filepath = yield this.renderFilePath(this['file'], ctx, liquid)
assert(filepath, () => `illegal filename "${filepath}"`)

const childCtx = new Context({}, ctx.opts, ctx.sync)
const scope = yield hash.render(ctx)
Expand All @@ -64,13 +59,48 @@ export default {
scope['forloop'] = new ForloopDrop(collection.length)
for (const item of collection) {
scope[alias] = item
const templates = yield liquid._parseFile(filepath, childCtx.opts, childCtx.sync)
yield renderer.renderTemplates(templates, childCtx, emitter)
const templates = yield liquid.parseFileImpl(filepath, childCtx.sync)
yield liquid.renderer.renderTemplates(templates, childCtx, emitter)
scope.forloop.next()
}
} else {
const templates = yield liquid._parseFile(filepath, childCtx.opts, childCtx.sync)
yield renderer.renderTemplates(templates, childCtx, emitter)
const templates = yield liquid.parseFileImpl(filepath, childCtx.sync)
yield liquid.renderer.renderTemplates(templates, childCtx, emitter)
}
}
} 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)
}
114 changes: 58 additions & 56 deletions src/liquid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ export class Liquid {
public parser: Parser
public filters: FilterMap
public tags: TagMap
public parseFileImpl: (file: string, sync?: boolean) => Iterator<Template[]>

public constructor (opts: LiquidOptions = {}) {
this.options = applyDefault(normalize(opts))
this.parser = new Parser(this)
this.renderer = new Render()
this.filters = new FilterMap(this.options.strictFilters, this)
this.tags = new TagMap()
this.parseFileImpl = this.options.cache ? this._parseFileCached : this._parseFile

forOwn(builtinTags, (conf: TagImplOptions, name: string) => this.registerTag(snakeCase(name), conf))
forOwn(builtinFilters, (handler: FilterImplOptions, name: string) => this.registerFilter(snakeCase(name), handler))
Expand All @@ -41,64 +43,61 @@ export class Liquid {
return this.parser.parse(tokens)
}

public _render (tpl: Template[], scope?: object, opts?: LiquidOptions, sync?: boolean): IterableIterator<any> {
const options = { ...this.options, ...normalize(opts) }
const ctx = new Context(scope, options, sync)
const emitter = new Emitter(options.keepOutputType)
public _render (tpl: Template[], scope?: object, sync?: boolean): IterableIterator<any> {
const ctx = new Context(scope, this.options, sync)
const emitter = new Emitter(this.options.keepOutputType)
return this.renderer.renderTemplates(tpl, ctx, emitter)
}
public async render (tpl: Template[], scope?: object, opts?: LiquidOptions): Promise<any> {
return toPromise(this._render(tpl, scope, opts, false))
public async render (tpl: Template[], scope?: object): Promise<any> {
return toPromise(this._render(tpl, scope, false))
}
public renderSync (tpl: Template[], scope?: object, opts?: LiquidOptions): any {
return toValue(this._render(tpl, scope, opts, true))
public renderSync (tpl: Template[], scope?: object): any {
return toValue(this._render(tpl, scope, true))
}

public _parseAndRender (html: string, scope?: object, opts?: LiquidOptions, sync?: boolean): IterableIterator<any> {
public _parseAndRender (html: string, scope?: object, sync?: boolean): IterableIterator<any> {
const tpl = this.parse(html)
return this._render(tpl, scope, opts, sync)
return this._render(tpl, scope, sync)
}
public async parseAndRender (html: string, scope?: object, opts?: LiquidOptions): Promise<any> {
return toPromise(this._parseAndRender(html, scope, opts, false))
public async parseAndRender (html: string, scope?: object): Promise<any> {
return toPromise(this._parseAndRender(html, scope, false))
}
public parseAndRenderSync (html: string, scope?: object, opts?: LiquidOptions): any {
return toValue(this._parseAndRender(html, scope, opts, true))
public parseAndRenderSync (html: string, scope?: object): any {
return toValue(this._parseAndRender(html, scope, true))
}

public * _parseFile (file: string, opts?: LiquidOptions, sync?: boolean) {
const options = { ...this.options, ...normalize(opts) }
const paths = options.root.map(root => options.fs.resolve(root, file, options.extname))
if (options.fs.fallback !== undefined) {
const filepath = options.fs.fallback(file)
if (filepath !== undefined) paths.push(filepath)
}
private * _parseFileCached (file: string, sync?: boolean) {
const cache = this.options.cache!
let tpls = yield cache.read(file)
if (tpls) return tpls

for (const filepath of paths) {
const { cache } = options
if (cache) {
const tpls = yield cache.read(filepath)
if (tpls) return tpls
}
if (!(sync ? options.fs.existsSync(filepath) : yield options.fs.exists(filepath))) continue
const tpl = this.parse(sync ? options.fs.readFileSync(filepath) : yield options.fs.readFile(filepath), filepath)
if (cache) cache.write(filepath, tpl)
tpls = yield this._parseFile(file, sync)
cache.write(file, tpls)
return tpls
}
private * _parseFile (file: string, sync?: boolean) {
const { fs, root } = this.options

for (const filepath of this.lookupFiles(file, this.options)) {
if (!(sync ? fs.existsSync(filepath) : yield fs.exists(filepath))) continue
const tpl = this.parse(sync ? fs.readFileSync(filepath) : yield fs.readFile(filepath), filepath)
return tpl
}
throw this.lookupError(file, options.root)
throw this.lookupError(file, root)
}
public async parseFile (file: string, opts?: LiquidOptions): Promise<Template[]> {
return toPromise(this._parseFile(file, opts, false))
public async parseFile (file: string): Promise<Template[]> {
return toPromise(this.parseFileImpl(file, false))
}
public parseFileSync (file: string, opts?: LiquidOptions): Template[] {
return toValue(this._parseFile(file, opts, true))
public parseFileSync (file: string): Template[] {
return toValue(this.parseFileImpl(file, true))
}
public async renderFile (file: string, ctx?: object, opts?: LiquidOptions) {
const templates = await this.parseFile(file, opts)
return this.render(templates, ctx, opts)
public async renderFile (file: string, ctx?: object) {
const templates = await this.parseFile(file)
return this.render(templates, ctx)
}
public renderFileSync (file: string, ctx?: object, opts?: LiquidOptions) {
const templates = this.parseFileSync(file, opts)
return this.renderSync(templates, ctx, opts)
public renderFileSync (file: string, ctx?: object) {
const templates = this.parseFileSync(file)
return this.renderSync(templates, ctx)
}

public _evalValue (str: string, ctx: Context): IterableIterator<any> {
Expand All @@ -123,9 +122,25 @@ export class Liquid {
}
public express () {
const self = this // eslint-disable-line
let firstCall = true

return function (this: any, filePath: string, ctx: object, callback: (err: Error | null, rendered: string) => void) {
const opts = { root: [...normalizeStringArray(this.root), ...self.options.root] }
self.renderFile(filePath, ctx, opts).then(html => callback(null, html) as any, callback as any)
if (firstCall) {
firstCall = false
self.options.root.unshift(...normalizeStringArray(this.root))
}
self.renderFile(filePath, ctx).then(html => callback(null, html) as any, callback as any)
}
}

private * lookupFiles (file: string, options: NormalizedFullOptions) {
const { root, fs, extname } = options
for (const dir of root) {
yield fs.resolve(dir, file, extname)
}
if (fs.fallback !== undefined) {
const filepath = fs.fallback(file)
if (filepath !== undefined) yield filepath
}
}

Expand All @@ -135,17 +150,4 @@ export class Liquid {
err.code = 'ENOENT'
return err
}

/**
* @deprecated use parseFile instead
*/
public async getTemplate (file: string, opts?: LiquidOptions): Promise<Template[]> {
return this.parseFile(file, opts)
}
/**
* @deprecated use parseFileSync instead
*/
public getTemplateSync (file: string, opts?: LiquidOptions): Template[] {
return this.parseFileSync(file, opts)
}
}
2 changes: 1 addition & 1 deletion src/parser/tokenizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export class Tokenizer {
private rawBeginAt = -1

constructor (
private input: string,
public input: string,
private trie: Trie,
private file: string = ''
) {
Expand Down
3 changes: 1 addition & 2 deletions src/render/expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,9 @@ export function evalToken (token: Token | undefined, ctx: Context, lenient = fal
}

function evalPropertyAccessToken (token: PropertyAccessToken, ctx: Context, lenient: boolean) {
const variable = token.getVariableAsText()
const props: string[] = token.props.map(prop => evalToken(prop, ctx, false))
try {
return ctx.get([variable, ...props])
return ctx.get([token.propertyName, ...props])
} catch (e) {
if (lenient && e.name === 'InternalUndefinedVariableError') return null
throw (new UndefinedVariableError(e, token))
Expand Down

0 comments on commit 59d2366

Please sign in to comment.