docs: update docs and demo for Value usage, fixes #568

This commit is contained in:
Harttle
2022-12-14 02:14:03 +08:00
committed by Harttle
parent a9f93d7235
commit d24655887f
12 changed files with 187 additions and 127 deletions
-11
View File
@@ -8,17 +8,6 @@ const engine = new Liquid({
extname: '.liquid' extname: '.liquid'
}) })
engine.registerTag('header', {
parse: function (token) {
const [key, val] = token.args.split(':')
this[key] = val
},
render: function (ctx) {
const title = this.liquid.evalValue(this.content, ctx)
return `<h1>${title}</h1>`
}
})
function demo () { function demo () {
console.log(' demo') console.log(' demo')
console.log('------------------------') console.log('------------------------')
+8 -8
View File
@@ -1,4 +1,4 @@
const { Liquid } = require('liquidjs') const { Liquid, Tag, Value } = require('liquidjs')
const engine = new Liquid({ const engine = new Liquid({
extname: '.liquid', extname: '.liquid',
@@ -11,13 +11,13 @@ const engine = new Liquid({
partials: './partials' partials: './partials'
}) })
engine.registerTag('header', { engine.registerTag('header', class HeaderTag extends Tag {
parse: function (token) { constructor (token, remainTokens, liquid) {
const [key, val] = token.args.split(':') super(token, remainTokens, liquid)
this[key] = val this.value = new Value(token.args, liquid)
}, }
render: async function (scope, emitter) { * render (ctx, emitter) {
const title = await this.liquid.evalValue(this.content, scope) const title = yield this.value.value(ctx)
emitter.write(`<h1>${title}</h1>`) emitter.write(`<h1>${title}</h1>`)
} }
}) })
+1 -1
View File
@@ -1,5 +1,5 @@
{% layout 'html.liquid' %} {% layout 'html.liquid' %}
{%header content: title | capitalize%} {%header title | capitalize%}
<ul> <ul>
{% for todo in todos %} {% for todo in todos %}
{% render 'todo.liquid' with todo, index: forloop.index%} {% render 'todo.liquid' with todo, index: forloop.index%}
+11 -10
View File
@@ -1,24 +1,25 @@
import { Liquid, TagToken, Context, Emitter } from 'liquidjs' import { Value, Liquid, TagToken, Context, Emitter, Tag, TopLevelToken } from 'liquidjs'
const engine = new Liquid({ const engine = new Liquid({
root: __dirname, root: __dirname,
extname: '.liquid' extname: '.liquid'
}) })
engine.registerTag('header', { engine.registerTag('header', class HeaderTag extends Tag {
parse: function (token: TagToken) { private value: Value
const [key, val] = token.args.split(':') constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
this[key] = val super(token, remainTokens, liquid)
}, this.value = new Value(token.args, liquid)
render: async function (context: Context, emitter: Emitter) { }
const title = await this.liquid.evalValue(this['content'], context) * render (ctx: Context, emitter: Emitter) {
const title = yield this.value.value(ctx)
emitter.write(`<h1>${title}</h1>`) emitter.write(`<h1>${title}</h1>`)
} }
}) })
const ctx = { const scope = {
todos: ['fork and clone', 'make it better', 'make a pull request'], todos: ['fork and clone', 'make it better', 'make a pull request'],
title: 'Welcome to liquidjs!' title: 'Welcome to liquidjs!'
} }
engine.renderFile('todolist', ctx).then(console.log) engine.renderFile('todolist', scope).then(console.log)
+1 -1
View File
@@ -1,4 +1,4 @@
{%header content: title | capitalize%} {%header title | capitalize%}
<ul> <ul>
{% for todo in todos %} {% for todo in todos %}
+5 -3
View File
@@ -1,7 +1,9 @@
{ {
"compilerOptions": { "compilerOptions": {
"types": [ "target": "es6",
"node" "moduleResolution": "node",
] "types": [
"node"
]
} }
} }
@@ -6,14 +6,14 @@ title: Register Filters/Tags
```typescript ```typescript
// Usage: {% upper name %} // Usage: {% upper name %}
import { TagToken, Context, Emitter, TopLevelToken } from 'liquidjs' import { Value, TagToken, Context, Emitter, TopLevelToken } from 'liquidjs'
engine.registerTag('upper', { engine.registerTag('upper', {
parse: function(tagToken: TagToken, remainTokens: TopLevelToken[]) { parse: function(tagToken: TagToken, remainTokens: TopLevelToken[]) {
this.str = tagToken.args; // name this.value = new Value(token.args, liquid)
}, },
render: function*(ctx: Context) { render: function*(ctx: Context) {
const str = yield this.liquid.evalValue(this.str, ctx); // 'alice' const str = yield this.value.value(ctx); // 'alice'
return str.toUpperCase() // 'ALICE' return str.toUpperCase() // 'ALICE'
} }
}); });
@@ -41,7 +41,7 @@ engine.registerTag('upper', class UpperTag extends Tag {
}); });
``` ```
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/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
## Register Filters ## Register Filters
@@ -58,7 +58,7 @@ Filter arguments will be passed to the registered filter function, for example:
engine.registerFilter('add', (initial, arg1, arg2) => initial + arg1 + arg2) engine.registerFilter('add', (initial, arg1, arg2) => initial + arg1 + arg2)
``` ```
See existing filter implementations here: <https://github.com/harttle/liquidjs/tree/master/src/builtin/filters> See existing filter implementations here: <https://github.com/harttle/liquidjs/tree/master/src/filters>
## Unregister Tags/Filters ## Unregister Tags/Filters
+65 -39
View File
@@ -24,67 +24,93 @@ The synchronous version of methods contains a `Sync` suffix:
## Implement Sync-Compatible Tags ## Implement Sync-Compatible Tags
### Requirements LiquidJS uses a generator-based async implementation to support both async and sync in one piece of tag implementation. For example, below `UpperTag` can be used in both `engine.renderSync()` and `engine.render()`.
All builtin tags are *sync-compatible* and safe to use for both sync and async APIs. To make your custom tag *sync-compatible*, you'll need to avoid return a `Promise`. That means the `render(context, emitter)`: ```typescript
import { TagToken, Context, Emitter, TopLevelToken, Value, Tag, Liquid } from 'liquidjs'
- Should not directly `return <Promise>`, and // Usage: {% upper "alice" %}
- Should not be declared as `async`. // Output: ALICE
engine.registerTag('upper', class UpperTag extends Tag {
private value: Value
constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(token, remainTokens, liquid)
this.value = new Value(token.args, liquid)
}
* render (ctx: Context, emitter: Emitter) {
const title = yield this.value.value(ctx)
emitter.write(title.toUpperCase())
}
})
```
All builtin tags are implemented this way and safe to use in both sync and async (I'll call it *sync-compatible*). To make your custom tag *sync-compatible*, you'll need to:
- declare render function as `* render()`, in which
- do not directly `return <Promise>`, and
- do not call any APIs that returns a Promise.
## Call APIs that return a Promise
But LiquidJS is Promise-friendly, right? You can still call Promise-based functions and wait for that Promise within tag implementations. Just replace `await` with `yield`. e.g. we're calling `fs.readFile()` which returns a `Promise`:
```typescript
* render (ctx: Context, emitter: Emitter) {
const file = yield this.value.value(ctx)
const title = yield fs.readFile(file, 'utf8')
emitter.write(title.toUpperCase())
}
```
Now that this `* render()` calls an API that returns a Promise, so it's no longer *sync-compatible*.
{% note info Non Sync-Compatible Tags %} {% note info Non Sync-Compatible Tags %}
Non <em>sync-compatible</em> tags are also valid tags, will work just fine for asynchronous API calls. When called synchronously, tags that return a <code>Promise</code> will be rendered as <code>[object Promise]</code>. Non <em>sync-compatible</em> tags are also valid tags, will work just fine for asynchronous API calls. When called synchronously, tags that return a <code>Promise</code> will be rendered as <code>[object Promise]</code>.
{% endnote %} {% endnote %}
### Await Promises ## Convert LiquidJS async Generator to Promise
But LiquidJS is Promise-friendly, right? You can still call Promise-based functions and wait for that Promise within tag implementations. Just replace `await` with `yield` and keep `* render()` instead of `async render()`. e.g.
You can convert a Generator to Promise by [toPromise][toPromise], for example:
```typescript ```typescript
import { TagToken, Context, Emitter, TopLevelToken } from 'liquidjs' import { TagToken, Context, Emitter, TopLevelToken, Value, Tag, Liquid, toPromise } from 'liquidjs'
// Usage: {% upper "alice" %} // Usage: {% upper "alice" %}
// Output: ALICE // Output: ALICE
engine.registerTag('upper', { engine.registerTag('upper', class UpperTag extends Tag {
parse: function(tagToken: TagToken, remainTokens: TopLevelToken[]) { private value: Value
this.str = tagToken.args constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
}, super(token, remainTokens, liquid)
* render: function(ctx: Context) { this.value = new Value(token.args, liquid)
// _evalValue will behave synchronously when called by synchronous API }
// in which case `ctx.sync == true` async render (ctx: Context, emitter: Emitter) {
var str = yield this.liquid._evalValue(this.str, ctx) const title = await toPromise(this.value.value(ctx))
return str.toUpperCase() emitter.write(title.toUpperCase())
} }
}) })
``` ```
See this JSFiddle: <http://jsfiddle.net/ctj364up/6/>. ## Async only Tags
## Async-only Tags If your tag is intend to be used only asynchronously, it can be declared as `async render()` so you can use `await` in its implementation directly:
For tags that intended to be used only by async API, or those cannot be implemented synchronously, there's no difference between using generator-base syntax or async syntax. I'll call them *async-only tags*.
For example, if the above `this.liquid._evalValue()` doesn't respect `ctx.sync` and always returns a Promise, even if the tag is implemented using `* render()` and `yield this.liquid._evalValue()`, it will be rendered as `<object Promise>` anyway.
For *async-only tags*, you can use async syntax at will. Be careful some APIs in LiquidJS return Promises and others return Generators. You'll need [toPromise][toPromise] API to convert a Generator to a Promise, for example:
```typescript ```typescript
import { TagToken, Context, Emitter, TopLevelToken, toPromise } from 'liquidjs' import { toPromise, TagToken, Context, Emitter, TopLevelToken, Value, Tag, Liquid } from 'liquidjs'
// Usage: {% upper "alice" %} // Usage: {% upper "alice" %}
// Output: ALICE // Output: ALICE
engine.registerTag('upper', { engine.registerTag('upper', class UpperTag extends Tag {
parse: function(tagToken: TagToken, remainTokens: TopLevelToken[]) { private value: Value
this.str = tagToken.args; // name constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
}, super(token, remainTokens, liquid)
render: async function(ctx: Context) { this.value = new Value(token.args, liquid)
var str = await toPromise(this.liquid._evalValue(this.str, ctx)); }
// Or use the alternate API that returns a Promise async render (ctx: Context, emitter: Emitter) {
// var str = await this.liquid.evalValue(this.str, ctx); const title = await toPromise(this.value.value(ctx))
return str.toUpperCase() emitter.write(`<h1>${title}</h1>`)
} }
}); })
``` ```
See this JSFiddle: <http://jsfiddle.net/ctj364up/5/>.
[Liquid]: /api/classes/liquid_.liquid.html [Liquid]: /api/classes/liquid_.liquid.html
[toPromise]: /api/modules/liquid_.html#toPromise [toPromise]: /api/modules/liquid_.html#toPromise
@@ -8,10 +8,10 @@ title: 注册标签和过滤器
// 使用方式: {% upper name %} // 使用方式: {% upper name %}
engine.registerTag('upper', { engine.registerTag('upper', {
parse: function(tagToken, remainTokens) { parse: function(tagToken, remainTokens) {
this.str = tagToken.args; // name this.value = new Value(token.args, liquid)
}, },
render: async function(scope, hash) { render: function*(scope, hash) {
var str = await this.liquid.evalValue(this.str, scope); // 'alice' const str = yield this.value.value(ctx); // 'alice'
return str.toUpperCase() // 'Alice' return str.toUpperCase() // 'Alice'
} }
}); });
@@ -20,7 +20,26 @@ engine.registerTag('upper', {
* `parse`: 从 `remainTokens` 中读取后续的标签/输出/HTML,直到找到你期望的结束标签。 * `parse`: 从 `remainTokens` 中读取后续的标签/输出/HTML,直到找到你期望的结束标签。
* `render`: 把 scope 数据和此前解析得到的 Token 结合,输出 HTML 字符串。 * `render`: 把 scope 数据和此前解析得到的 Token 结合,输出 HTML 字符串。
查看已有的标签实现:<https://github.com/harttle/liquidjs/tree/master/src/builtin/tags> 对于更复杂的标签实现,可以提供一个继承自 `Tag` 的类:
```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'
}
});
```
可以参考已有的标签实现:<https://github.com/harttle/liquidjs/tree/master/src/tags>
## 注册过滤器 ## 注册过滤器
@@ -36,7 +55,7 @@ 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/filters>。对于复杂的标签,也可以用一个类来实现:
```typescript ```typescript
// Usage: {% upper name:"alice" %} // Usage: {% upper name:"alice" %}
+65 -42
View File
@@ -24,66 +24,89 @@ LiquidJS 支持同步调用也支持异步调用,支持 Promise。为了同异
## 如何实现兼容同步的标签 ## 如何实现兼容同步的标签
### 要求 LiquidJS 使用基于生成器的异步实现,来让同一份代码支持同步和异步调用。例如下面的 `UpperTag` 既可以用于 `engine.renderSync()` 也可以用于 `engine.render()`:
所有内置标签都兼容同步,可以安全地用于同步或异步 API。为了让你的自定义标签页支持同步,你的标签不能返回 `Promise`,这意味着你的 `render(context, emitter)` 函数:
- 不能直接 `return <Promise>`,
- 也不能声明为 `async`。
{% note info 不兼容同步的标签 %}
不兼容同步的标签也仍然是合法标签,在异步 API 下也会正常运行。被同步调用时,返回 <code>Promise</code> 的标签会被渲染成 <code>[object Promise]</code>。
{% endnote %}
### 等待 Promise
但 LiquidJS 是支持 `Promise` 的,你仍然可以调用返回 `Promise` 的方法并等它 resolve。只需要把 `await` 换成 `yield` 并保留 `* render()` 不要改成 `async render()`。例如:
```typescript ```typescript
import { TagToken, Context, Emitter, TopLevelToken } from 'liquidjs' import { TagToken, Context, Emitter, TopLevelToken, Value, Tag, Liquid } from 'liquidjs'
// Usage: {% upper "alice" %} // Usage: {% upper "alice" %}
// Output: ALICE // Output: ALICE
engine.registerTag('upper', { engine.registerTag('upper', class UpperTag extends Tag {
parse: function(tagToken: TagToken, remainTokens: TopLevelToken[]) { private value: Value
this.str = tagToken.args constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
}, super(token, remainTokens, liquid)
* render: function(ctx: Context) { this.value = new Value(token.args, liquid)
// 同步调用时 `ctx.sync == true`,`_evalValue()` 会同步地执行 }
var str = yield this.liquid._evalValue(this.str, ctx) * render (ctx: Context, emitter: Emitter) {
return str.toUpperCase() const title = yield this.value.value(ctx)
} emitter.write(title.toUpperCase())
}
}) })
``` ```
见这个 JSFiddle:<http://jsfiddle.net/ctj364up/6/>。 所有内置标签都兼容同步,可以安全地用于同步或异步 API。实现同时支持同异步的标签,需要:
## 只支持异步的标签 - render 函数声明成 `* render()`,并且在里面
- 不能直接 `return <Promise>`,
- 不能调用会返回 Promise 的函数。
对于只用于异步 API 的标签,或者只能实现为异步的标签,使用生成器语法和 async 语法并没有区别。 ## 调用返回 Promise 的函数
例如,如果上面的 `this.liquid._evalValue()` 不会检查 `ctx.sync` 而且总是返回一个 `Promise`,那么即使这个标签用 `* render()` 和 `yield this.liquid._evalValue()` 实现,最终也会渲染成 `<object Promise>`。 但 LiquidJS 是支持 `Promise` 的,你仍然可以调用返回 `Promise` 的方法并等它 resolve。只需要把 `await` 换成 `yield`。例如:
这时可以直接使用 async 语法。注意有些 LiquidJS API 会返回 `Promise`,有些会返回生成器。你需要用 [toPromise][toPromise] API 来把生成器转换为 `Promise`,比如:
```typescript ```typescript
import { TagToken, Context, Emitter, TopLevelToken, toPromise } from 'liquidjs' * render (ctx: Context, emitter: Emitter) {
const file = yield this.value.value(ctx)
const title = yield fs.readFile(file, 'utf8')
emitter.write(title.toUpperCase())
}
```
现在 `* render()` 调用了一个返回 Promise 的 API,它就不再兼容同步了。不兼容同步的标签也仍然是合法标签,在异步 API 下也会正常运行。被同步调用时,返回 <code>Promise</code> 的标签会被渲染成 <code>[object Promise]</code>。
## 把 LiquidJS 生成器转换成 Promise
有些 LiquidJS API 会返回 `Promise`,有些会返回生成器。你可以用 [toPromise][toPromise] 来把生成器转换为 `Promise`,比如:
```typescript
import { TagToken, Context, Emitter, TopLevelToken, Value, Tag, Liquid, toPromise } from 'liquidjs'
// Usage: {% upper "alice" %} // Usage: {% upper "alice" %}
// Output: ALICE // Output: ALICE
engine.registerTag('upper', { engine.registerTag('upper', class UpperTag extends Tag {
parse: function(tagToken: TagToken, remainTokens: TopLevelToken[]) { private value: Value
this.str = tagToken.args; // name constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
}, super(token, remainTokens, liquid)
render: async function(ctx: Context) { this.value = new Value(token.args, liquid)
var str = await toPromise(this.liquid._evalValue(this.str, ctx)); }
// Or use the alternate API that returns a Promise async render (ctx: Context, emitter: Emitter) {
// var str = await this.liquid.evalValue(this.str, ctx); const title = await toPromise(this.value.value(ctx))
return str.toUpperCase() emitter.write(title.toUpperCase())
} }
}); })
``` ```
见这个 JSFiddle:<http://jsfiddle.net/ctj364up/5/>。 ## 纯异步标签
如果你的标签就不打算支持同步,可以干脆实现成 `async render()`,这样就可以使用更熟悉的 `await` 了:
```typescript
import { toPromise, TagToken, Context, Emitter, TopLevelToken, Value, Tag, Liquid } from 'liquidjs'
// Usage: {% upper "alice" %}
// Output: ALICE
engine.registerTag('upper', class UpperTag extends Tag {
private value: Value
constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(token, remainTokens, liquid)
this.value = new Value(token.args, liquid)
}
async render (ctx: Context, emitter: Emitter) {
const title = await toPromise(this.value.value(ctx))
emitter.write(`<h1>${title}</h1>`)
}
})
```
[Liquid]: /api/classes/liquid_.liquid.html [Liquid]: /api/classes/liquid_.liquid.html
[toPromise]: /api/modules/liquid_.html#toPromise [toPromise]: /api/modules/liquid_.html#toPromise
+1 -1
View File
@@ -79,7 +79,7 @@ export class Liquid {
public _evalValue (str: string, scope?: object | Context): IterableIterator<any> { public _evalValue (str: string, scope?: object | Context): IterableIterator<any> {
const value = new Value(str, this) const value = new Value(str, this)
const ctx = scope instanceof Context ? scope : new Context(scope, this.options) const ctx = scope instanceof Context ? scope : new Context(scope, this.options)
return value.value(ctx, false) return value.value(ctx)
} }
public async evalValue (str: string, scope?: object | Context): Promise<any> { public async evalValue (str: string, scope?: object | Context): Promise<any> {
return toPromise(this._evalValue(str, scope)) return toPromise(this._evalValue(str, scope))
+1 -1
View File
@@ -17,7 +17,7 @@ export class Value {
this.initial = tokenizer.readExpression() this.initial = tokenizer.readExpression()
this.filters = tokenizer.readFilters().map(({ name, args }) => new Filter(name, this.getFilter(liquid, 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')
let val = yield this.initial.evaluate(ctx, lenient) let val = yield this.initial.evaluate(ctx, lenient)