refactor: Tag class support in registerTag()

This commit is contained in:
Jun Yang
2022-11-27 14:04:01 +08:00
parent 1f6ce7c822
commit 92992689cd
145 changed files with 694 additions and 768 deletions
+21 -2
View File
@@ -12,8 +12,8 @@ engine.registerTag('upper', {
parse: function(tagToken: TagToken, remainTokens: TopLevelToken[]) { parse: function(tagToken: TagToken, remainTokens: TopLevelToken[]) {
this.str = tagToken.args; // name this.str = tagToken.args; // name
}, },
render: async function(ctx: Context) { render: function*(ctx: Context) {
var str = await this.liquid.evalValue(this.str, ctx); // 'alice' const str = yield this.liquid.evalValue(this.str, ctx); // 'alice'
return str.toUpperCase() // 'ALICE' return str.toUpperCase() // 'ALICE'
} }
}); });
@@ -22,6 +22,25 @@ engine.registerTag('upper', {
* `parse`: Read tokens from `remainTokens` until your end token. * `parse`: Read tokens from `remainTokens` until your end token.
* `render`: Combine scope data with your parsed tokens into HTML string. * `render`: Combine scope data with your parsed tokens into HTML string.
For complex tag implementation, you can also provide a tag class:
```typescript
// Usage: {% upper name:"alice" %}
import { Hash, Tag, TagToken, Context, Emitter, TopLevelToken, Liquid } from 'liquidjs'
engine.registerTag('upper', class UpperTag extends Tag {
private hash: Hash
constructor(tagToken: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(tagToken, remainTokens, liquid)
this.hash = new Hash(tagToken.args)
}
* render(ctx: Context) {
const hash = yield this.hash.render();
return hash.name.toUpperCase() // 'ALICE'
}
});
```
See existing tag implementations here: <https://github.com/harttle/liquidjs/tree/master/src/builtin/tags> See existing tag implementations here: <https://github.com/harttle/liquidjs/tree/master/src/builtin/tags>
See demo example here: https://github.com/harttle/liquidjs/blob/master/demo/typescript/index.ts See demo example here: https://github.com/harttle/liquidjs/blob/master/demo/typescript/index.ts
@@ -36,7 +36,24 @@ engine.registerFilter('upper', v => v.toUpperCase())
engine.registerFilter('add', (initial, arg1, arg2) => initial + arg1 + arg2) engine.registerFilter('add', (initial, arg1, arg2) => initial + arg1 + arg2)
``` ```
查看已有的过滤器实现:<https://github.com/harttle/liquidjs/tree/master/src/builtin/filters> 查看已有的过滤器实现:<https://github.com/harttle/liquidjs/tree/master/src/builtin/filters>。对于复杂的标签,也可以用一个类来实现:
```typescript
// Usage: {% upper name:"alice" %}
import { Hash, Tag, TagToken, Context, Emitter, TopLevelToken, Liquid } from 'liquidjs'
engine.registerTag('upper', class UpperTag extends Tag {
private hash: Hash
constructor(tagToken: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(tagToken, remainTokens, liquid)
this.hash = new Hash(tagToken.args)
}
* render(ctx: Context) {
const hash = yield this.hash.render();
return hash.name.toUpperCase() // 'ALICE'
}
});
```
## 反注册标签/过滤器 ## 反注册标签/过滤器
+1 -4
View File
@@ -9,7 +9,7 @@
"./dist/liquid.node.cjs.js": "./dist/liquid.browser.umd.js", "./dist/liquid.node.cjs.js": "./dist/liquid.browser.umd.js",
"./dist/liquid.node.esm.js": "./dist/liquid.browser.esm.js" "./dist/liquid.node.esm.js": "./dist/liquid.browser.esm.js"
}, },
"types": "dist/liquid.d.ts", "types": "dist/index.d.ts",
"engines": { "engines": {
"node": ">=14" "node": ">=14"
}, },
@@ -44,9 +44,6 @@
"LICENSE", "LICENSE",
"README.md" "README.md"
], ],
"engines": {
"node": ">=4.8.7"
},
"keywords": [ "keywords": [
"liquid", "liquid",
"template engine", "template engine",
+3 -3
View File
@@ -38,16 +38,16 @@ const versionInjection = versionInjector({
logger: console, logger: console,
exclude: [] exclude: []
}) })
const input = './src/liquid.ts' const input = './src/index.ts'
const browserFS = { const browserFS = {
include: './src/liquid-options.ts', include: './src/liquid-options.ts',
delimiters: ['', ''], delimiters: ['', ''],
'./fs/node': './fs/browser' './fs/node': './fs/browser'
} }
const browserStream = { const browserStream = {
include: './src/render/render.ts', include: './src/emitters/index.ts',
delimiters: ['', ''], delimiters: ['', ''],
'../emitters/streamed-emitter': '../emitters/streamed-emitter-browser' './streamed-emitter': './streamed-emitter-browser'
} }
const esmRequire = { const esmRequire = {
include: './src/fs/node.ts', include: './src/fs/node.ts',
+2
View File
@@ -0,0 +1,2 @@
export * from './cache'
export * from './lru'
+1 -1
View File
@@ -10,7 +10,7 @@ class Node<T> {
} }
export class LRU<T> implements Cache<T> { export class LRU<T> implements Cache<T> {
private cache: { [key: string]: Node<T> } = {} private cache: Record<string, Node<T>> = {}
private head: Node<T> private head: Node<T>
private tail: Node<T> private tail: Node<T>
+1 -3
View File
@@ -1,8 +1,6 @@
enum BlockMode { export enum BlockMode {
/* store rendered html into blocks */ /* store rendered html into blocks */
OUTPUT, OUTPUT,
/* output rendered html directly */ /* output rendered html directly */
STORE STORE
} }
export default BlockMode
+1 -3
View File
@@ -2,9 +2,7 @@ import { Drop } from '../drop/drop'
import { __assign } from 'tslib' import { __assign } from 'tslib'
import { NormalizedFullOptions, defaultOptions, RenderOptions } from '../liquid-options' import { NormalizedFullOptions, defaultOptions, RenderOptions } 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, InternalUndefinedVariableError, toValueSync } from '../util'
import { InternalUndefinedVariableError } from '../util/error'
import { toValueSync } from '../util/async'
type PropertyKey = string | number; type PropertyKey = string | number;
+3
View File
@@ -0,0 +1,3 @@
export * from './context'
export * from './scope'
export * from './block-mode'
+2 -3
View File
@@ -1,8 +1,7 @@
import { Drop } from '../drop/drop' import { Drop } from '../drop/drop'
export interface PlainObject { interface ScopeObject extends Record<string, any> {
[key: string]: any;
toLiquid?: () => any; toLiquid?: () => any;
} }
export type Scope = PlainObject | Drop export type Scope = ScopeObject | Drop
+2 -2
View File
@@ -1,5 +1,5 @@
import { isNil, isString, toValue } from '../util/underscore' import { isNil, isString, toValue } from '../util'
import { EmptyDrop } from '../drop/empty-drop' import { EmptyDrop } from '../drop'
export class BlankDrop extends EmptyDrop { export class BlankDrop extends EmptyDrop {
public equals (value: any) { public equals (value: any) {
+1 -1
View File
@@ -1,4 +1,4 @@
import { isFunction } from '../util/underscore' import { isFunction } from '../util'
export interface Comparable { export interface Comparable {
equals: (rhs: any) => boolean; equals: (rhs: any) => boolean;
+1 -1
View File
@@ -1,6 +1,6 @@
import { Drop } from './drop' import { Drop } from './drop'
import { Comparable } from './comparable' import { Comparable } from './comparable'
import { isObject, isString, isArray, toValue } from '../util/underscore' import { isObject, isString, isArray, toValue } from '../util'
export class EmptyDrop extends Drop implements Comparable { export class EmptyDrop extends Drop implements Comparable {
public equals (value: any) { public equals (value: any) {
+7
View File
@@ -0,0 +1,7 @@
export * from './drop'
export * from './null-drop'
export * from './empty-drop'
export * from './blank-drop'
export * from './forloop-drop'
export * from './block-drop'
export * from './comparable'
+1 -1
View File
@@ -1,6 +1,6 @@
import { Drop } from './drop' import { Drop } from './drop'
import { Comparable } from './comparable' import { Comparable } from './comparable'
import { isNil, toValue } from '../util/underscore' import { isNil, toValue } from '../util'
export class NullDrop extends Drop implements Comparable { export class NullDrop extends Drop implements Comparable {
public equals (value: any) { public equals (value: any) {
+4
View File
@@ -0,0 +1,4 @@
export * from './emitter'
export * from './simple-emitter'
export * from './streamed-emitter'
export * from './keeping-type-emitter'
+2 -2
View File
@@ -1,5 +1,5 @@
import { stringify, toValue } from '../util/underscore' import { stringify, toValue } from '../util'
import { Emitter } from '../types' import { Emitter } from './emitter'
export class KeepingTypeEmitter implements Emitter { export class KeepingTypeEmitter implements Emitter {
public buffer: any = ''; public buffer: any = '';
+1 -1
View File
@@ -1,4 +1,4 @@
import { stringify } from '../util/underscore' import { stringify } from '../util'
import { Emitter } from './emitter' import { Emitter } from './emitter'
export class SimpleEmitter implements Emitter { export class SimpleEmitter implements Emitter {
+1 -1
View File
@@ -1,4 +1,4 @@
import { stringify } from '../util/underscore' import { stringify } from '../util'
import { Emitter } from './emitter' import { Emitter } from './emitter'
import { PassThrough } from 'stream' import { PassThrough } from 'stream'
+5 -6
View File
@@ -1,9 +1,8 @@
import { argumentsToValue, toValue, stringify, caseInsensitiveCompare, isArray, isNil, last as arrayLast, hasOwnProperty } from '../util/underscore' import { toArray, argumentsToValue, toValue, stringify, caseInsensitiveCompare, isArray, isNil, last as arrayLast, hasOwnProperty } from '../util'
import { toArray } from '../util/collection' import { isTruthy } from '../render'
import { isTruthy } from '../render/boolean' import { FilterImpl } from '../template'
import { FilterImpl } from '../template/filter/filter-impl' import { Scope } from '../context'
import { Scope } from '../context/scope' import { isComparable } from '../drop'
import { isComparable } from '../drop/comparable'
export const join = argumentsToValue((v: any[], arg: string) => toArray(v).join(arg === undefined ? ' ' : arg)) export const join = argumentsToValue((v: any[], arg: string) => toArray(v).join(arg === undefined ? ' ' : arg))
export const last = argumentsToValue((v: any) => isArray(v) ? arrayLast(v) : '') export const last = argumentsToValue((v: any) => isArray(v) ? arrayLast(v) : '')
+2 -5
View File
@@ -1,8 +1,5 @@
import strftime from '../util/strftime' import { toValue, stringify, isString, isNumber, TimezoneDate, LiquidDate, strftime } from '../util'
import { LiquidDate } from '../util/liquid-date' import { FilterImpl } from '../template'
import { toValue, stringify, isString, isNumber } from '../util/underscore'
import { FilterImpl } from '../template/filter/filter-impl'
import { TimezoneDate } from '../util/timezone-date'
export function date (this: FilterImpl, v: string | Date, format: string, timeZoneOffset?: number) { export function date (this: FilterImpl, v: string | Date, format: string, timeZoneOffset?: number) {
const opts = this.context.opts const opts = this.context.opts
+2 -2
View File
@@ -5,9 +5,9 @@ import * as arrayFilters from './array'
import * as dateFilters from './date' import * as dateFilters from './date'
import * as stringFilters from './string' import * as stringFilters from './string'
import { Default, json } from './misc' import { Default, json } from './misc'
import { FilterImplOptions } from '../template/filter/filter-impl-options' import { FilterImplOptions } from '../template'
export const filters: { [key: string]: FilterImplOptions } = { export const filters: Record<string, FilterImplOptions> = {
...htmlFilters, ...htmlFilters,
...mathFilters, ...mathFilters,
...urlFilters, ...urlFilters,
+1 -1
View File
@@ -1,6 +1,6 @@
import { isFalsy } from '../render/boolean' import { isFalsy } from '../render/boolean'
import { identify, isArray, isString, toValue } from '../util/underscore' import { identify, isArray, isString, toValue } from '../util/underscore'
import { FilterImpl } from '../template/filter/filter-impl' import { FilterImpl } from '../template'
export function Default<T1 extends boolean, T2> (this: FilterImpl, value: T1, defaultValue: T2, ...args: Array<[string, any]>): T1 | T2 { export function Default<T1 extends boolean, T2> (this: FilterImpl, value: T1, defaultValue: T2, ...args: Array<[string, any]>): T1 | T2 {
value = toValue(value) value = toValue(value)
+1 -2
View File
@@ -3,8 +3,7 @@
* *
* * prefer stringify() to String() since `undefined`, `null` should eval '' * * prefer stringify() to String() since `undefined`, `null` should eval ''
*/ */
import { escapeRegExp, stringify } from '../util/underscore' import { assert, escapeRegExp, stringify } from '../util'
import { assert } from '../util/assert'
export function append (v: string, arg: string) { export function append (v: string, arg: string) {
assert(arguments.length === 2, 'append expect 2 arguments') assert(arguments.length === 2, 'append expect 2 arguments')
+1 -1
View File
@@ -1,4 +1,4 @@
import { last } from '../util/underscore' import { last } from '../util'
function domResolve (root: string, path: string) { function domResolve (root: string, path: string) {
const base = document.createElement('base') const base = document.createElement('base')
+2
View File
@@ -0,0 +1,2 @@
export * from './loader'
export * from './fs'
+1 -2
View File
@@ -1,6 +1,5 @@
import { FS } from './fs' import { FS } from './fs'
import { escapeRegex } from '../util/underscore' import { assert, escapeRegex } from '../util'
import { assert } from '../util/assert'
export interface LoaderOptions { export interface LoaderOptions {
fs: FS; fs: FS;
+3 -3
View File
@@ -1,10 +1,10 @@
import * as _ from '../util/underscore' import { promisify } from '../util'
import { sep, resolve as nodeResolve, extname, dirname as nodeDirname } from 'path' import { sep, resolve as nodeResolve, extname, dirname as nodeDirname } from 'path'
import { stat, statSync, readFile as nodeReadFile, readFileSync as nodeReadFileSync } from 'fs' import { stat, statSync, readFile as nodeReadFile, readFileSync as nodeReadFileSync } from 'fs'
import { requireResolve } from './node-require' import { requireResolve } from './node-require'
const statAsync = _.promisify(stat) const statAsync = promisify(stat)
const readFileAsync = _.promisify<string, string, string>(nodeReadFile) const readFileAsync = promisify<string, string, string>(nodeReadFile)
export async function exists (filepath: string) { export async function exists (filepath: string) {
try { try {
+16
View File
@@ -0,0 +1,16 @@
/* istanbul ignore file */
export const version = '[VI]{version}[/VI]'
export * as TypeGuards from './util/type-guards'
export { toValue, TimezoneDate, createTrie, Trie, toPromise, toValueSync, assert, ParseError, TokenizationError, AssertionError } from './util'
export { Drop } from './drop'
export { Emitter } from './emitters'
// TODO change to _evalToken
export { defaultOperators, Operators, _evalToken, evalQuotedToken, Expression, isFalsy, isTruthy } from './render'
export { Context, Scope } from './context'
export { Value, Hash, Template, FilterImplOptions, Tag, Filter } from './template'
export { Token, TopLevelToken, TagToken, ValueToken } from './tokens'
export { TokenKind, Tokenizer, ParseStream } from './parser'
export { filters } from './filters'
export { tags } from './tags'
export { defaultOptions } from './liquid-options'
export { Liquid } from './liquid'
+3 -5
View File
@@ -1,11 +1,9 @@
import { isArray, isString, isFunction } from './util/underscore' import { assert, isArray, isString, isFunction } from './util'
import { LiquidCache } from './cache/cache' import { LRU, LiquidCache } from './cache'
import { LRU } from './cache/lru'
import { FS } from './fs/fs' import { FS } from './fs/fs'
import * as fs from './fs/node' import * as fs from './fs/node'
import { defaultOperators, Operators } from './render/operator' import { defaultOperators, Operators } from './render'
import { filters } from './filters' import { filters } from './filters'
import { assert } from './util/assert'
type OutputEscape = (value: any) => string type OutputEscape = (value: any) => string
type OutputEscapeOption = 'escape' | 'json' | OutputEscape type OutputEscapeOption = 'escape' | 'json' | OutputEscape
+12 -26
View File
@@ -1,38 +1,24 @@
import { Context } from './context/context' import { Context } from './context'
import { forOwn } from './util/underscore' import { toPromise, toValueSync, isFunction, forOwn } from './util'
import { Template } from './template/template' import { TagClass, createTagClass, TagImplOptions, FilterImplOptions, Template, Value } from './template'
import { LookupType } from './fs/loader' import { LookupType } from './fs/loader'
import { Render } from './render/render' import { Render } from './render'
import Parser from './parser/parser' import { Parser } from './parser'
import { TagImplOptions } from './template/tag/tag-impl-options'
import { Value } from './template/value'
import { tags } from './tags' import { tags } from './tags'
import { filters } from './filters' import { filters } from './filters'
import { TagMap } from './template/tag/tag-map'
import { FilterMap } from './template/filter/filter-map'
import { LiquidOptions, normalizeDirectoryList, NormalizedFullOptions, normalize, RenderOptions } from './liquid-options' import { LiquidOptions, normalizeDirectoryList, NormalizedFullOptions, normalize, RenderOptions } from './liquid-options'
import { FilterImplOptions } from './template/filter/filter-impl-options'
import { toPromise, toValueSync } from './util/async'
export * from './util/error'
export * from './types'
export const version = '[VI]{version}[/VI]'
export class Liquid { export class Liquid {
public readonly options: NormalizedFullOptions public readonly options: NormalizedFullOptions
public readonly renderer: Render public readonly renderer = new Render()
public readonly parser: Parser public readonly parser: Parser
public readonly filters: FilterMap public readonly filters: Record<string, FilterImplOptions> = {}
public readonly tags: TagMap public readonly tags: Record<string, TagClass> = {}
public constructor (opts: LiquidOptions = {}) { public constructor (opts: LiquidOptions = {}) {
this.options = normalize(opts) this.options = normalize(opts)
this.parser = new Parser(this) this.parser = new Parser(this)
this.renderer = new Render() forOwn(tags, (conf: TagClass, name: string) => this.registerTag(name, conf))
this.filters = new FilterMap(this.options.strictFilters, this)
this.tags = new TagMap()
forOwn(tags, (conf: TagImplOptions, name: string) => this.registerTag(name, conf))
forOwn(filters, (handler: FilterImplOptions, name: string) => this.registerFilter(name, handler)) forOwn(filters, (handler: FilterImplOptions, name: string) => this.registerFilter(name, handler))
} }
public parse (html: string, filepath?: string): Template[] { public parse (html: string, filepath?: string): Template[] {
@@ -103,10 +89,10 @@ export class Liquid {
} }
public registerFilter (name: string, filter: FilterImplOptions) { public registerFilter (name: string, filter: FilterImplOptions) {
this.filters.set(name, filter) this.filters[name] = filter
} }
public registerTag (name: string, tag: TagImplOptions) { public registerTag (name: string, tag: TagClass | TagImplOptions) {
this.tags.set(name, tag) this.tags[name] = isFunction(tag) ? tag : createTagClass(tag)
} }
public plugin (plugin: (this: Liquid, L: typeof Liquid) => void) { public plugin (plugin: (this: Liquid, L: typeof Liquid) => void) {
return plugin.call(this, Liquid) return plugin.call(this, Liquid)
+1 -1
View File
@@ -5,6 +5,6 @@ type KeyValuePair = [string?, ValueToken?]
export type FilterArg = ValueToken | KeyValuePair export type FilterArg = ValueToken | KeyValuePair
export function isKeyValuePair (arr: FilterArg): arr is KeyValuePair { // TODO check export function isKeyValuePair (arr: FilterArg): arr is KeyValuePair {
return isArray(arr) return isArray(arr)
} }
+5
View File
@@ -0,0 +1,5 @@
export * from './tokenizer'
export * from './parser'
export * from './parse-stream'
export * from './parse-string-literal'
export * from './token-kind'
+2 -3
View File
@@ -1,8 +1,7 @@
import { IDENTIFIER, TYPES } from '../util/character' import { Trie, TrieNode, IDENTIFIER, TYPES } from '../util'
import { Trie } from '../util/operator-trie'
export function matchOperator (str: string, begin: number, trie: Trie, end = str.length) { export function matchOperator (str: string, begin: number, trie: Trie, end = str.length) {
let node = trie let node: TrieNode = trie
let i = begin let i = begin
let info let info
while (node[str[i]] && i < end) { while (node[str[i]] && i < end) {
+4 -5
View File
@@ -1,13 +1,12 @@
import { Token } from '../tokens/token' import { Token, TopLevelToken } from '../tokens'
import { Template } from '../template/template' import { Template } from '../template'
import { isTagToken } from '../util/type-guards' import { isTagToken } from '../util'
import { TopLevelToken } from '../tokens/toplevel-token'
type ParseToken<T extends Token> = ((token: T, remainTokens: T[]) => Template) type ParseToken<T extends Token> = ((token: T, remainTokens: T[]) => Template)
export class ParseStream<T extends Token = TopLevelToken> { export class ParseStream<T extends Token = TopLevelToken> {
private tokens: T[] private tokens: T[]
private handlers: {[key: string]: (arg: any) => void} = {} private handlers: Record<string, (arg: any) => void> = {}
private stopRequested = false private stopRequested = false
private parseToken: ParseToken<T> private parseToken: ParseToken<T>
+11 -15
View File
@@ -1,19 +1,13 @@
import { ParseError } from '../util/error' import { toPromise, assert, isTagToken, isOutputToken, ParseError } from '../util'
import { Liquid, Tokenizer } from '../liquid' import { Tokenizer } from './tokenizer'
import { ParseStream } from './parse-stream' import { ParseStream } from './parse-stream'
import { isTagToken, isOutputToken } from '../util/type-guards' import { TopLevelToken, OutputToken } from '../tokens'
import { OutputToken } from '../tokens/output-token' import { Template, Output, HTML } from '../template'
import { Tag } from '../template/tag/tag' import { LiquidCache } from '../cache'
import { Output } from '../template/output' import { FS, Loader, LookupType } from '../fs'
import { HTML } from '../template/html' import type { Liquid } from '../liquid'
import { Template } from '../template/template'
import { TopLevelToken } from '../tokens/toplevel-token'
import { LiquidCache } from '../cache/cache'
import { Loader, LookupType } from '../fs/loader'
import { toPromise } from '../util/async'
import { FS } from '../fs/fs'
export default class Parser { export class Parser {
public parseFile: (file: string, sync?: boolean, type?: LookupType, currentFile?: string) => Generator<unknown, Template[], Template[] | string> public parseFile: (file: string, sync?: boolean, type?: LookupType, currentFile?: string) => Generator<unknown, Template[], Template[] | string>
private liquid: Liquid private liquid: Liquid
@@ -44,7 +38,9 @@ export default class Parser {
public parseToken (token: TopLevelToken, remainTokens: TopLevelToken[]) { public parseToken (token: TopLevelToken, remainTokens: TopLevelToken[]) {
try { try {
if (isTagToken(token)) { if (isTagToken(token)) {
return new Tag(token, remainTokens, this.liquid) const TagClass = this.liquid.tags[token.name]
assert(TagClass, `tag "${token.name}" not found`)
return new TagClass(token, remainTokens, this.liquid)
} }
if (isOutputToken(token)) { if (isOutputToken(token)) {
return new Output(token as OutputToken, this.liquid) return new Output(token as OutputToken, this.liquid)
+6 -27
View File
@@ -1,31 +1,10 @@
import { whiteSpaceCtrl } from './whitespace-ctrl' import { TagToken, HTMLToken, HashToken, QuotedToken, LiquidTagToken, OutputToken, ValueToken, Token, RangeToken, FilterToken, TopLevelToken, PropertyAccessToken, OperatorToken, LiteralToken, IdentifierToken, NumberToken } from '../tokens'
import { NumberToken } from '../tokens/number-token' import { Trie, createTrie, ellipsis, literalValues, assert, TokenizationError, TYPES, QUOTE, BLANK, IDENTIFIER } from '../util'
import { IdentifierToken } from '../tokens/identifier-token' import { Operators, Expression } from '../render'
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 { FilterToken } from '../tokens/filter-token'
import { HashToken } from '../tokens/hash-token'
import { QuotedToken } from '../tokens/quoted-token'
import { ellipsis } from '../util/underscore'
import { HTMLToken } from '../tokens/html-token'
import { TagToken } from '../tokens/tag-token'
import { Token } from '../tokens/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 { NormalizedFullOptions, defaultOptions } from '../liquid-options' import { NormalizedFullOptions, defaultOptions } from '../liquid-options'
import { TYPES, QUOTE, BLANK, IDENTIFIER } from '../util/character' import { FilterArg } from './filter-arg'
import { matchOperator } from './match-operator' import { matchOperator } from './match-operator'
import { Trie, createTrie } from '../util/operator-trie' import { whiteSpaceCtrl } from './whitespace-ctrl'
import { Expression } from '../render/expression'
import { Operators } from '../render/operator'
import { LiquidTagToken } from '../tokens/liquid-tag-token'
export class Tokenizer { export class Tokenizer {
p = 0 p = 0
@@ -36,7 +15,7 @@ export class Tokenizer {
constructor ( constructor (
public input: string, public input: string,
operators: Operators = defaultOptions.operators, operators: Operators = defaultOptions.operators,
public file: string = '' public file?: string
) { ) {
this.N = input.length this.N = input.length
this.opTrie = createTrie(operators) this.opTrie = createTrie(operators)
+2 -3
View File
@@ -1,7 +1,6 @@
import { Token } from '../tokens/token' import { Token } from '../tokens'
import { isTagToken, isHTMLToken, isDelimitedToken } from '../util/type-guards'
import { NormalizedFullOptions } from '../liquid-options' import { NormalizedFullOptions } from '../liquid-options'
import { TYPES, INLINE_BLANK, BLANK } from '../util/character' import { isTagToken, isHTMLToken, isDelimitedToken, TYPES, INLINE_BLANK, BLANK } from '../util'
export function whiteSpaceCtrl (tokens: Token[], options: NormalizedFullOptions) { export function whiteSpaceCtrl (tokens: Token[], options: NormalizedFullOptions) {
let inRaw = false let inRaw = false
+13 -31
View File
@@ -1,19 +1,8 @@
import { QuotedToken } from '../tokens/quoted-token' import { RangeToken, OperatorToken, Token, LiteralToken, NumberToken, PropertyAccessToken, QuotedToken } from '../tokens'
import { PropertyAccessToken } from '../tokens/property-access-token' import { isQuotedToken, isWordToken, isNumberToken, isLiteralToken, isRangeToken, isPropertyAccessToken, UndefinedVariableError, range, isOperatorToken, literalValues, assert } from '../util'
import { NumberToken } from '../tokens/number-token' import { parseStringLiteral } from '../parser'
import { assert } from '../util/assert' import { Context } from '../context'
import { literalValues } from '../util/literal' import { Operators } from '../render'
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 { range } from '../util/underscore'
import { Operators } from '../render/operator'
import { UndefinedVariableError } from '../util/error'
import { toValueSync } from '../util/async'
export class Expression { export class Expression {
private postfix: Token[] private postfix: Token[]
@@ -25,7 +14,7 @@ export class Expression {
assert(ctx, 'unable to evaluate: context not defined') assert(ctx, 'unable to evaluate: context not defined')
const operands: any[] = [] const operands: any[] = []
for (const token of this.postfix) { for (const token of this.postfix) {
if (TypeGuards.isOperatorToken(token)) { if (isOperatorToken(token)) {
const r = operands.pop() const r = operands.pop()
const l = operands.pop() const l = operands.pop()
const result = yield evalOperatorToken(ctx.opts.operators, token, l, r, ctx) const result = yield evalOperatorToken(ctx.opts.operators, token, l, r, ctx)
@@ -38,20 +27,13 @@ export class Expression {
} }
} }
/**
* @deprecated use `_evalToken` instead
*/
export function * evalToken (token: Token | undefined, ctx: Context, lenient = false) {
return toValueSync(_evalToken(token, ctx, lenient))
}
export function * _evalToken (token: Token | undefined, ctx: Context, lenient = false): IterableIterator<unknown> { export function * _evalToken (token: Token | undefined, ctx: Context, lenient = false): IterableIterator<unknown> {
if (TypeGuards.isPropertyAccessToken(token)) return yield evalPropertyAccessToken(token, ctx, lenient) if (isPropertyAccessToken(token)) return yield evalPropertyAccessToken(token, ctx, lenient)
if (TypeGuards.isRangeToken(token)) return yield evalRangeToken(token, ctx) if (isRangeToken(token)) return yield evalRangeToken(token, ctx)
if (TypeGuards.isLiteralToken(token)) return evalLiteralToken(token) if (isLiteralToken(token)) return evalLiteralToken(token)
if (TypeGuards.isNumberToken(token)) return evalNumberToken(token) if (isNumberToken(token)) return evalNumberToken(token)
if (TypeGuards.isWordToken(token)) return token.getText() if (isWordToken(token)) return token.getText()
if (TypeGuards.isQuotedToken(token)) return evalQuotedToken(token) if (isQuotedToken(token)) return evalQuotedToken(token)
} }
function * evalPropertyAccessToken (token: PropertyAccessToken, ctx: Context, lenient: boolean): IterableIterator<unknown> { function * evalPropertyAccessToken (token: PropertyAccessToken, ctx: Context, lenient: boolean): IterableIterator<unknown> {
@@ -94,7 +76,7 @@ function * evalRangeToken (token: RangeToken, ctx: Context) {
function * toPostfix (tokens: IterableIterator<Token>): IterableIterator<Token> { function * toPostfix (tokens: IterableIterator<Token>): IterableIterator<Token> {
const ops: OperatorToken[] = [] const ops: OperatorToken[] = []
for (const token of tokens) { for (const token of tokens) {
if (TypeGuards.isOperatorToken(token)) { if (isOperatorToken(token)) {
while (ops.length && ops[ops.length - 1].getPrecedence() > token.getPrecedence()) { while (ops.length && ops[ops.length - 1].getPrecedence() > token.getPrecedence()) {
yield ops.pop()! yield ops.pop()!
} }
+4
View File
@@ -0,0 +1,4 @@
export * from './render'
export * from './expression'
export * from './operator'
export * from './boolean'
+4 -5
View File
@@ -1,11 +1,10 @@
import { isComparable } from '../drop/comparable' import { isComparable } from '../drop/comparable'
import { Context } from '../context/context' import { Context } from '../context'
import { isFunction, toValue } from '../util/underscore' import { isFunction, toValue } from '../util'
import { isTruthy } from '../render/boolean' import { isTruthy } from '../render/boolean'
export interface Operators { export type OperatorHandler = (lhs: any, rhs: any, ctx: Context) => boolean;
[key: string]: (lhs: any, rhs: any, ctx: Context) => boolean; export type Operators = Record<string, OperatorHandler>
}
export const defaultOperators: Operators = { export const defaultOperators: Operators = {
'==': (l: any, r: any) => { '==': (l: any, r: any) => {
+4 -8
View File
@@ -1,11 +1,7 @@
import { RenderError } from '../util/error' import { toPromise, RenderError } from '../util'
import { Context } from '../context/context' import { Context } from '../context'
import { Template } from '../template/template' import { Template } from '../template'
import { Emitter } from '../emitters/emitter' import { Emitter, KeepingTypeEmitter, StreamedEmitter, SimpleEmitter } from '../emitters'
import { SimpleEmitter } from '../emitters/simple-emitter'
import { StreamedEmitter } from '../emitters/streamed-emitter'
import { toPromise } from '../util/async'
import { KeepingTypeEmitter } from '../emitters/keeping-type-emitter'
export class Render { export class Render {
public renderTemplatesToNodeStream (templates: Template[], ctx: Context): NodeJS.ReadableStream { public renderTemplatesToNodeStream (templates: Template[], ctx: Context): NodeJS.ReadableStream {
+10 -7
View File
@@ -1,15 +1,18 @@
import { Value, Tokenizer, assert, TagImplOptions, TagToken, Context } from '../types' import { Value, assert, Tokenizer, Liquid, TopLevelToken, TagToken, Context, Tag } from '..'
export default class extends Tag {
private key: string
private value: Value
export default { constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
parse: function (token: TagToken) { super(token, remainTokens, liquid)
const tokenizer = new Tokenizer(token.args, this.liquid.options.operators) const tokenizer = new Tokenizer(token.args, liquid.options.operators)
this.key = tokenizer.readIdentifier().content this.key = tokenizer.readIdentifier().content
tokenizer.skipBlank() tokenizer.skipBlank()
assert(tokenizer.peek() === '=', () => `illegal token ${token.getText()}`) assert(tokenizer.peek() === '=', () => `illegal token ${token.getText()}`)
tokenizer.advance() tokenizer.advance()
this.value = new Value(tokenizer.remaining(), this.liquid) this.value = new Value(tokenizer.remaining(), this.liquid)
}, }
render: function * (ctx: Context): Generator<unknown, void, unknown> { * render (ctx: Context): Generator<unknown, void, unknown> {
ctx.bottom()[this.key] = yield this.value.value(ctx, this.liquid.options.lenientIf) ctx.bottom()[this.key] = yield this.value.value(ctx, this.liquid.options.lenientIf)
} }
} as TagImplOptions }
+20 -15
View File
@@ -1,29 +1,34 @@
import BlockMode from '../context/block-mode' import { BlockMode } from '../context'
import { BlockDrop } from '../drop/block-drop' import { isTagToken } from '../util'
import { TagToken, TopLevelToken, Template, Context, TagImpl, Emitter } from '../types' import { BlockDrop } from '../drop'
import { Liquid, TagToken, TopLevelToken, Template, Context, Emitter, Tag } from '..'
export default { export default class extends Tag {
parse (this: TagImpl, token: TagToken, remainTokens: TopLevelToken[]) { private block: string
private tpls: Template[] = []
constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(token, remainTokens, liquid)
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[] while (remainTokens.length) {
this.liquid.parser.parseStream(remainTokens) const token = remainTokens.shift()!
.on('tag:endblock', function () { this.stop() }) if (isTagToken(token) && token.name === 'endblock') return
.on('template', (tpl: Template) => this.tpls.push(tpl)) const template = liquid.parser.parseToken(token, remainTokens)
.on('end', () => { throw new Error(`tag ${token.getText()} not closed`) }) this.tpls.push(template)
.start() }
}, throw new Error(`tag ${token.getText()} not closed`)
}
* render (this: TagImpl, ctx: Context, emitter: Emitter) { * render (ctx: Context, emitter: Emitter) {
const blockRender = this.getBlockRender(ctx) const blockRender = this.getBlockRender(ctx)
if (ctx.getRegister('blockMode') === BlockMode.STORE) { if (ctx.getRegister('blockMode') === BlockMode.STORE) {
ctx.getRegister('blocks')[this.block] = blockRender ctx.getRegister('blocks')[this.block] = blockRender
} else { } else {
yield blockRender(new BlockDrop(), emitter) yield blockRender(new BlockDrop(), emitter)
} }
}, }
getBlockRender (this: TagImpl, ctx: Context) { private getBlockRender (ctx: Context) {
const { liquid, tpls } = this const { liquid, tpls } = this
const renderChild = ctx.getRegister('blocks')[this.block] const renderChild = ctx.getRegister('blocks')[this.block]
const renderCurrent = function * (superBlock: BlockDrop, emitter: Emitter) { const renderCurrent = function * (superBlock: BlockDrop, emitter: Emitter) {
+3 -3
View File
@@ -1,7 +1,7 @@
import { Emitter, Context } from '../types' import { Context, Emitter, Tag } from '..'
export default { export default class extends Tag {
render: function (ctx: Context, emitter: Emitter) { render (ctx: Context, emitter: Emitter) {
emitter['break'] = true emitter['break'] = true
} }
} }
+17 -16
View File
@@ -1,28 +1,29 @@
import { Tokenizer, assert, Template, Context, TagImplOptions, TagToken, TopLevelToken } from '../types' import { Liquid, Tag, Tokenizer, assert, Template, Context, TagToken, TopLevelToken } from '..'
import { evalQuotedToken } from '../render/expression' import { evalQuotedToken } from '../render'
import { isTagToken } from '../util'
export default { export default class extends Tag {
parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) { private variable: string
private templates: Template[] = []
constructor (tagToken: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(tagToken, remainTokens, liquid)
const tokenizer = new Tokenizer(tagToken.args, this.liquid.options.operators) const tokenizer = new Tokenizer(tagToken.args, this.liquid.options.operators)
this.variable = readVariableName(tokenizer) this.variable = readVariableName(tokenizer)!
assert(this.variable, () => `${tagToken.args} not valid identifier`) assert(this.variable, () => `${tagToken.args} not valid identifier`)
this.templates = [] while (remainTokens.length) {
const token = remainTokens.shift()!
const stream = this.liquid.parser.parseStream(remainTokens) if (isTagToken(token) && token.name === 'endcapture') return
stream.on('tag:endcapture', () => stream.stop()) this.templates.push(liquid.parser.parseToken(token, remainTokens))
.on('template', (tpl: Template) => this.templates.push(tpl)) }
.on('end', () => {
throw new Error(`tag ${tagToken.getText()} not closed`) throw new Error(`tag ${tagToken.getText()} not closed`)
}) }
stream.start() * render (ctx: Context): Generator<unknown, void, string> {
},
render: function * (ctx: Context): Generator<unknown, void, string> {
const r = this.liquid.renderer const r = this.liquid.renderer
const html = yield r.renderTemplates(this.templates, ctx) const html = yield r.renderTemplates(this.templates, ctx)
ctx.bottom()[this.variable] = html ctx.bottom()[this.variable] = html
} }
} as TagImplOptions }
function readVariableName (tokenizer: Tokenizer) { function readVariableName (tokenizer: Tokenizer) {
const word = tokenizer.readIdentifier().content const word = tokenizer.readIdentifier().content
+10 -8
View File
@@ -1,10 +1,12 @@
import { toValue, _evalToken, Value, Emitter, TagToken, TopLevelToken, Context, Template, TagImplOptions, ParseStream } from '../types' import { ValueToken, Liquid, Tokenizer, toValue, _evalToken, Value, Emitter, TagToken, TopLevelToken, Context, Template, Tag, ParseStream } from '..'
import { Tokenizer } from '../parser/tokenizer'
export default { export default class extends Tag {
parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) { private cond: Value
private cases: { val?: ValueToken, templates: Template[] }[] = []
private elseTemplates: Template[] = []
constructor (tagToken: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(tagToken, remainTokens, liquid)
this.cond = new Value(tagToken.args, this.liquid) this.cond = new Value(tagToken.args, this.liquid)
this.cases = []
this.elseTemplates = [] this.elseTemplates = []
let p: Template[] = [] let p: Template[] = []
@@ -31,9 +33,9 @@ export default {
}) })
stream.start() stream.start()
}, }
render: function * (ctx: Context, emitter: Emitter) { * render (ctx: Context, emitter: Emitter): Generator<unknown, unknown, unknown> {
const r = this.liquid.renderer const r = this.liquid.renderer
const cond = toValue(yield this.cond.value(ctx, ctx.opts.lenientIf)) const cond = toValue(yield this.cond.value(ctx, ctx.opts.lenientIf))
for (const branch of this.cases) { for (const branch of this.cases) {
@@ -45,4 +47,4 @@ export default {
} }
yield r.renderTemplates(this.elseTemplates, ctx, emitter) yield r.renderTemplates(this.elseTemplates, ctx, emitter)
} }
} as TagImplOptions }
+12 -15
View File
@@ -1,17 +1,14 @@
import { TagToken } from '../tokens/tag-token' import { Liquid, TopLevelToken, TagToken, Tag } from '..'
import { TopLevelToken } from '../tokens/toplevel-token' import { isTagToken } from '../util'
import { TagImplOptions } from '../template/tag/tag-impl-options'
export default { export default class extends Tag {
parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) { constructor (tagToken: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
const stream = this.liquid.parser.parseStream(remainTokens) super(tagToken, remainTokens, liquid)
stream while (remainTokens.length) {
.on('token', (token: TagToken) => { const token = remainTokens.shift()!
if (token.name === 'endcomment') stream.stop() if (isTagToken(token) && token.name === 'endcomment') return
}) }
.on('end', () => { throw new Error(`tag ${tagToken.getText()} not closed`)
throw new Error(`tag ${tagToken.getText()} not closed`) }
}) render () {}
stream.start()
} }
} as TagImplOptions
+3 -3
View File
@@ -1,7 +1,7 @@
import { Emitter, Context } from '../types' import { Tag, Emitter, Context } from '..'
export default { export default class extends Tag {
render: function (ctx: Context, emitter: Emitter) { render (ctx: Context, emitter: Emitter) {
emitter['continue'] = true emitter['continue'] = true
} }
} }
+11 -13
View File
@@ -1,15 +1,14 @@
import { assert } from '../util/assert' import { Tokenizer, assert, TopLevelToken, Liquid, ValueToken, _evalToken, Emitter, TagToken, Context, Tag } from '..'
import { _evalToken, Emitter, TagToken, Context, TagImplOptions } from '../types'
import { Tokenizer } from '../parser/tokenizer'
export default { export default class extends Tag {
parse: function (tagToken: TagToken) { private candidates: ValueToken[] = []
private group?: ValueToken
constructor (tagToken: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(tagToken, remainTokens, liquid)
const tokenizer = new Tokenizer(tagToken.args, this.liquid.options.operators) const tokenizer = new Tokenizer(tagToken.args, this.liquid.options.operators)
const group = tokenizer.readValue() const group = tokenizer.readValue()
tokenizer.skipBlank() tokenizer.skipBlank()
this.candidates = []
if (group) { if (group) {
if (tokenizer.peek() === ':') { if (tokenizer.peek() === ':') {
this.group = group this.group = group
@@ -23,10 +22,10 @@ export default {
tokenizer.readTo(',') tokenizer.readTo(',')
} }
assert(this.candidates.length, () => `empty candidates: ${tagToken.getText()}`) assert(this.candidates.length, () => `empty candidates: ${tagToken.getText()}`)
}, }
render: function * (ctx: Context, emitter: Emitter) { * render (ctx: Context, emitter: Emitter): Generator<unknown, unknown, unknown> {
const group = yield _evalToken(this.group, ctx) const group = (yield _evalToken(this.group, ctx)) as ValueToken
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]
@@ -38,7 +37,6 @@ 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 _evalToken(candidate, ctx) return yield _evalToken(candidate, ctx)
emitter.write(html) }
} }
} as TagImplOptions
+9 -7
View File
@@ -1,16 +1,18 @@
import { Tokenizer, Emitter, TagToken, Context, TagImplOptions } from '../types' import { Tag, Liquid, TopLevelToken, Tokenizer, Emitter, TagToken, Context } from '..'
import { isNumber, stringify } from '../util/underscore' import { isNumber, stringify } from '../util'
export default { export default class extends Tag {
parse: function (token: TagToken) { private variable: string
constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(token, remainTokens, liquid)
const tokenizer = new Tokenizer(token.args, this.liquid.options.operators) const tokenizer = new Tokenizer(token.args, this.liquid.options.operators)
this.variable = tokenizer.readIdentifier().content this.variable = tokenizer.readIdentifier().content
}, }
render: function (context: Context, emitter: Emitter) { render (context: Context, emitter: Emitter) {
const scope = context.environments const scope = context.environments
if (!isNumber(scope[this.variable])) { if (!isNumber(scope[this.variable])) {
scope[this.variable] = 0 scope[this.variable] = 0
} }
emitter.write(stringify(--scope[this.variable])) emitter.write(stringify(--scope[this.variable]))
} }
} as TagImplOptions }
+8 -8
View File
@@ -1,13 +1,13 @@
import { Value } from '../template/value' import { Liquid, TopLevelToken, Emitter, Value, TagToken, Context, Tag } from '..'
import { Emitter } from '../emitters/emitter'
import { TagImplOptions, TagToken, Context } from '../types'
export default { export default class extends Tag {
parse: function (token: TagToken) { private value: Value
constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(token, remainTokens, liquid)
this.value = new Value(token.args, this.liquid) this.value = new Value(token.args, this.liquid)
}, }
render: function * (ctx: Context, emitter: Emitter): Generator<unknown, void, unknown> { * render (ctx: Context, emitter: Emitter): Generator<unknown, void, unknown> {
const val = yield this.value.value(ctx, false) const val = yield this.value.value(ctx, false)
emitter.write(val) emitter.write(val)
} }
} as TagImplOptions }
+16 -13
View File
@@ -1,24 +1,27 @@
import { assert, Tokenizer, _evalToken, Emitter, TagToken, TopLevelToken, Context, Template, TagImplOptions, ParseStream } from '../types' import { Hash, ValueToken, Liquid, Tag, Tokenizer, _evalToken, Emitter, TagToken, TopLevelToken, Context, Template, ParseStream } from '..'
import { toEnumerable } from '../util/collection' import { toEnumerable } from '../util/collection'
import { ForloopDrop } from '../drop/forloop-drop' import { ForloopDrop } from '../drop/forloop-drop'
import { Hash, HashValue } from '../template/tag/hash'
const MODIFIERS = ['offset', 'limit', 'reversed'] const MODIFIERS = ['offset', 'limit', 'reversed']
type valueof<T> = T[keyof T] type valueof<T> = T[keyof T]
export default { export default class extends Tag {
type: 'block', private variable: string
parse: function (token: TagToken, remainTokens: TopLevelToken[]) { private collection: ValueToken
const tokenizer = new Tokenizer(token.args, this.liquid.options.operators) private hash: Hash
private templates: Template[]
private elseTemplates: Template[]
constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(token, remainTokens, liquid)
const tokenizer = new Tokenizer(token.args, this.liquid.options.operators)
const variable = tokenizer.readIdentifier() const variable = tokenizer.readIdentifier()
const inStr = tokenizer.readIdentifier() const inStr = tokenizer.readIdentifier()
const collection = tokenizer.readValue() const collection = tokenizer.readValue()
assert( if (!variable.size() || inStr.content !== 'in' || !collection) {
variable.size() && inStr.content === 'in' && collection, throw new Error(`illegal tag: ${token.getText()}`)
() => `illegal tag: ${token.getText()}` }
)
this.variable = variable.content this.variable = variable.content
this.collection = collection this.collection = collection
@@ -37,8 +40,8 @@ export default {
}) })
stream.start() stream.start()
}, }
render: function * (ctx: Context, emitter: Emitter): Generator<unknown, void | string, HashValue | Template[]> { * render (ctx: Context, emitter: Emitter): Generator<unknown, void | string, Template[]> {
const r = this.liquid.renderer const r = this.liquid.renderer
let collection = toEnumerable(yield _evalToken(this.collection, ctx)) let collection = toEnumerable(yield _evalToken(this.collection, ctx))
@@ -77,7 +80,7 @@ export default {
} }
ctx.pop() ctx.pop()
} }
} as TagImplOptions }
function reversed<T> (arr: Array<T>) { function reversed<T> (arr: Array<T>) {
return [...arr].reverse() return [...arr].reverse()
+10 -9
View File
@@ -1,12 +1,13 @@
import { Value, Emitter, isTruthy, TagToken, TopLevelToken, Context, Template, TagImplOptions } from '../types' import { Liquid, Tag, Value, Emitter, isTruthy, TagToken, TopLevelToken, Context, Template } from '..'
export default { export default class extends Tag {
parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) { private branches: { predicate: Value, templates: Template[] }[] = []
this.branches = [] private elseTemplates: Template[] = []
this.elseTemplates = []
constructor (tagToken: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(tagToken, remainTokens, liquid)
let p let p
this.liquid.parser.parseStream(remainTokens) liquid.parser.parseStream(remainTokens)
.on('start', () => this.branches.push({ .on('start', () => this.branches.push({
predicate: new Value(tagToken.args, this.liquid), predicate: new Value(tagToken.args, this.liquid),
templates: (p = []) templates: (p = [])
@@ -20,9 +21,9 @@ export default {
.on('template', (tpl: Template) => p.push(tpl)) .on('template', (tpl: Template) => p.push(tpl))
.on('end', () => { throw new Error(`tag ${tagToken.getText()} not closed`) }) .on('end', () => { throw new Error(`tag ${tagToken.getText()} not closed`) })
.start() .start()
}, }
render: function * (ctx: Context, emitter: Emitter): Generator<unknown, void, string> { * render (ctx: Context, emitter: Emitter): Generator<unknown, void, string> {
const r = this.liquid.renderer const r = this.liquid.renderer
for (const { predicate, templates } of this.branches) { for (const { predicate, templates } of this.branches) {
@@ -34,4 +35,4 @@ export default {
} }
yield r.renderTemplates(this.elseTemplates, ctx, emitter) yield r.renderTemplates(this.elseTemplates, ctx, emitter)
} }
} as TagImplOptions }
+14 -13
View File
@@ -1,14 +1,15 @@
import { assert, Tokenizer, _evalToken, Hash, Emitter, TagToken, Context, TagImplOptions } from '../types' import { Template, ValueToken, TopLevelToken, Liquid, Tag, assert, Tokenizer, _evalToken, Hash, Emitter, TagToken, Context } from '..'
import BlockMode from '../context/block-mode' import { BlockMode, Scope } from '../context'
import { parseFilePath, renderFilePath } from './render' import { parseFilePath, renderFilePath } from './render'
export default { export default class extends Tag {
parseFilePath, private withVar?: ValueToken
renderFilePath, private hash: Hash
parse: function (token: TagToken) { constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(token, remainTokens, liquid)
const args = token.args const args = token.args
const tokenizer = new Tokenizer(args, this.liquid.options.operators) const tokenizer = new Tokenizer(args, this.liquid.options.operators)
this['file'] = this.parseFilePath(tokenizer, this.liquid) this['file'] = parseFilePath(tokenizer, this.liquid)
this['currentFile'] = token.file this['currentFile'] = token.file
const begin = tokenizer.p const begin = tokenizer.p
@@ -21,22 +22,22 @@ export default {
} else tokenizer.p = begin } else tokenizer.p = begin
this.hash = new Hash(tokenizer.remaining(), this.liquid.options.jekyllInclude) this.hash = new Hash(tokenizer.remaining(), this.liquid.options.jekyllInclude)
}, }
render: function * (ctx: Context, emitter: Emitter) { * render (ctx: Context, emitter: Emitter): Generator<unknown, void, unknown> {
const { liquid, hash, withVar } = this const { liquid, hash, withVar } = this
const { renderer } = liquid const { renderer } = liquid
const filepath = yield this.renderFilePath(this['file'], ctx, liquid) const filepath = (yield renderFilePath(this['file'], ctx, liquid)) as string
assert(filepath, () => `illegal filename "${filepath}"`) assert(filepath, () => `illegal filename "${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)) as Scope
if (withVar) scope[filepath] = yield _evalToken(withVar, ctx) if (withVar) scope[filepath] = yield _evalToken(withVar, ctx)
const templates = yield liquid._parsePartialFile(filepath, ctx.sync, this['currentFile']) const templates = (yield liquid._parsePartialFile(filepath, ctx.sync, this['currentFile'])) as Template[]
ctx.push(ctx.opts.jekyllInclude ? { include: scope } : scope) ctx.push(ctx.opts.jekyllInclude ? { include: scope } : scope)
yield renderer.renderTemplates(templates, ctx, emitter) yield renderer.renderTemplates(templates, ctx, emitter)
ctx.pop() ctx.pop()
ctx.restoreRegister(saved) ctx.restoreRegister(saved)
} }
} as TagImplOptions }
+9 -7
View File
@@ -1,12 +1,14 @@
import { isNumber, stringify } from '../util/underscore' import { isNumber, stringify } from '../util'
import { Tokenizer, Emitter, TagToken, Context, TagImplOptions } from '../types' import { Tag, Liquid, TopLevelToken, Tokenizer, Emitter, TagToken, Context } from '..'
export default { export default class extends Tag {
parse: function (token: TagToken) { private variable: string
constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(token, remainTokens, liquid)
const tokenizer = new Tokenizer(token.args, this.liquid.options.operators) const tokenizer = new Tokenizer(token.args, this.liquid.options.operators)
this.variable = tokenizer.readIdentifier().content this.variable = tokenizer.readIdentifier().content
}, }
render: function (context: Context, emitter: Emitter) { render (context: Context, emitter: Emitter) {
const scope = context.environments const scope = context.environments
if (!isNumber(scope[this.variable])) { if (!isNumber(scope[this.variable])) {
scope[this.variable] = 0 scope[this.variable] = 0
@@ -15,4 +17,4 @@ export default {
scope[this.variable]++ scope[this.variable]++
emitter.write(stringify(val)) emitter.write(stringify(val))
} }
} as TagImplOptions }
+2 -2
View File
@@ -19,8 +19,8 @@ import Continue from './continue'
import echo from './echo' import echo from './echo'
import liquid from './liquid' import liquid from './liquid'
import inlineComment from './inline-comment' import inlineComment from './inline-comment'
import { TagImplOptions } from '../template/tag/tag-impl-options' import type { TagClass } from '../template/tag'
export const tags: { [key: string]: TagImplOptions } = { export const tags: Record<string, TagClass> = {
assign, 'for': For, capture, 'case': Case, comment, include, render, decrement, increment, cycle, 'if': If, layout, block, raw, tablerow, unless, 'break': Break, 'continue': Continue, echo, liquid, '#': inlineComment assign, 'for': For, capture, 'case': Case, comment, include, render, decrement, increment, cycle, 'if': If, layout, block, raw, tablerow, unless, 'break': Break, 'continue': Continue, echo, liquid, '#': inlineComment
} }
+6 -6
View File
@@ -1,11 +1,11 @@
import { TagToken } from '../tokens/tag-token' import { TagToken, Liquid, TopLevelToken, Tag } from '..'
import { TopLevelToken } from '../tokens/toplevel-token'
import { TagImplOptions } from '../template/tag/tag-impl-options'
export default { export default class extends Tag {
parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) { constructor (tagToken: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(tagToken, remainTokens, liquid)
if (tagToken.args.search(/\n\s*[^#\s]/g) !== -1) { if (tagToken.args.search(/\n\s*[^#\s]/g) !== -1) {
throw new Error('every line of an inline comment must start with a \'#\' character') throw new Error('every line of an inline comment must start with a \'#\' character')
} }
} }
} as TagImplOptions render () { }
}
+18 -16
View File
@@ -1,29 +1,31 @@
import { assert, Tokenizer, Emitter, Hash, TagToken, TopLevelToken, Context, TagImplOptions } from '../types' import { Scope, Template, Liquid, Tag, assert, Tokenizer, Emitter, Hash, TagToken, TopLevelToken, Context } from '..'
import BlockMode from '../context/block-mode' import { BlockMode } from '../context'
import { parseFilePath, renderFilePath } from './render' import { parseFilePath, renderFilePath, ParsedFileName } from './render'
import { BlankDrop } from '../drop/blank-drop' import { BlankDrop } from '../drop'
export default { export default class extends Tag {
parseFilePath, private hash: Hash
renderFilePath, private tpls: Template[]
parse: function (token: TagToken, remainTokens: TopLevelToken[]) { private file?: ParsedFileName
constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(token, remainTokens, liquid)
const tokenizer = new Tokenizer(token.args, this.liquid.options.operators) const tokenizer = new Tokenizer(token.args, this.liquid.options.operators)
this['file'] = this.parseFilePath(tokenizer, this.liquid) this.file = parseFilePath(tokenizer, this.liquid)
this['currentFile'] = token.file this['currentFile'] = token.file
this.hash = new Hash(tokenizer.remaining()) this.hash = new Hash(tokenizer.remaining())
this.tpls = this.liquid.parser.parseTokens(remainTokens) this.tpls = this.liquid.parser.parseTokens(remainTokens)
}, }
render: function * (ctx: Context, emitter: Emitter) { * render (ctx: Context, emitter: Emitter): Generator<unknown, unknown, unknown> {
const { liquid, hash, file } = this const { liquid, hash, file } = this
const { renderer } = liquid const { renderer } = liquid
if (file === null) { if (file === undefined) {
ctx.setRegister('blockMode', BlockMode.OUTPUT) ctx.setRegister('blockMode', BlockMode.OUTPUT)
yield renderer.renderTemplates(this.tpls, ctx, emitter) yield renderer.renderTemplates(this.tpls, ctx, emitter)
return return
} }
const filepath = yield this.renderFilePath(this['file'], ctx, liquid) const filepath = (yield renderFilePath(this.file, ctx, liquid)) as string
assert(filepath, () => `illegal filename "${filepath}"`) assert(filepath, () => `illegal filename "${filepath}"`)
const templates = yield liquid._parseLayoutFile(filepath, ctx.sync, this['currentFile']) const templates = (yield liquid._parseLayoutFile(filepath, ctx.sync, this['currentFile'])) as Template[]
// render remaining contents and store rendered results // render remaining contents and store rendered results
ctx.setRegister('blockMode', BlockMode.STORE) ctx.setRegister('blockMode', BlockMode.STORE)
@@ -35,8 +37,8 @@ export default {
ctx.setRegister('blockMode', BlockMode.OUTPUT) ctx.setRegister('blockMode', BlockMode.OUTPUT)
// render the layout file use stored blocks // render the layout file use stored blocks
ctx.push(yield hash.render(ctx)) ctx.push((yield hash.render(ctx)) as Scope)
yield renderer.renderTemplates(templates, ctx, emitter) yield renderer.renderTemplates(templates, ctx, emitter)
ctx.pop() ctx.pop()
} }
} as TagImplOptions }
+8 -8
View File
@@ -1,14 +1,14 @@
import { Emitter } from '../emitters/emitter' import { Template, Tokenizer, Emitter, Liquid, TopLevelToken, TagToken, Context, Tag } from '..'
import { TagImplOptions, TagToken, Context } from '../types'
import { Tokenizer } from '../parser/tokenizer'
export default { export default class extends Tag {
parse: function (token: TagToken) { private tpls: Template[]
constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(token, remainTokens, liquid)
const tokenizer = new Tokenizer(token.args, this.liquid.options.operators) const tokenizer = new Tokenizer(token.args, this.liquid.options.operators)
const tokens = tokenizer.readLiquidTagTokens(this.liquid.options) const tokens = tokenizer.readLiquidTagTokens(this.liquid.options)
this.tpls = this.liquid.parser.parseTokens(tokens) this.tpls = this.liquid.parser.parseTokens(tokens)
}, }
render: function * (ctx: Context, emitter: Emitter): Generator<unknown, void, unknown> { * render (ctx: Context, emitter: Emitter): Generator<unknown, void, unknown> {
yield this.liquid.renderer.renderTemplates(this.tpls, ctx, emitter) yield this.liquid.renderer.renderTemplates(this.tpls, ctx, emitter)
} }
} as TagImplOptions }
+14 -17
View File
@@ -1,21 +1,18 @@
import { TagToken, TopLevelToken, TagImplOptions } from '../types' import { Liquid, TagToken, TopLevelToken, Tag } from '..'
import { isTagToken } from '../util'
export default { export default class extends Tag {
parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) { private tokens: TopLevelToken[] = []
this.tokens = [] constructor (tagToken: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(tagToken, remainTokens, liquid)
const stream = this.liquid.parser.parseStream(remainTokens) while (remainTokens.length) {
stream const token = remainTokens.shift()!
.on('token', (token: TagToken) => { if (isTagToken(token) && token.name === 'endraw') return
if (token.name === 'endraw') stream.stop() this.tokens.push(token)
else this.tokens.push(token) }
})
.on('end', () => {
throw new Error(`tag ${tagToken.getText()} not closed`) throw new Error(`tag ${tagToken.getText()} not closed`)
}) }
stream.start() render () {
},
render: function () {
return this.tokens.map((token: TopLevelToken) => token.getText()).join('') return this.tokens.map((token: TopLevelToken) => token.getText()).join('')
} }
} as TagImplOptions }
+24 -26
View File
@@ -1,18 +1,20 @@
import { __assign } from 'tslib' import { __assign } from 'tslib'
import { assert } from '../util/assert' import { ForloopDrop } from '../drop'
import { ForloopDrop } from '../drop/forloop-drop' import { toEnumerable } from '../util'
import { toEnumerable } from '../util/collection' import { TopLevelToken, assert, Liquid, Token, Template, evalQuotedToken, TypeGuards, Tokenizer, _evalToken, Hash, Emitter, TagToken, Context, Tag } from '..'
import { Liquid } from '../liquid'
import { Token, Template, evalQuotedToken, TypeGuards, Tokenizer, _evalToken, Hash, Emitter, TagToken, Context, TagImplOptions } from '../types'
export default { export type ParsedFileName = Template[] | Token | string | undefined
parseFilePath,
renderFilePath, export default class extends Tag {
parse: function (token: TagToken) { private file: ParsedFileName
private currentFile?: string
private hash: Hash
constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(token, remainTokens, liquid)
const args = token.args const args = token.args
const tokenizer = new Tokenizer(args, this.liquid.options.operators) const tokenizer = new Tokenizer(args, this.liquid.options.operators)
this['file'] = this.parseFilePath(tokenizer, this.liquid) this.file = parseFilePath(tokenizer, this.liquid)
this['currentFile'] = token.file this.currentFile = token.file
while (!tokenizer.end()) { while (!tokenizer.end()) {
tokenizer.skipBlank() tokenizer.skipBlank()
const begin = tokenizer.p const begin = tokenizer.p
@@ -33,8 +35,7 @@ export default {
this[keyword.content] = { value, alias: alias && alias.content } this[keyword.content] = { value, alias: alias && alias.content }
tokenizer.skipBlank() tokenizer.skipBlank()
if (tokenizer.peek() === ',') tokenizer.advance() if (tokenizer.peek() === ',') tokenizer.advance()
// matched! continue // matched!
continue
} }
} }
} }
@@ -45,10 +46,10 @@ export default {
break break
} }
this.hash = new Hash(tokenizer.remaining()) this.hash = new Hash(tokenizer.remaining())
}, }
render: function * (ctx: Context, emitter: Emitter) { * render (ctx: Context, emitter: Emitter): Generator<unknown, void, unknown> {
const { liquid, hash } = this const { liquid, hash } = this
const filepath = yield this.renderFilePath(this['file'], ctx, liquid) const filepath = (yield renderFilePath(this['file'], ctx, liquid)) as string
assert(filepath, () => `illegal filename "${filepath}"`) assert(filepath, () => `illegal filename "${filepath}"`)
const childCtx = new Context({}, ctx.opts, { sync: ctx.sync, globals: ctx.globals, strictVariables: ctx.strictVariables }) const childCtx = new Context({}, ctx.opts, { sync: ctx.sync, globals: ctx.globals, strictVariables: ctx.strictVariables })
@@ -61,23 +62,20 @@ export default {
if (this['for']) { if (this['for']) {
const { value, alias } = this['for'] const { value, alias } = this['for']
let collection = yield _evalToken(value, ctx) const collection = toEnumerable(yield _evalToken(value, ctx))
collection = toEnumerable(collection)
scope['forloop'] = new ForloopDrop(collection.length, value.getText(), alias) scope['forloop'] = new ForloopDrop(collection.length, value.getText(), alias)
for (const item of collection) { for (const item of collection) {
scope[alias] = item scope[alias] = item
const templates = yield liquid._parsePartialFile(filepath, childCtx.sync, this['currentFile']) const templates = (yield liquid._parsePartialFile(filepath, childCtx.sync, this['currentFile'])) as Template[]
yield liquid.renderer.renderTemplates(templates, childCtx, emitter) yield liquid.renderer.renderTemplates(templates, childCtx, emitter)
scope['forloop'].next() scope['forloop'].next()
} }
} else { } else {
const templates = yield liquid._parsePartialFile(filepath, childCtx.sync, this['currentFile']) const templates = (yield liquid._parsePartialFile(filepath, childCtx.sync, this['currentFile'])) as Template[]
yield liquid.renderer.renderTemplates(templates, childCtx, emitter) yield liquid.renderer.renderTemplates(templates, childCtx, emitter)
} }
} }
} as TagImplOptions }
type ParsedFileName = Template[] | Token | string | undefined
/** /**
* @return null for "none", * @return null for "none",
@@ -85,11 +83,11 @@ type ParsedFileName = Template[] | Token | string | undefined
* @return Token for expression (not quoted) * @return Token for expression (not quoted)
* @throws TypeError if cannot read next token * @throws TypeError if cannot read next token
*/ */
export function parseFilePath (tokenizer: Tokenizer, liquid: Liquid): ParsedFileName | null { export function parseFilePath (tokenizer: Tokenizer, liquid: Liquid): ParsedFileName {
if (liquid.options.dynamicPartials) { if (liquid.options.dynamicPartials) {
const file = tokenizer.readValue() const file = tokenizer.readValue()
if (file === undefined) throw new TypeError(`illegal argument "${tokenizer.input}"`) if (file === undefined) throw new TypeError(`illegal argument "${tokenizer.input}"`)
if (file.getText() === 'none') return null if (file.getText() === 'none') return
if (TypeGuards.isQuotedToken(file)) { if (TypeGuards.isQuotedToken(file)) {
// for filenames like "files/{{file}}", eval as liquid template // for filenames like "files/{{file}}", eval as liquid template
const templates = liquid.parse(evalQuotedToken(file)) const templates = liquid.parse(evalQuotedToken(file))
@@ -99,7 +97,7 @@ export function parseFilePath (tokenizer: Tokenizer, liquid: Liquid): ParsedFile
} }
const tokens = [...tokenizer.readFileNameTemplate(liquid.options)] const tokens = [...tokenizer.readFileNameTemplate(liquid.options)]
const templates = optimize(liquid.parser.parseTokens(tokens)) const templates = optimize(liquid.parser.parseTokens(tokens))
return templates === 'none' ? null : templates return templates === 'none' ? undefined : templates
} }
function optimize (templates: Template[]): string | Template[] { function optimize (templates: Template[]): string | Template[] {
+18 -10
View File
@@ -1,20 +1,28 @@
import { toEnumerable } from '../util/collection' import { toEnumerable } from '../util/collection'
import { assert, _evalToken, Emitter, Hash, TagToken, TopLevelToken, Context, Template, TagImplOptions, ParseStream } from '../types' import { ValueToken, Liquid, Tag, _evalToken, Emitter, Hash, TagToken, TopLevelToken, Context, Template, ParseStream } from '..'
import { TablerowloopDrop } from '../drop/tablerowloop-drop' import { TablerowloopDrop } from '../drop/tablerowloop-drop'
import { Tokenizer } from '../parser/tokenizer' import { Tokenizer } from '../parser/tokenizer'
export default { export default class extends Tag {
parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) { private variable: string
private hash: Hash
private templates: Template[]
private collection: ValueToken
constructor (tagToken: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(tagToken, remainTokens, liquid)
const tokenizer = new Tokenizer(tagToken.args, this.liquid.options.operators) const tokenizer = new Tokenizer(tagToken.args, this.liquid.options.operators)
const variable = tokenizer.readIdentifier() const variable = tokenizer.readIdentifier()
tokenizer.skipBlank() tokenizer.skipBlank()
const tmp = tokenizer.readIdentifier() const predicate = tokenizer.readIdentifier()
assert(tmp && tmp.content === 'in', () => `illegal tag: ${tagToken.getText()}`) const collectionToken = tokenizer.readValue()
if (predicate.content !== 'in' || !collectionToken) {
throw new Error(`illegal tag: ${tagToken.getText()}`)
}
this.variable = variable.content this.variable = variable.content
this.collection = tokenizer.readValue() this.collection = collectionToken
this.hash = new Hash(tokenizer.remaining()) this.hash = new Hash(tokenizer.remaining())
this.templates = [] this.templates = []
@@ -28,11 +36,11 @@ export default {
}) })
stream.start() stream.start()
}, }
render: function * (ctx: Context, emitter: Emitter) { * render (ctx: Context, emitter: Emitter): Generator<unknown, void, unknown> {
let collection = toEnumerable(yield _evalToken(this.collection, ctx)) let collection = toEnumerable(yield _evalToken(this.collection, ctx))
const hash = yield this.hash.render(ctx) const hash = (yield this.hash.render(ctx)) as Record<string, any>
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
@@ -57,4 +65,4 @@ export default {
if (collection.length) emitter.write('</tr>') if (collection.length) emitter.write('</tr>')
ctx.pop() ctx.pop()
} }
} as TagImplOptions }
+9 -8
View File
@@ -1,9 +1,10 @@
import { Value, TopLevelToken, Template, Emitter, isTruthy, isFalsy, Context, TagImplOptions, TagToken } from '../types' import { Liquid, Tag, Value, TopLevelToken, Template, Emitter, isTruthy, isFalsy, Context, TagToken } from '..'
export default { export default class extends Tag {
parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) { private branches: { predicate: Value, test: (val: any, ctx: Context) => boolean, templates: Template[] }[] = []
this.branches = [] private elseTemplates: Template[] = []
this.elseTemplates = [] constructor (tagToken: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(tagToken, remainTokens, liquid)
let p let p
this.liquid.parser.parseStream(remainTokens) this.liquid.parser.parseStream(remainTokens)
.on('start', () => this.branches.push({ .on('start', () => this.branches.push({
@@ -21,9 +22,9 @@ export default {
.on('template', (tpl: Template) => p.push(tpl)) .on('template', (tpl: Template) => p.push(tpl))
.on('end', () => { throw new Error(`tag ${tagToken.getText()} not closed`) }) .on('end', () => { throw new Error(`tag ${tagToken.getText()} not closed`) })
.start() .start()
}, }
render: function * (ctx: Context, emitter: Emitter) { * render (ctx: Context, emitter: Emitter): Generator<unknown, unknown, unknown> {
const r = this.liquid.renderer const r = this.liquid.renderer
for (const { predicate, test, templates } of this.branches) { for (const { predicate, test, templates } of this.branches) {
@@ -36,4 +37,4 @@ export default {
yield r.renderTemplates(this.elseTemplates, ctx, emitter) yield r.renderTemplates(this.elseTemplates, ctx, emitter)
} }
} as TagImplOptions }
+11
View File
@@ -0,0 +1,11 @@
import type { Context } from '../context'
import type { Liquid } from '../liquid'
export interface FilterImpl {
context: Context;
liquid: Liquid;
}
export interface FilterImplOptions {
(this: FilterImpl, value: any, ...args: any[]): any;
}
@@ -1,9 +1,9 @@
import { _evalToken } from '../../render/expression' import { _evalToken } from '../render'
import { Context } from '../../context/context' import { Context } from '../context'
import { identify } from '../../util/underscore' import { identify } from '../util/underscore'
import { FilterImplOptions } from './filter-impl-options' import { FilterImplOptions } from './filter-impl-options'
import { FilterArg, isKeyValuePair } from '../../parser/filter-arg' import { FilterArg, isKeyValuePair } from '../parser/filter-arg'
import { Liquid } from '../../liquid' import { Liquid } from '../liquid'
export class Filter { export class Filter {
public name: string public name: string
@@ -11,7 +11,7 @@ export class Filter {
private impl: FilterImplOptions private impl: FilterImplOptions
private liquid: Liquid private liquid: Liquid
public constructor (name: string, impl: FilterImplOptions, args: FilterArg[], liquid: Liquid) { public constructor (name: string, impl: FilterImplOptions | undefined, args: FilterArg[], liquid: Liquid) {
this.name = name this.name = name
this.impl = impl || identify this.impl = impl || identify
this.args = args this.args = args
@@ -1,5 +0,0 @@
import { FilterImpl } from './filter-impl'
export interface FilterImplOptions {
(this: FilterImpl, value: any, ...args: any[]): any;
}
-7
View File
@@ -1,7 +0,0 @@
import { Context } from '../../context/context'
import { Liquid } from '../../liquid'
export interface FilterImpl {
context: Context;
liquid: Liquid;
}
-28
View File
@@ -1,28 +0,0 @@
import { FilterImplOptions } from './filter-impl-options'
import { Filter } from './filter'
import { FilterArg } from '../../parser/filter-arg'
import { assert } from '../../util/assert'
import { Liquid } from '../../liquid'
export class FilterMap {
private impls: {[key: string]: FilterImplOptions} = {}
constructor (
private readonly strictFilters: boolean,
private readonly liquid: Liquid
) {}
get (name: string) {
const impl = this.impls[name]
assert(impl || !this.strictFilters, () => `undefined filter: ${name}`)
return impl
}
set (name: string, impl: FilterImplOptions) {
this.impls[name] = impl
}
create (name: string, args: FilterArg[]) {
return new Filter(name, this.get(name), args, this.liquid)
}
}
@@ -1,10 +1,9 @@
import { _evalToken } 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'
import { Token } from '../tokens/token'
export interface HashValue { type HashValueTokens = Record<string, Token | undefined>
[key: string]: any;
}
/** /**
* Key-Value Pairs Representing Tag Arguments * Key-Value Pairs Representing Tag Arguments
@@ -15,14 +14,14 @@ export interface HashValue {
* hash['reversed'] === undefined * hash['reversed'] === undefined
*/ */
export class Hash { export class Hash {
hash: HashValue = {} hash: HashValueTokens = {}
constructor (markup: string, jekyllStyle?: boolean) { constructor (markup: string, jekyllStyle?: boolean) {
const tokenizer = new Tokenizer(markup, {}) const tokenizer = new Tokenizer(markup, {})
for (const hash of tokenizer.readHashes(jekyllStyle)) { for (const hash of tokenizer.readHashes(jekyllStyle)) {
this.hash[hash.name.content] = hash.value this.hash[hash.name.content] = hash.value
} }
} }
* render (ctx: Context): Generator<unknown, HashValue, unknown> { * render (ctx: Context): Generator<unknown, Record<string, any>, unknown> {
const hash = {} const hash = {}
for (const key of Object.keys(this.hash)) { for (const key of Object.keys(this.hash)) {
hash[key] = this.hash[key] === undefined ? true : yield _evalToken(this.hash[key], ctx) hash[key] = this.hash[key] === undefined ? true : yield _evalToken(this.hash[key], ctx)
+4 -5
View File
@@ -1,8 +1,7 @@
import { TemplateImpl } from '../template/template-impl' import { TemplateImpl, Template } from '../template'
import { Template } from '../template/template' import { HTMLToken } from '../tokens'
import { HTMLToken } from '../tokens/html-token' import { Context } from '../context'
import { Context } from '../context/context' import { Emitter } from '../emitters'
import { Emitter } from '../emitters/emitter'
export class HTML extends TemplateImpl<HTMLToken> implements Template { export class HTML extends TemplateImpl<HTMLToken> implements Template {
private str: string private str: string
+10
View File
@@ -0,0 +1,10 @@
export * from './template'
export * from './template-impl'
export * from './tag'
export * from './tag-options-adapter'
export * from './filter'
export * from './filter-impl-options'
export * from './hash'
export * from './value'
export * from './output'
export * from './html'
+2 -3
View File
@@ -1,11 +1,10 @@
import { Value } from './value' import { Value } from './value'
import { TemplateImpl } from '../template/template-impl' import { Template, TemplateImpl } from '../template'
import { Template } from '../template/template'
import { Context } from '../context/context' import { Context } from '../context/context'
import { Emitter } from '../emitters/emitter' import { Emitter } from '../emitters/emitter'
import { OutputToken } from '../tokens/output-token' import { OutputToken } from '../tokens/output-token'
import { Liquid } from '../liquid' import { Liquid } from '../liquid'
import { Filter } from './filter/filter' import { Filter } from './filter'
export class Output extends TemplateImpl<OutputToken> implements Template { export class Output extends TemplateImpl<OutputToken> implements Template {
private value: Value private value: Value
+27
View File
@@ -0,0 +1,27 @@
import { isFunction } from '../util'
import { Hash } from './hash'
import { Tag, TagClass, TagRenderReturn } from './tag'
import { TagToken, TopLevelToken } from '../tokens'
import { Emitter } from '../emitters'
import { Context } from '../context'
import type { Liquid } from '../liquid'
export interface TagImplOptions {
parse?: (this: Tag, token: TagToken, remainingTokens: TopLevelToken[]) => void;
render: (this: Tag, ctx: Context, emitter: Emitter, hash: Record<string, any>) => TagRenderReturn;
}
export function createTagClass (options: TagImplOptions): TagClass {
return class extends Tag {
constructor (token: TagToken, tokens: TopLevelToken[], liquid: Liquid) {
super(token, tokens, liquid)
if (isFunction(options.parse)) {
options.parse.call(this, token, tokens)
}
}
* render (ctx: Context, emitter: Emitter): TagRenderReturn {
const hash = (yield new Hash(this.token.args).render(ctx)) as Record<string, any>
return yield options.render.call(this, ctx, emitter, hash)
}
}
}
+24
View File
@@ -0,0 +1,24 @@
import { TemplateImpl } from './template-impl'
import type { Emitter } from '../emitters/emitter'
import type { Context } from '../context/context'
import type { TopLevelToken, TagToken } from '../tokens'
import type { Template } from './template'
import type { Liquid } from '../liquid'
export type TagRenderReturn = Generator<unknown, unknown, unknown> | Promise<unknown> | unknown
export abstract class Tag extends TemplateImpl<TagToken> implements Template {
public name: string
protected liquid: Liquid
public constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(token)
this.name = token.name
this.liquid = liquid
}
public abstract render (ctx: Context, emitter: Emitter): TagRenderReturn;
}
export interface TagClass {
new(token: TagToken, tokens: TopLevelToken[], liquid: Liquid): Tag
}
-11
View File
@@ -1,11 +0,0 @@
import { Context } from '../../context/context'
import { TagToken } from '../../tokens/tag-token'
import { TopLevelToken } from '../../tokens/toplevel-token'
import { TagImpl } from './tag-impl'
import { HashValue } from '../../template/tag/hash'
import { Emitter } from '../../emitters/emitter'
export interface TagImplOptions {
parse?: (this: TagImpl, token: TagToken, remainingTokens: TopLevelToken[]) => void;
render: (this: TagImpl, ctx: Context, emitter: Emitter, hash: HashValue) => void | string | Promise<void | string> | Generator<unknown, void | string, unknown>;
}
-7
View File
@@ -1,7 +0,0 @@
import { Liquid } from '../../liquid'
import { TagImplOptions } from './tag-impl-options'
export interface TagImpl extends TagImplOptions {
liquid: Liquid;
[key: string]: any;
}
-16
View File
@@ -1,16 +0,0 @@
import { TagImplOptions } from './tag-impl-options'
import { assert } from '../../util/assert'
export class TagMap {
private impls: {[key: string]: TagImplOptions} = {}
get (name: string) {
const impl = this.impls[name]
assert(impl, () => `tag "${name}" not found`)
return impl
}
set (name: string, impl: TagImplOptions) {
this.impls[name] = impl
}
}
-29
View File
@@ -1,29 +0,0 @@
import { isFunction } from '../../util/underscore'
import { Liquid } from '../../liquid'
import { TemplateImpl } from '../../template/template-impl'
import { Emitter, Hash, Context, TagToken, Template, TopLevelToken } from '../../types'
import { TagImpl } from './tag-impl'
import { HashValue } from './hash'
export class Tag extends TemplateImpl<TagToken> implements Template {
public name: string
private impl: TagImpl
public constructor (token: TagToken, tokens: TopLevelToken[], liquid: Liquid) {
super(token)
this.name = token.name
const impl = liquid.tags.get(token.name)
this.impl = Object.create(impl)
this.impl.liquid = liquid
if (this.impl.parse) {
this.impl.parse(token, tokens)
}
}
public * render (ctx: Context, emitter: Emitter): Generator<unknown, unknown, HashValue | unknown> {
const hash = (yield new Hash(this.token.args).render(ctx)) as HashValue
const impl = this.impl
if (isFunction(impl.render)) return yield impl.render(ctx, emitter, hash)
}
}
+12 -6
View File
@@ -1,8 +1,9 @@
import { Expression } from '../render/expression' import { Filter } from './filter'
import { Tokenizer } from '../parser/tokenizer' import { Expression } from '../render'
import { Filter } from './filter/filter' import { Tokenizer } from '../parser'
import { Context } from '../context/context' import { assert } from '../util'
import { Liquid } from '../liquid' import type { Liquid } from '../liquid'
import type { Context } from '../context'
export class Value { export class Value {
public readonly filters: Filter[] = [] public readonly filters: Filter[] = []
@@ -14,7 +15,7 @@ export class Value {
public constructor (str: string, liquid: Liquid) { public constructor (str: string, liquid: Liquid) {
const tokenizer = new Tokenizer(str, liquid.options.operators) const tokenizer = new Tokenizer(str, liquid.options.operators)
this.initial = tokenizer.readExpression() this.initial = tokenizer.readExpression()
this.filters = tokenizer.readFilters().map(({ name, args }) => new Filter(name, liquid.filters.get(name), args, liquid)) this.filters = tokenizer.readFilters().map(({ name, args }) => new Filter(name, this.getFilter(liquid, name), args, liquid))
} }
public * value (ctx: Context, lenient: boolean): Generator<unknown, unknown, unknown> { public * value (ctx: Context, lenient: boolean): Generator<unknown, unknown, unknown> {
lenient = lenient || (ctx.opts.lenientIf && this.filters.length > 0 && this.filters[0].name === 'default') lenient = lenient || (ctx.opts.lenientIf && this.filters.length > 0 && this.filters[0].name === 'default')
@@ -25,4 +26,9 @@ export class Value {
} }
return val return val
} }
private getFilter (liquid: Liquid, name: string) {
const impl = liquid.filters[name]
assert(impl || !liquid.options.strictFilters, () => `undefined filter: ${name}`)
return impl
}
} }
+2 -2
View File
@@ -1,6 +1,6 @@
import { Token } from './token' import { Token } from './token'
import { TokenKind } from '../parser/token-kind' import { TokenKind } from '../parser'
import { last } from '../util/underscore' import { last } from '../util'
export abstract class DelimitedToken extends Token { export abstract class DelimitedToken extends Token {
public trimLeft = false public trimLeft = false
+1 -1
View File
@@ -1,6 +1,6 @@
import { Token } from './token' import { Token } from './token'
import { FilterArg } from '../parser/filter-arg' import { FilterArg } from '../parser/filter-arg'
import { TokenKind } from '../parser/token-kind' import { TokenKind } from '../parser'
export class FilterToken extends Token { export class FilterToken extends Token {
public constructor ( public constructor (
+1 -1
View File
@@ -1,7 +1,7 @@
import { Token } from './token' import { Token } from './token'
import { ValueToken } from './value-token' import { ValueToken } from './value-token'
import { IdentifierToken } from './identifier-token' import { IdentifierToken } from './identifier-token'
import { TokenKind } from '../parser/token-kind' import { TokenKind } from '../parser'
export class HashToken extends Token { export class HashToken extends Token {
constructor ( constructor (
+1 -1
View File
@@ -1,5 +1,5 @@
import { Token } from './token' import { Token } from './token'
import { TokenKind } from '../parser/token-kind' import { TokenKind } from '../parser'
export class HTMLToken extends Token { export class HTMLToken extends Token {
trimLeft = 0 trimLeft = 0
+2 -2
View File
@@ -1,6 +1,6 @@
import { Token } from './token' import { Token } from './token'
import { NUMBER, TYPES, SIGN } from '../util/character' import { NUMBER, TYPES, SIGN } from '../util'
import { TokenKind } from '../parser/token-kind' import { TokenKind } from '../parser'
export class IdentifierToken extends Token { export class IdentifierToken extends Token {
public content: string public content: string
+17
View File
@@ -0,0 +1,17 @@
export * from './top-level-token'
export * from './tag-token'
export * from './output-token'
export * from './html-token'
export * from './number-token'
export * from './identifier-token'
export * from './literal-token'
export * from './operator-token'
export * from './property-access-token'
export * from './filter-token'
export * from './hash-token'
export * from './quoted-token'
export * from './token'
export * from './range-token'
export * from './value-token'
export * from './liquid-tag-token'
export * from './delimited-token'
+2 -3
View File
@@ -1,8 +1,7 @@
import { DelimitedToken } from './delimited-token' import { DelimitedToken } from './delimited-token'
import { TokenizationError } from '../util/error' import { TokenizationError } from '../util'
import { NormalizedFullOptions } from '../liquid-options' import { NormalizedFullOptions } from '../liquid-options'
import { TokenKind } from '../parser/token-kind' import { Tokenizer, TokenKind } from '../parser'
import { Tokenizer } from '../parser/tokenizer'
export class LiquidTagToken extends DelimitedToken { export class LiquidTagToken extends DelimitedToken {
public name: string public name: string
+1 -1
View File
@@ -1,5 +1,5 @@
import { Token } from './token' import { Token } from './token'
import { TokenKind } from '../parser/token-kind' import { TokenKind } from '../parser'
export class LiteralToken extends Token { export class LiteralToken extends Token {
public literal: string public literal: string
+1 -1
View File
@@ -1,6 +1,6 @@
import { Token } from './token' import { Token } from './token'
import { IdentifierToken } from './identifier-token' import { IdentifierToken } from './identifier-token'
import { TokenKind } from '../parser/token-kind' import { TokenKind } from '../parser'
export class NumberToken extends Token { export class NumberToken extends Token {
constructor ( constructor (
+1 -1
View File
@@ -1,5 +1,5 @@
import { Token } from './token' import { Token } from './token'
import { TokenKind } from '../parser/token-kind' import { TokenKind } from '../parser'
export const precedence = { export const precedence = {
'==': 1, '==': 1,
+1 -1
View File
@@ -1,6 +1,6 @@
import { DelimitedToken } from './delimited-token' import { DelimitedToken } from './delimited-token'
import { NormalizedFullOptions } from '../liquid-options' import { NormalizedFullOptions } from '../liquid-options'
import { TokenKind } from '../parser/token-kind' import { TokenKind } from '../parser'
export class OutputToken extends DelimitedToken { export class OutputToken extends DelimitedToken {
public constructor ( public constructor (
+1 -2
View File
@@ -1,8 +1,7 @@
import { Token } from './token' import { Token } from './token'
import { IdentifierToken } from './identifier-token' import { IdentifierToken } from './identifier-token'
import { QuotedToken } from './quoted-token' import { QuotedToken } from './quoted-token'
import { TokenKind } from '../parser/token-kind' import { TokenKind, parseStringLiteral } from '../parser'
import { parseStringLiteral } from '../parser/parse-string-literal'
export class PropertyAccessToken extends Token { export class PropertyAccessToken extends Token {
public propertyName: string public propertyName: string
+1 -1
View File
@@ -1,5 +1,5 @@
import { Token } from './token' import { Token } from './token'
import { TokenKind } from '../parser/token-kind' import { TokenKind } from '../parser'
export class QuotedToken extends Token { export class QuotedToken extends Token {
constructor ( constructor (
+1 -1
View File
@@ -1,6 +1,6 @@
import { Token } from './token' import { Token } from './token'
import { ValueToken } from './value-token' import { ValueToken } from './value-token'
import { TokenKind } from '../parser/token-kind' import { TokenKind } from '../parser'
export class RangeToken extends Token { export class RangeToken extends Token {
constructor ( constructor (
+2 -3
View File
@@ -1,8 +1,7 @@
import { DelimitedToken } from './delimited-token' import { DelimitedToken } from './delimited-token'
import { TokenizationError } from '../util/error' import { TokenizationError } from '../util/error'
import { NormalizedFullOptions } from '../liquid-options' import { Tokenizer, TokenKind } from '../parser'
import { TokenKind } from '../parser/token-kind' import type { NormalizedFullOptions } from '../liquid-options'
import { Tokenizer } from '../parser/tokenizer'
export class TagToken extends DelimitedToken { export class TagToken extends DelimitedToken {
public name: string public name: string
+1 -1
View File
@@ -1,4 +1,4 @@
import { TokenKind } from '../parser/token-kind' import { TokenKind } from '../parser'
export abstract class Token { export abstract class Token {
public constructor ( public constructor (
+5
View File
@@ -0,0 +1,5 @@
import type { TagToken } from './tag-token'
import type { HTMLToken } from './html-token'
import type { OutputToken } from './output-token'
export type TopLevelToken = TagToken | OutputToken | HTMLToken
-5
View File
@@ -1,5 +0,0 @@
import { TagToken } from './tag-token'
import { HTMLToken } from './html-token'
import { OutputToken } from './output-token'
export type TopLevelToken = TagToken | OutputToken | HTMLToken
-32
View File
@@ -1,32 +0,0 @@
/* istanbul ignore file */
import * as TypeGuards from './util/type-guards'
export { TypeGuards }
export { ParseError, TokenizationError, AssertionError } from './util/error'
export { assert } from './util/assert'
export { Drop } from './drop/drop'
export { Emitter } from './emitters/emitter'
export { Expression } from './render/expression'
export { isFalsy, isTruthy } from './render/boolean'
export { TagToken } from './tokens/tag-token'
export { Context } from './context/context'
export { Template } from './template/template'
export { FilterImplOptions } from './template/filter/filter-impl-options'
export { TagImplOptions } from './template/tag/tag-impl-options'
export { TagImpl } from './template/tag/tag-impl'
export { ParseStream } from './parser/parse-stream'
export { Token } from './tokens/token'
export { TokenKind } from './parser/token-kind'
export { TopLevelToken } from './tokens/toplevel-token'
export { Tokenizer } from './parser/tokenizer'
export { Hash } from './template/tag/hash'
export { Value } from './template/value'
// eslint-disable-next-line deprecation/deprecation
export { _evalToken, evalToken, evalQuotedToken } from './render/expression'
export { toPromise, toValueSync } from './util/async'
export { defaultOperators, Operators } from './render/operator'
export { createTrie, Trie } from './util/operator-trie'
export { toValue } from './util/underscore'
export { TimezoneDate } from './util/timezone-date'
export { filters } from './filters'
export { tags } from './tags'
export { defaultOptions } from './liquid-options'
+3 -3
View File
@@ -1,11 +1,11 @@
import { isNil, isString, isObject, isArray, isIterable, toValue } from './underscore' import { isNil, isString, isObject, isArray, isIterable, toValue } from './underscore'
export function toEnumerable (val: any) { export function toEnumerable<T = unknown> (val: any): T[] {
val = toValue(val) val = toValue(val)
if (isArray(val)) return val if (isArray(val)) return val
if (isString(val) && val.length > 0) return [val] if (isString(val) && val.length > 0) return [val] as unknown as T[]
if (isIterable(val)) return Array.from(val) if (isIterable(val)) return Array.from(val)
if (isObject(val)) return Object.keys(val).map((key) => [key, val[key]]) if (isObject(val)) return Object.keys(val).map((key) => [key, val[key]]) as unknown as T[]
return [] return []
} }

Some files were not shown because too many files have changed in this diff Show More